[
  {
    "path": ".agents/skills/llamactl-qa/SKILL.md",
    "content": "---\nname: llamactl-qa\ndescription: Plan and run a design-QA / \"taste test\" of llamactl changes against a real backend. Cooperatively builds a small matrix of cases worth eyeballing, runs them against a chosen backend (test environment by default, local kind+tilt for new API contract changes), and writes a design-review report to `thoughts/shared/qa/`. Use this for changes to `llamactl` commands, output formats, auth, or control-plane API contracts. For UI/template smoke tests, use `llamactl_browser_test` instead.\n---\n\n# llamactl CLI QA\n\nA design QA, not an integration test. The goal is to put the actual rendered output in front of a human reviewer so they can judge whether the design is right — alignment, naming, key ordering, error wording, what's noisy, what's missing. The LLM's job is to set up well-chosen cases and call out what looks off; the human's job is to read the outputs and decide.\n\nTwo halves. First, a planning pass that turns \"I changed X\" into a small, sign-off-able matrix of cases worth running. Second, an execution pass that runs them and writes a report whose body is mostly verbatim output plus design-review notes.\n\nEvery row in the matrix is there because we agreed it covers a real risk or design question in the diff.\n\n## Mode flags\n\nParse these from the user's invocation before doing anything else:\n\n- `--auto`, `auto`, `fully auto`, `non-chatty`, or `-f` means run autonomously. Pick the backend, draft the matrix internally, execute it, write the report, and return only the report pointer plus any blocking issue. Do not ask for backend selection or matrix confirmation.\n- In auto mode, prefer the least risky backend that still exercises the changed surface. Use offline rows for parse/render/local-file behavior, local kind+tilt for new control-plane contracts, and a test environment for read-only checks or safe writes. **Never use production for mutating QA.** Switch to a test environment first.\n- Auto mode does not relax mutation rules. When creating deployments for QA (e.g., to test edit or delete flows), always create your own throwaway deployments rather than editing existing ones that might be important. Clean them up at the end of the run.\n\n### What this is NOT\n\n- **Not an automated integration test.** No `jq has(...)` shape assertions, no `python -c \"assert ...\"`. If a row reads as \"the command works\", drop it; if it reads as \"let's see the output and judge it\", keep it. Real integration tests are a separate task (authoring real pytest cases against the API), not this one.\n- **Not exhaustive coverage.** 4–8 well-chosen rows. The point is taste, not a regression matrix.\n- **Not pass/fail.** The report's job is \"here's what the surface actually looks like\", not \"all green\".\n\nllamactl is churning right now (multi-slice rework: output modes, apply/delete, template/apply-loop). Treat this skill as a living reference. When a run teaches you something about llamactl that isn't in here, edit the skill before closing the task. See \"Self-update\" at the bottom.\n\n## When to use\n\n- Changes to `llamactl` commands (new flags, new output modes, command splits, behavior changes).\n- Changes to control-plane API contracts that llamactl consumes.\n- Changes to auth / profile / project resolution.\n\nSkip when the change is fully covered by pytest, or when the failure mode is in the UI (use `llamactl_browser_test`).\n\n## Pick a backend\n\n1. **Test environment / test project.** Default for QA that creates or mutates deployments. Use a non-production environment or a dedicated test project so throwaway deployments don't pollute real workspaces. **Never run mutating QA commands against production.** When creating test deployments (to later edit/delete them as part of the QA), create them yourself rather than editing existing deployments that may be important. Pin every command with `--project <test-project-id>` so an active-profile slip can't write into the wrong project.\n2. **Local kind + tilt.** When the change is a new API contract (new endpoint, new field, new behavior on the control plane) and you need to exercise it against a control plane that actually has the change. `uv run operator/dev.py up`. See `AGENTS.md` and `operator/AGENTS.md` for setup. Local mode runs with no auth: the `http://localhost:8011` env is preconfigured; switch to it with `llamactl environments use http://localhost:8011` (arg is the API URL, not a name) and a `default` profile is created automatically. No tokens, no `auth login`. Before drafting the matrix, list existing deployments with `kubectl get llamadeployments -A` — there's often something already in the cluster from prior work that you can target with `--project <id>`, saving the cost of a fresh `deployments create`.\n3. **Offline only.** For changes that only affect local rendering (template output, help text, error formatting), no backend is needed at all. Run commands that don't hit the API (e.g., `deployments template`, `--help`, non-interactive error paths).\n\nAsk the user which backend before drafting the matrix. The answer changes which rows are realistic. Skip this in auto mode and choose from the rules above.\n\n## Test project scaffolds\n\nSome QA needs a real on-disk project (anything that reads `cwd` — e.g., `deployments template`, `serve`, `apply -f` once it lands, anything that consults `pyproject.toml`'s `[tool.llamadeploy]` block, the `.env`, or git state). Use `llamactl init` into a temp dir rather than hand-rolling files or reusing your dev tree.\n\n```bash\nWORK=$(mktemp -d -t llamactl-qa.XXXXXX)\nuv run llamactl init --no-interactive --template basic --dir \"$WORK/app\"\n```\n\nWhat you get:\n\n- A scaffolded project with `pyproject.toml` ([tool.llamadeploy] block), `.env.template`, `src/`, `tests/`.\n- An initial git commit (`init` runs `git init` + commit). `is_git_repo` is true; `git_ref` is `main`; no remote.\n- No `.env` (only `.env.template`). Copy/edit if a row needs `available_secrets` populated: `cp \"$WORK/app/.env.template\" \"$WORK/app/.env\"` then append e.g. `OPENAI_API_KEY=test`.\n- Available templates: `basic-ui`, `showcase`, `document-qa`, `extraction-review`, `classify-extract-sec`, `extract-reconcile-invoice`, `basic`, `document_parsing`, `human_in_the_loop`, `invoice_extraction`, `rag`, `web_scraping`. `basic` is fastest and has no required secrets — use it unless the row needs a template that lists required secrets.\n\nFor rows that need a non-git directory: `mktemp -d` and don't init. For rows that need a remote: `git -C \"$WORK/app\" remote add origin https://github.com/<you>/qa.git`. Don't push.\n\nRun commands from inside the project: `(cd \"$WORK/app\" && llamactl deployments template)`. Don't `cd` in the parent shell — keeps rows independent. Capture full paths in the report so the reader can recreate.\n\nCleanup is `rm -rf \"$WORK\"` at the end of the run. Mention it in the report's footer.\n\nIf a row needs to **commit** changes (e.g., to test git-state-dependent behavior), do it inside the temp tree with a fresh `user.email` / `user.name` so the host's git config doesn't bleed in:\n\n```bash\ngit -C \"$WORK/app\" -c user.email=qa@example.com -c user.name=qa commit -am \"<msg>\"\n```\n\nDon't try to make this temp project deployable to a real backend in the same run as offline QA. If a row needs a server-side deployment to edit or inspect, create a throwaway deployment yourself (e.g., `create -f` with a minimal YAML + `--no-push`) rather than editing an existing deployment that might be important. Clean it up with `deployments delete` at the end of the run.\n\n## llamactl mental model\n\nThe bits that matter when designing a matrix.\n\n**Scoping hierarchy**: environments → profiles → organizations → projects → deployments. An environment is a control-plane URL (e.g., `http://localhost:8011` for local). Each environment can have one or more profiles (authenticated identities). Each profile has an active organization, and each organization contains projects. Deployments live inside projects. `llamactl environments get` shows environments. `llamactl auth get` shows profiles in the active environment. `llamactl environments use <url>` changes environment. `llamactl projects use [id]` changes the active project within the current profile.\n\nA profile is `(env, oauth tokens, active org, active project)`. `--project <id>` overrides the profile's active project for one call. Before running any QA, check `llamactl environments get` and `llamactl auth get` to confirm you're in the right environment and project — don't assume from a previous session. There are multiple environments with deployments available for testing; use a non-production one for mutating operations.\n\nRead commands accept `-o text|json|yaml`. Text is human-facing; json/yaml are assertable. Prefer json for QA — exact-match assertions catch field-rename regressions text formatting hides.\n\nRead commands (`deployments get/list/history/logs`, `auth get`, `environments get`) do not need `--no-interactive` when their argument is supplied. The flag is a no-op there post-Slice-A.5 and slated to be removed in the parent plan's Phase 3. Don't pad commands with it. Write/select commands (`deployments delete`, `deployments edit`, `deployments rollback` without `--git-sha`) still branch on the interactive flag — pass `--no-interactive` for those if you don't want a prompt, or supply every required arg.\n\nThe command surface is moving. Run `--help` on any command you're testing before assuming flag names; don't go from memory.\n\n## Cooperative planning\n\nDon't run anything until the user has signed off on the matrix. Skip confirmation in auto mode.\n\n1. Read the diff. `git diff <base>...HEAD --stat` and targeted reads on the command files. Identify the behavior surface: which subcommands, flags, output formats, endpoints changed.\n2. List design questions, not features. One line each. The right question reads as \"would the user be happy seeing this output?\", not \"does the command run\". Examples: \"Does the `spec:` / `status:` split actually feel right when you eyeball `get -o yaml`?\" \"Does the plain-table list output stay readable for a deployment whose repo URL is 80 chars?\" \"Does the no-secrets case render as cleanly as the with-secrets case?\"\n3. Map questions to a small matrix. One row per case. 4–8 rows is usually right; 12 means you're testing features.\n4. Present the matrix inline. Wait for the user to keep, cut, or add rows. In auto mode, keep this matrix internal and run it.\n\nMatrix row format. Note \"Look for\" replaces a pass-fail \"Expect\" — the reviewer is meant to read the output and judge against these prompts, not check a list of facts.\n\n| # | Command | Backend | Look for | Covers |\n|---|---|---|---|---|\n| 1 | `deployments get my-app -o yaml --project <test>` | test-env | spec/status split reads cleanly; field ordering; null vs omitted; PAT presentation | new display model shape |\n| 2 | `deployments get --project <test>` | test-env | column choices, alignment, behavior with long repo URLs | plain-table list mode |\n| 3 | `deployments get my-app --project <other-test>` vs default | test-env | does the override actually retarget the project | `--project` override |\n\nIf a row's \"Look for\" reads as \"the command works\", drop it. If it reads as \"I'd want to eyeball this\", keep it.\n\n## Execution\n\nRun rows top to bottom. For each, the goal is to capture the actual output the human reviewer will judge.\n\nCapture:\n\n- The exact command (including inline env vars).\n- Exit code and elapsed time (latency is part of UX).\n- **Verbatim stdout and stderr.** Don't trim, don't summarize, don't paraphrase. The report's body is the output, not your verdict on it. Truncation only when output is genuinely huge (200+ line log dumps); keep head, tail, and a `... <N> lines elided ...` marker.\n- Your design-review observations as one-line \"Notes\" — what a thoughtful reviewer would point at. \"REPO column is 67 chars wide on a 100-char terminal — leaves no room for two repos to align.\" \"`status.warning: null` reads cleanly; the omitted vs explicit-null asymmetry is visible.\" \"Why are both `display_name` and `name` here when they're equal?\"\n\nWhat NOT to do:\n\n- **No `jq has(...)` / `python -c \"assert ...\"` style assertions.** The report is for human judgment, not pass/fail. If you find yourself asserting a key exists, you're in the wrong skill — that's pytest territory.\n- **Don't pre-judge.** Notes flag things to look at; the reader judges. \"Suspicious: `personal_access_token` shown as `********` even when the underlying secret name is also `GITHUB_PAT`\" is fine. \"Wrong: PAT should be unmasked\" is not — that's the reader's call.\n- **Don't paper over surprises.** If a row produces a stack trace or an unexpected shape, capture it verbatim and call it out in Highlights. Re-running with different flags to make the symptom go away is forbidden; that's the QA finding.\n\nFor comparison rows (`--project` override, `list` vs `get`, text vs json, with-data vs empty), capture both calls in the same row, side by side. The reader should be able to eyeball the difference without scrolling.\n\nLong-running commands (`serve`, `logs --follow`) go in the background with `run_in_background: true`. Stop them before moving on.\n\n### Storing raw transcripts\n\nDon't dump `.log` / `.out` / `.err` files into `thoughts/shared/qa/`. The report is the artifact. Embed verbatim outputs in the report with fenced blocks.\n\nIf a single output is genuinely too large to embed (200+ lines of logs, many-deployment dumps), put it in `thoughts/shared/qa/raw/<date>-<scope>/<row-id>.txt` and link it from the report row. Default to embedding; the subdir is the exception, not the rule.\n\n## Report\n\nThe report is a design and interaction review surface, not a test result. The reader scrolls through it and forms their own opinion: are columns aligned, is naming consistent, does this command feel right, is the error message clear. Your job is to put the evidence in front of them and flag the design questions worth their attention.\n\nWrite to `thoughts/shared/qa/<date>-llamactl-<scope>.md`. Post a one-or-two-sentence pointer inline plus the file link. The user reads the file, not your inline summary.\n\nTemplate:\n\n````\n# llamactl QA: <one-line scope>\n\nBackend: <test environment | local kind | offline | other>\nProfile: <name> (<env URL>)\nProject: <test project id/name>\nBranch: <branch>  Commit: <short sha>\n\n## Design questions for the reviewer\n\n<3–6 bullets. These are the things you want the reader's eyes on. Not \"all rows passed\". Examples:\n- \"Is `spec:` + `status:` the right split, or is the extra indent annoying?\"\n- \"REPO column gets pushed off-screen when the URL isn't a github short. Do we want a `--wide` mode or terminal-aware wrapping?\"\n- \"`auth get` text mode shows ACTIVE as yes/no — is that legible enough?\"\n- \"When secrets are unset, the JSON spec block is just `{display_name, repo_url, deployment_file_path, suspended: false}`. Is `suspended: false` worth showing on a fresh deploy or is it noise?\"\n\nLean open-ended. If a question has an obvious answer, it doesn't belong here.>\n\n## Rows\n\n### 1. <one-line description, e.g. \"deployments get my-app -o yaml\">\n\nCommand:\n```\n$ <exact command>\n```\n\nOutput (exit=<n>, <elapsed>):\n```\n<verbatim stdout>\n```\n<if stderr non-empty:>\nStderr:\n```\n<verbatim stderr>\n```\n\nNotes:\n- <one-line observation a thoughtful reviewer would make>\n- <another, if there's something else worth flagging>\n\n### 2. <comparison row>\n\nCommand A:\n```\n$ <command 1>\n```\n\nOutput A (exit=<n>):\n```\n<output>\n```\n\nCommand B:\n```\n$ <command 2>\n```\n\nOutput B (exit=<n>):\n```\n<output>\n```\n\nNotes:\n- <what the side-by-side reveals>\n\n... same shape for remaining rows ...\n\n## Followups\n\n- <design questions that need a human answer>\n- <regressions or design choices that should turn into issues / changes>\n- <items the matrix didn't cover but the reader should know about>\n````\n\nRules:\n\n- **Show, don't tell.** \"Output shows a spec block with editable fields\" → no. Paste the YAML. The reader can see the shape.\n- **Keep verbatim formatting.** Tables in fenced blocks preserve column rendering. Don't reformat or \"clean up\" output.\n- **Comparison rows live in one section.** Two adjacent fenced blocks labeled clearly beat two separate rows.\n- **Notes are observations, not verdicts.** Point at what's interesting; let the reader judge.\n- **No celebration.** If everything looks reasonable, the report just looks reasonable. Don't add \"all green\" framing.\n\nThe inline chat reply is one or two sentences pointing at the file. Don't restate the report.\n\n## Common gotchas\n\n- Stale profile. `llamactl auth get` first, every session. A profile pointing at last week's env produces 401s that look like a code bug.\n- Active project drift. A test that \"works on my machine\" can fail elsewhere because the active project differs. Pin every row with `--project`.\n- Non-test project mutation. If you mutate without `--project`, the active profile decides where it lands. Always pin write commands.\n- Wrong environment for mutations. `llamactl environments get` shows which environment is active. Never run create/edit/delete against production — switch to a test environment first. The active environment persists across sessions, so always verify before running mutating commands.\n- Editing existing deployments. Don't edit deployments you didn't create — they may be someone's real work. Create throwaway deployments for QA and delete them when done.\n- TUI/prompt risk on write commands. `deployments delete/edit/rollback` and `create` will prompt when interactive. For QA, supply every required arg or pass `--no-interactive`. Read commands (`get`, `logs`, `history`, `auth get`, `environments get`) don't need the flag.\n- `tee` ate a row of a multi-line table once during a run. Use shell `>` redirect for QA captures, not `tee`, when you need every line preserved.\n- `serve` vs tilt. `llamactl serve` runs the appserver locally with no kubernetes. Tilt runs the full cloud stack in kind. Different scopes; pick one.\n- Don't leak IDs. Deployment and org IDs are fine in your terminal and in `thoughts/`, not fine in a public PR description. Strip or alias them in anything that might leave the repo.\n- Worktree venv mismatch. When QAing a worktree branch, the parent shell's `VIRTUAL_ENV` often points at the main repo's `.venv` (the worktree-specific one shadows it). `uv run` then warns and ignores the worktree env, silently using a stale binary. Either `unset VIRTUAL_ENV` once at the top of your session, or invoke the worktree's binary directly (`.venv/bin/llamactl`) for QA captures. The worktree binary is the one with the diff under test.\n\n## Self-update\n\nllamactl's command surface is moving. When a run teaches you a fact this skill doesn't have, edit it.\n\nTriggers for an edit:\n\n- A flag name, command path, or output shape was different from what the skill implied.\n- A failure mode showed up that isn't in \"Common gotchas\".\n- A backend setup step (especially local tilt) needed something the skill didn't say.\n\nWhat to add:\n\n- New gotchas go to \"Common gotchas\" as one-line entries.\n- New mental-model facts go to \"llamactl mental model\".\n- Tilt or backend changes go to \"Pick a backend\".\n\nEdit the file at `.agents/skills/llamactl-qa/SKILL.md` (the `.claude/skills/` entry is a symlink into here, maintained by `uv run dev sync-skills`). Keep the voice terse; don't pad. If you're unsure whether the fact is durable or just incidental to one run, leave it out. False additions are worse than missing ones.\n"
  },
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in our repository](https://github.com/changesets/changesets)\n\nWe have a quick list of common questions to get you started engaging with this project in\n[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.1.1/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [],\n  \"access\": \"restricted\",\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": ".git\n.venv\n__pycache__\nnode_modules\n.mypy_cache\n.pytest_cache\n.ruff_cache\n*.egg-info\ndocs\n.claude\n.worktree\nthoughts\narchitecture-docs\nexamples/dbos\nexamples/observability\nlib\nopenapi.json\n*.ipynb\n**/.mypy_cache\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/actions/pr-fix-comment/action.yml",
    "content": "# SPDX-License-Identifier: MIT\n\nname: \"Auto-Fix PR Manager\"\ndescription: \"Creates fix PRs and manages PR comments for auto-fix workflows\"\n\ninputs:\n  app-id:\n    description: \"GitHub App ID for authentication\"\n    required: false\n    default: \"\"\n  private-key:\n    description: \"GitHub App private key for authentication\"\n    required: false\n    default: \"\"\n  add-paths:\n    description: \"Paths to add to the fix PR (newline-separated)\"\n    required: true\n  check-name:\n    description: \"Human-readable name for the check (e.g., 'Frontend Lint', 'Backend Lint', 'Frontend SDK')\"\n    required: true\n  fix-command:\n    description: \"Command to run to fix issues (shown in comment, e.g., 'pnpm lint:fix')\"\n    required: true\n  fix-branch-name:\n    description: \"Branch name for the fix PR. Defaults to 'chore/fix-{check-name-slug}-{pr-number}'\"\n    required: false\n  commit-message:\n    description: \"Commit message for the fix PR. Defaults to 'chore: auto-fix {check-name} issues'\"\n    required: false\n  pr-title:\n    description: \"Title for the fix PR. Defaults to 'chore: fix {check-name} for #{pr-number}'\"\n    required: false\n  pr-body:\n    description: \"Body for the fix PR (markdown). Auto-generated if not provided.\"\n    required: false\n  comment-marker:\n    description: \"Unique marker ID to identify comments. Defaults to slugified check-name.\"\n    required: false\n  warning-title:\n    description: \"Title for the warning comment. Defaults to '{check-name} Fix Required'\"\n    required: false\n  warning-body:\n    description: \"Body text for the warning comment. Auto-generated if not provided.\"\n    required: false\n  success-title:\n    description: \"Title for the success comment. Defaults to '{check-name} Check Passed'\"\n    required: false\n  success-body:\n    description: \"Body text for the success comment. Auto-generated if not provided.\"\n    required: false\n\noutputs:\n  fix-pr-number:\n    description: \"PR number of the fix PR (if created)\"\n    value: ${{ steps.create-pr.outputs.pull-request-number }}\n  fix-pr-url:\n    description: \"URL of the fix PR (if created)\"\n    value: ${{ steps.create-pr.outputs.pull-request-url }}\n  comment-id:\n    description: \"ID of the comment that was created or updated\"\n    value: ${{ steps.find-comment.outputs.comment_id }}\n  action-taken:\n    description: \"What action was taken: 'created', 'updated-warning', 'updated-success', or 'none'\"\n    value: ${{ steps.manage-comment.outputs.action_taken }}\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Check if PR context\n      id: context\n      shell: bash\n      run: |\n        if [ -n \"${{ github.event.pull_request.number }}\" ]; then\n          echo \"is_pr=true\" >> $GITHUB_OUTPUT\n        else\n          echo \"is_pr=false\" >> $GITHUB_OUTPUT\n          echo \"Not a PR context - skipping fix PR creation\"\n        fi\n\n    - name: Check authentication inputs\n      id: auth-check\n      if: steps.context.outputs.is_pr == 'true'\n      shell: bash\n      env:\n        APP_ID: ${{ inputs.app-id }}\n        PRIVATE_KEY: ${{ inputs.private-key }}\n      run: |\n        if [ -n \"$APP_ID\" ] && [ -n \"$PRIVATE_KEY\" ]; then\n          echo \"has_auth=true\" >> $GITHUB_OUTPUT\n        else\n          echo \"has_auth=false\" >> $GITHUB_OUTPUT\n          echo \"Skipping auto-fix: authentication inputs not provided (likely a fork PR)\"\n        fi\n\n    - name: Generate GitHub App token\n      id: app-token\n      if: steps.context.outputs.is_pr == 'true' && steps.auth-check.outputs.has_auth == 'true'\n      uses: actions/create-github-app-token@v1\n      with:\n        app-id: ${{ inputs.app-id }}\n        private-key: ${{ inputs.private-key }}\n        owner: ${{ github.repository_owner }}\n\n    - name: Compute defaults\n      id: defaults\n      if: steps.context.outputs.is_pr == 'true' && steps.auth-check.outputs.has_auth == 'true'\n      shell: bash\n      env:\n        CHECK_NAME: ${{ inputs.check-name }}\n        FIX_COMMAND: ${{ inputs.fix-command }}\n        PR_NUMBER: ${{ github.event.pull_request.number }}\n        ADD_PATHS: ${{ inputs.add-paths }}\n        INPUT_FIX_BRANCH: ${{ inputs.fix-branch-name }}\n        INPUT_COMMIT_MSG: ${{ inputs.commit-message }}\n        INPUT_PR_TITLE: ${{ inputs.pr-title }}\n        INPUT_PR_BODY: ${{ inputs.pr-body }}\n        INPUT_COMMENT_MARKER: ${{ inputs.comment-marker }}\n        INPUT_WARNING_TITLE: ${{ inputs.warning-title }}\n        INPUT_WARNING_BODY: ${{ inputs.warning-body }}\n        INPUT_SUCCESS_TITLE: ${{ inputs.success-title }}\n        INPUT_SUCCESS_BODY: ${{ inputs.success-body }}\n      run: |\n        # Create a slug from the check name (e.g., \"Frontend Lint\" -> \"frontend-lint\")\n        CHECK_SLUG=$(echo \"$CHECK_NAME\" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')\n\n        # Set defaults\n        FIX_BRANCH=\"${INPUT_FIX_BRANCH:-chore/fix-${CHECK_SLUG}-${PR_NUMBER}}\"\n        COMMIT_MSG=\"${INPUT_COMMIT_MSG:-chore: auto-fix ${CHECK_NAME} issues}\"\n        PR_TITLE=\"${INPUT_PR_TITLE:-chore: fix ${CHECK_NAME} for #${PR_NUMBER}}\"\n        COMMENT_MARKER=\"${INPUT_COMMENT_MARKER:-${CHECK_SLUG}}\"\n        WARNING_TITLE=\"${INPUT_WARNING_TITLE:-${CHECK_NAME} Fix Required}\"\n        SUCCESS_TITLE=\"${INPUT_SUCCESS_TITLE:-${CHECK_NAME} Check Passed}\"\n        SUCCESS_BODY=\"${INPUT_SUCCESS_BODY:-All ${CHECK_NAME} checks passed.}\"\n        WARNING_BODY=\"${INPUT_WARNING_BODY:-Issues were detected that can be auto-fixed.\n\n        **To fix locally:** Run \\`${FIX_COMMAND}\\` before committing.}\"\n\n        # Generate PR body if not provided\n        if [ -z \"$INPUT_PR_BODY\" ]; then\n          PR_BODY=\"## Summary\n        Auto-fix ${CHECK_NAME} issues.\n\n        This PR was automatically created because issues were detected that can be auto-fixed.\n\n        ## Changed files\n        - \\`${ADD_PATHS}\\`\n\n        ## How to avoid this\n        Run \\`${FIX_COMMAND}\\` before committing.\n\n        ---\n        *Auto-generated for #${PR_NUMBER}*\"\n        else\n          PR_BODY=\"$INPUT_PR_BODY\"\n        fi\n\n        # Write outputs using heredocs for multiline content\n        {\n          echo \"fix-branch=${FIX_BRANCH}\"\n          echo \"commit-message=${COMMIT_MSG}\"\n          echo \"pr-title=${PR_TITLE}\"\n          echo \"comment-marker=${COMMENT_MARKER}\"\n          echo \"warning-title=${WARNING_TITLE}\"\n          echo \"success-title=${SUCCESS_TITLE}\"\n        } >> $GITHUB_OUTPUT\n\n        # Use heredocs for multiline outputs\n        {\n          echo \"pr-body<<EOFPRBODY\"\n          echo \"$PR_BODY\"\n          echo \"EOFPRBODY\"\n        } >> $GITHUB_OUTPUT\n\n        {\n          echo \"warning-body<<EOFWARN\"\n          echo \"$WARNING_BODY\"\n          echo \"EOFWARN\"\n        } >> $GITHUB_OUTPUT\n\n        {\n          echo \"success-body<<EOFSUCCESS\"\n          echo \"$SUCCESS_BODY\"\n          echo \"EOFSUCCESS\"\n        } >> $GITHUB_OUTPUT\n\n    - name: Create PR with changes\n      id: create-pr\n      if: steps.context.outputs.is_pr == 'true' && steps.auth-check.outputs.has_auth == 'true'\n      uses: peter-evans/create-pull-request@v6\n      with:\n        token: ${{ steps.app-token.outputs.token }}\n        commit-message: ${{ steps.defaults.outputs.commit-message }}\n        title: ${{ steps.defaults.outputs.pr-title }}\n        body: ${{ steps.defaults.outputs.pr-body }}\n        branch: ${{ steps.defaults.outputs.fix-branch }}\n        base: ${{ github.head_ref }}\n        add-paths: ${{ inputs.add-paths }}\n        labels: |\n          chore\n          automated pr\n        delete-branch: true\n\n    - name: Find existing bot comment\n      id: find-comment\n      if: steps.context.outputs.is_pr == 'true' && steps.auth-check.outputs.has_auth == 'true'\n      shell: bash\n      env:\n        GH_TOKEN: ${{ steps.app-token.outputs.token }}\n        PR_NUMBER: ${{ github.event.pull_request.number }}\n        COMMENT_MARKER: ${{ steps.defaults.outputs.comment-marker }}\n      run: |\n        # Use a unique HTML comment marker to identify our comments\n        # This is much more reliable than searching for text content\n        MARKER=\"<!-- pr-fix-comment:${COMMENT_MARKER} -->\"\n\n        # Find existing comment by searching for the exact marker\n        COMMENT_ID=$(gh api \"repos/${{ github.repository }}/issues/${PR_NUMBER}/comments\" \\\n          --jq \".[] | select(.body | startswith(\\\"${MARKER}\\\")) | .id\" | head -1)\n        echo \"comment_id=$COMMENT_ID\" >> $GITHUB_OUTPUT\n        echo \"marker=$MARKER\" >> $GITHUB_OUTPUT\n\n    - name: Close fix PR if no longer needed\n      if: steps.context.outputs.is_pr == 'true' && steps.auth-check.outputs.has_auth == 'true' && !steps.create-pr.outputs.pull-request-number\n      shell: bash\n      env:\n        GH_TOKEN: ${{ steps.app-token.outputs.token }}\n        FIX_PR_BRANCH: ${{ steps.defaults.outputs.fix-branch }}\n      run: |\n        FIX_PR=$(gh pr list --head \"${FIX_PR_BRANCH}\" --json number --jq '.[0].number')\n        if [ -n \"$FIX_PR\" ]; then\n          echo \"Closing fix PR #$FIX_PR as changes are no longer needed\"\n          gh pr close \"$FIX_PR\" --delete-branch --comment \"Closing this PR as the issue is now resolved in the original PR.\"\n        fi\n\n    - name: Manage PR comment\n      id: manage-comment\n      if: steps.context.outputs.is_pr == 'true' && steps.auth-check.outputs.has_auth == 'true'\n      shell: bash\n      env:\n        GH_TOKEN: ${{ steps.app-token.outputs.token }}\n        PR_NUMBER: ${{ github.event.pull_request.number }}\n        EXISTING_COMMENT_ID: ${{ steps.find-comment.outputs.comment_id }}\n        COMMENT_MARKER: ${{ steps.find-comment.outputs.marker }}\n        FIX_PR_CREATED: ${{ steps.create-pr.outputs.pull-request-number != '' }}\n        FIX_PR_URL: ${{ steps.create-pr.outputs.pull-request-url }}\n        WARNING_TITLE: ${{ steps.defaults.outputs.warning-title }}\n        WARNING_BODY: ${{ steps.defaults.outputs.warning-body }}\n        SUCCESS_TITLE: ${{ steps.defaults.outputs.success-title }}\n        SUCCESS_BODY: ${{ steps.defaults.outputs.success-body }}\n      run: |\n        if [ \"$FIX_PR_CREATED\" = \"true\" ]; then\n          # Fix PR was created - post or update warning comment\n          COMMENT_BODY=\"${COMMENT_MARKER}\n        ⚠️ **${WARNING_TITLE}**\n\n        ${WARNING_BODY}\n\n        **Action needed:** Merge this PR into your branch: ${FIX_PR_URL}\"\n\n          if [ -n \"$EXISTING_COMMENT_ID\" ]; then\n            echo \"Updating existing comment $EXISTING_COMMENT_ID with warning\"\n            gh api \"repos/${{ github.repository }}/issues/comments/${EXISTING_COMMENT_ID}\" \\\n              -X PATCH -f body=\"$COMMENT_BODY\"\n            echo \"action_taken=updated-warning\" >> $GITHUB_OUTPUT\n          else\n            echo \"Creating new warning comment\"\n            gh pr comment \"$PR_NUMBER\" --body \"$COMMENT_BODY\"\n            echo \"action_taken=created\" >> $GITHUB_OUTPUT\n          fi\n        else\n          # No fix PR needed - update existing comment to success (if exists)\n          if [ -n \"$EXISTING_COMMENT_ID\" ]; then\n            echo \"Updating comment to indicate issue is resolved\"\n            COMMENT_BODY=\"${COMMENT_MARKER}\n        ✅ **${SUCCESS_TITLE}**\n\n        ${SUCCESS_BODY}\"\n\n            gh api \"repos/${{ github.repository }}/issues/comments/${EXISTING_COMMENT_ID}\" \\\n              -X PATCH -f body=\"$COMMENT_BODY\"\n            echo \"action_taken=updated-success\" >> $GITHUB_OUTPUT\n          else\n            echo \"No action needed - no existing comment and check passed\"\n            echo \"action_taken=none\" >> $GITHUB_OUTPUT\n          fi\n        fi\n\n    - name: Fail if changes were needed\n      if: steps.context.outputs.is_pr == 'true' && steps.auth-check.outputs.has_auth == 'true' && steps.create-pr.outputs.pull-request-number\n      shell: bash\n      env:\n        FIX_PR_URL: ${{ steps.create-pr.outputs.pull-request-url }}\n        WARNING_TITLE: ${{ steps.defaults.outputs.warning-title }}\n      run: |\n        echo \"::error::${WARNING_TITLE}. Please merge the auto-generated PR: ${FIX_PR_URL}\"\n        exit 1\n"
  },
  {
    "path": ".github/actions/setup-publish/action.yml",
    "content": "name: Setup publish job\ndescription: Install Python + uv and download the publish plan artifact. Assumes the repo is already checked out.\nruns:\n  using: composite\n  steps:\n    - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6\n      with:\n        python-version: \"3.11\"\n    - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7\n    - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4\n      with:\n        name: publish-plan\n"
  },
  {
    "path": ".github/workflows/bump_llama_index_core.yml",
    "content": "name: Bump llama-index-core\n\non:\n  repository_dispatch:\n    types: [llama-index-core-release]\n  workflow_dispatch:\n\njobs:\n  bump-dependency:\n    if: github.repository == 'run-llama/llama-agents'\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: astral-sh/setup-uv@v6\n\n      - run: uv lock --upgrade-package llama-index-core\n\n      - name: Generate GitHub App token\n        id: app-token\n        uses: actions/create-github-app-token@v1\n        with:\n          app-id: ${{ secrets.CI_BOT_APP_ID }}\n          private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }}\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v6\n        with:\n          token: ${{ steps.app-token.outputs.token }}\n          commit-message: \"chore: bump llama-index-core\"\n          branch: bump-llama-index-core\n          delete-branch: true\n          title: \"chore: bump llama-index-core\"\n          labels: |\n            dependencies\n            automated\n"
  },
  {
    "path": ".github/workflows/chart-validate.yaml",
    "content": "name: chart-validate\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main, develop]\n    paths:\n      - \"charts/llama-agents/**\"\n      - \".github/workflows/chart-validate.yaml\"\n  workflow_dispatch: {}\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Helm\n        uses: azure/setup-helm@v4\n\n      - name: Install helm-unittest plugin\n        run: make -C operator helm-unittest-install\n\n      - name: Create kind cluster\n        uses: helm/kind-action@v1\n\n      - name: Install Prometheus Operator CRDs (for ServiceMonitor validation)\n        run: make -C operator helm-crds-prom-operator\n\n      - name: Install helm-docs\n        run: |\n          make -C operator helm-docs-install\n          echo \"$(go env GOPATH)/bin\" >> \"$GITHUB_PATH\"\n\n      - name: Verify README is up to date\n        run: make -C operator helm-docs-check\n\n      - name: Helm lint (default values)\n        run: make -C operator helm-lint\n\n      - name: Helm lint (dev values)\n        run: make -C operator helm-lint-dev\n\n      - name: Server-side dry-run apply (default values)\n        run: make -C operator helm-dry-run\n\n      - name: Server-side dry-run apply (dev values)\n        run: make -C operator helm-dry-run-dev\n\n      - name: Helm unit tests (if present)\n        run: |\n          if [ -d charts/llama-agents/tests ]; then\n            make -C operator helm-unittest\n          else\n            echo \"No helm-unittest tests found; skipping.\"\n          fi\n"
  },
  {
    "path": ".github/workflows/go-ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main, develop]\n    paths:\n      - \"operator/**\"\n      - \"docker/operator.Dockerfile\"\n      - \"scripts/process_manifests.py\"\n\njobs:\n  operator-lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.24\"\n          cache-dependency-path: ./operator/go.sum\n\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v8\n        with:\n          version: v2.1\n          working-directory: ./operator\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n\n      - name: Verify generated manifests are up to date\n        run: make -C operator operator-manifests-check\n\n      - name: Clean controller-gen cache\n        run: rm -rf ./operator/bin/controller-gen\n\n      - name: Run Go tests\n        run: make -C operator operator-test\n        env:\n          GOOS: linux\n          GOARCH: amd64\n\n  operator-build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build operator image (no push)\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: docker/operator.Dockerfile\n          platforms: linux/amd64\n          push: false\n          tags: llamaindex/llama-agents-operator:dev\n          cache-from: type=gha,scope=operator-amd64\n          cache-to: type=gha,mode=min,scope=operator-amd64\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Linting\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n\n      - name: Install dev dependencies\n        run: uv sync --all-extras --all-packages\n\n      - name: Run pre-commit\n        id: pre-commit\n        continue-on-error: true\n        run: uv run pre-commit run -a\n\n      - name: Auto-fix PR if needed\n        uses: ./.github/actions/pr-fix-comment\n        with:\n          app-id: ${{ secrets.CI_BOT_APP_ID }}\n          private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }}\n          add-paths: \".\"\n          check-name: \"Lint\"\n          fix-command: \"uv run pre-commit run -a\"\n\n      - name: Fail if pre-commit failed\n        if: steps.pre-commit.outcome == 'failure'\n        run: exit 1\n"
  },
  {
    "path": ".github/workflows/lockfile.yml",
    "content": "name: Lockfile\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"**/package.json\"\n      - \"pnpm-lock.yaml\"\n  pull_request:\n    paths:\n      - \"**/package.json\"\n      - \"pnpm-lock.yaml\"\n\njobs:\n  check:\n    name: Validate lockfile\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v5\n        with:\n          node-version: \"22\"\n\n      - name: Check lockfile is up to date\n        run: pnpm install --frozen-lockfile\n"
  },
  {
    "path": ".github/workflows/publish_changesets.yml",
    "content": "name: Version Bump and Release\n\non:\n  push:\n    branches:\n      - main\n      - dev\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\npermissions:\n  contents: write\n  id-token: write\n  packages: write\n\njobs:\n  # ---------------------------------------------------------------------------\n  # Release PR management + publish planning.\n  #\n  # 1. Runs changesets/action to open/update the \"version packages\" release PR\n  #    when there are pending changesets. When that PR merges into main/dev,\n  #    the changesets folder is empty on the next push and this step is a\n  #    no-op — then we proceed to plan & publish.\n  # 2. Runs `dev changeset-plan` unconditionally. The plan is a reconcile\n  #    loop: it diffs current workspace versions against what's already on\n  #    PyPI / Docker Hub / the Helm registry and emits actions for the gaps.\n  #    An empty plan is normal and results in all downstream jobs no-opping.\n  # ---------------------------------------------------------------------------\n  plan:\n    name: Plan release\n    runs-on: ubuntu-latest\n    environment: release\n    if: github.repository == 'run-llama/llama-agents' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')\n    outputs:\n      has_work: ${{ steps.plan.outputs.has_work }}\n      pypi: ${{ steps.plan.outputs.pypi }}\n      docker_builds: ${{ steps.plan.outputs.docker_builds }}\n      docker_manifests: ${{ steps.plan.outputs.docker_manifests }}\n      helm: ${{ steps.plan.outputs.helm }}\n    steps:\n      - name: Checkout Repo\n        uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5\n\n      - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5\n        with:\n          node-version: \"22\"\n          cache: \"pnpm\"\n\n      - name: Setup Python\n        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6\n        with:\n          python-version: \"3.11\"\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Generate GitHub App token\n        id: app-token\n        uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1\n        with:\n          app-id: ${{ secrets.CI_BOT_APP_ID }}\n          private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }}\n\n      - name: Install helm-docs\n        run: |\n          make -C operator helm-docs-install\n          echo \"$(go env GOPATH)/bin\" >> \"$GITHUB_PATH\"\n\n      - name: Create / update Release Pull Request\n        id: changesets\n        uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1\n        with:\n          commit: \"chore: version packages\"\n          title: \"chore: version packages\"\n          # Only manage the release PR here. Actual publishing is done by\n          # the fan-out jobs below so docker builds run in parallel on\n          # native amd64 / arm64 runners.\n          version: pnpm -w run version\n          setupGitUser: false\n          commitMode: github-api\n        env:\n          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}\n          GH_TOKEN: ${{ steps.app-token.outputs.token }}\n\n      # When there are pending changesets, the step above runs ``pnpm -w\n      # run version`` to build the release PR. With ``commitMode:\n      # github-api`` those edits are committed through the API and left\n      # uncommitted in the local working tree. If we don't reset, the\n      # plan step below scans the mutated tree, emits actions for the\n      # *next* versions, and the downstream publish jobs (which do a\n      # fresh checkout of the branch) then build artifacts for the\n      # previous versions and fail to match the plan. Reset so the plan\n      # reflects what is actually committed on the branch.\n      - name: Reset working tree after changesets action\n        run: git reset --hard HEAD\n\n      - name: Compute publish plan\n        id: plan\n        run: uv run dev changeset-plan --output publish-plan.json\n\n      - name: Upload publish plan\n        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4\n        with:\n          name: publish-plan\n          path: publish-plan.json\n          retention-days: 14\n\n  # ---------------------------------------------------------------------------\n  # PyPI: one job per package so the Actions UI shows a checkmark per release.\n  # ---------------------------------------------------------------------------\n  pypi:\n    name: \"pypi: ${{ matrix.item.package }}\"\n    needs: plan\n    if: needs.plan.outputs.pypi != '[]'\n    runs-on: ubuntu-latest\n    environment: release\n    strategy:\n      fail-fast: false\n      matrix:\n        item: ${{ fromJSON(needs.plan.outputs.pypi) }}\n    steps:\n      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5\n      - uses: ./.github/actions/setup-publish\n      - run: uv run dev publish-action --plan publish-plan.json --id '${{ matrix.item.id }}'\n\n  # ---------------------------------------------------------------------------\n  # Docker builds: one job per (image, platform) so amd64 / arm64 build on\n  # native runners in parallel — no QEMU emulation. The manifest job below\n  # stitches the per-arch tags into the final multi-arch tags.\n  # ---------------------------------------------------------------------------\n  docker-build:\n    name: \"docker: ${{ matrix.item.package }} (${{ matrix.item.platform }})\"\n    needs: plan\n    if: needs.plan.outputs.docker_builds != '[]'\n    runs-on: ${{ matrix.item.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}\n    environment: release\n    strategy:\n      fail-fast: false\n      matrix:\n        item: ${{ fromJSON(needs.plan.outputs.docker_builds) }}\n    steps:\n      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5\n      - uses: ./.github/actions/setup-publish\n      - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3\n      # Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_CACHE_URL for buildx type=gha cache.\n      - uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3\n      - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3\n        with:\n          registry: docker.io\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - run: uv run dev publish-action --plan publish-plan.json --id '${{ matrix.item.id }}'\n\n  docker-manifest:\n    name: \"docker-manifest: ${{ matrix.item.package }}\"\n    needs: [plan, docker-build]\n    if: needs.plan.outputs.docker_manifests != '[]'\n    runs-on: ubuntu-latest\n    environment: release\n    strategy:\n      fail-fast: false\n      matrix:\n        item: ${{ fromJSON(needs.plan.outputs.docker_manifests) }}\n    steps:\n      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5\n      - uses: ./.github/actions/setup-publish\n      - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3\n      - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3\n        with:\n          registry: docker.io\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - run: uv run dev publish-action --plan publish-plan.json --id '${{ matrix.item.id }}'\n\n  # ---------------------------------------------------------------------------\n  # Helm: one job per chart. Depends on the manifest job because charts\n  # reference the final image tags and must not be published before them.\n  # ---------------------------------------------------------------------------\n  helm:\n    name: \"helm: ${{ matrix.item.package }}\"\n    needs: [plan, docker-manifest]\n    if: |\n      always() &&\n      needs.plan.result == 'success' &&\n      (needs.docker-manifest.result == 'success' || needs.docker-manifest.result == 'skipped') &&\n      needs.plan.outputs.helm != '[]'\n    runs-on: ubuntu-latest\n    environment: release\n    strategy:\n      fail-fast: false\n      matrix:\n        item: ${{ fromJSON(needs.plan.outputs.helm) }}\n    steps:\n      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5\n      - uses: ./.github/actions/setup-publish\n      - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4\n        with:\n          version: '3.18.1'\n      - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3\n        with:\n          registry: docker.io\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - run: uv run dev publish-action --plan publish-plan.json --id '${{ matrix.item.id }}'\n\n  # ---------------------------------------------------------------------------\n  # Finalize: create git tags for everything we just published and fire\n  # downstream repository dispatches. Runs only if all publish jobs\n  # succeeded (or were legitimately skipped because there was no work).\n  # ---------------------------------------------------------------------------\n  finalize:\n    name: Finalize release\n    needs: [plan, pypi, docker-build, docker-manifest, helm]\n    if: |\n      always() &&\n      needs.plan.result == 'success' &&\n      needs.plan.outputs.has_work == 'true' &&\n      !contains(needs.*.result, 'failure') &&\n      !contains(needs.*.result, 'cancelled')\n    runs-on: ubuntu-latest\n    environment: release\n    steps:\n      - name: Generate GitHub App token\n        id: app-token\n        uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1\n        with:\n          app-id: ${{ secrets.CI_BOT_APP_ID }}\n          private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }}\n      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5\n        with:\n          fetch-depth: 0\n          token: ${{ steps.app-token.outputs.token }}\n      - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5\n        with:\n          node-version: \"22\"\n          cache: \"pnpm\"\n      # Install the workspace so ``pnpm exec changeset tag`` uses the\n      # @changesets/cli version pinned by pnpm-lock.yaml, with every\n      # transitive dep locked — ``npx``/``pnpm dlx`` would ignore the\n      # lockfile and resolve fresh, which we don't want in the release\n      # path.\n      - run: pnpm install --frozen-lockfile\n      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4\n        with:\n          name: publish-plan\n      # Use changesets/action in publish mode to (1) tag each package\n      # at its current version, (2) push the tags, and (3) create a\n      # GitHub Release per tag with the CHANGELOG entry as the body.\n      # The ``publish`` script is a no-op beyond ``changeset tag`` —\n      # the fan-out jobs above handle the actual PyPI / Docker / Helm\n      # publishing. The action only parses ``New tag: ...`` lines from\n      # the publish script output to decide which packages to release.\n      - name: Create tags and GitHub releases\n        uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1\n        with:\n          publish: pnpm exec changeset tag\n          setupGitUser: false\n          commitMode: github-api\n          createGithubReleases: true\n        env:\n          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}\n          GH_TOKEN: ${{ steps.app-token.outputs.token }}\n\n      - name: Extract published llama-agents-server version\n        if: github.ref == 'refs/heads/main'\n        id: server-version\n        run: |\n          VERSION=$(jq -r '.pypi[] | select(.package == \"llama-agents-server\") | .version' publish-plan.json)\n          if [ -n \"$VERSION\" ] && [ \"$VERSION\" != \"null\" ]; then\n            echo \"version=$VERSION\" >> \"$GITHUB_OUTPUT\"\n            echo \"tag=llama-agents-server@v${VERSION}\" >> \"$GITHUB_OUTPUT\"\n            echo \"Found llama-agents-server version: $VERSION\"\n          else\n            echo \"llama-agents-server was not published in this release\"\n          fi\n\n      - name: Trigger OpenAPI publish for llama-agents-server\n        if: github.ref == 'refs/heads/main' && steps.server-version.outputs.version != ''\n        uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3\n        with:\n          token: ${{ steps.app-token.outputs.token }}\n          event-type: publish-openapi\n          client-payload: '{\"tag\": \"${{ steps.server-version.outputs.tag }}\"}'\n\n      # The default app-token above is scoped to this repo only, so it\n      # can't dispatch to the downstream repo. Mint a second token\n      # scoped to DOWNSTREAM_DISPATCH_REPO for the dispatch call.\n      - name: Parse downstream dispatch repo\n        id: dispatch-repo\n        if: github.ref == 'refs/heads/main' && vars.DOWNSTREAM_DISPATCH_REPO != ''\n        env:\n          DISPATCH_REPO: ${{ vars.DOWNSTREAM_DISPATCH_REPO }}\n        run: |\n          echo \"owner=${DISPATCH_REPO%%/*}\" >> \"$GITHUB_OUTPUT\"\n          echo \"repo=${DISPATCH_REPO##*/}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Generate downstream dispatch token\n        id: dispatch-token\n        if: github.ref == 'refs/heads/main' && vars.DOWNSTREAM_DISPATCH_REPO != ''\n        uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1\n        with:\n          app-id: ${{ secrets.CI_BOT_APP_ID }}\n          private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }}\n          owner: ${{ steps.dispatch-repo.outputs.owner }}\n          repositories: ${{ steps.dispatch-repo.outputs.repo }}\n\n      - name: Trigger downstream version bump\n        if: github.ref == 'refs/heads/main' && vars.DOWNSTREAM_DISPATCH_REPO != ''\n        env:\n          GH_TOKEN: ${{ steps.dispatch-token.outputs.token }}\n          DISPATCH_REPO: ${{ vars.DOWNSTREAM_DISPATCH_REPO }}\n        run: |\n          PACKAGES=$(jq -c '[.pypi[] | {name: .package, version: .version}] + [.docker_manifests[] | {name: .image, version: .version}] + [.helm[] | {name: .package, version: .version}]' publish-plan.json)\n          gh api \"repos/$DISPATCH_REPO/dispatches\" \\\n            --input - <<< \"{\\\"event_type\\\":\\\"bump-cloud-llama-deploy\\\",\\\"client_payload\\\":{\\\"packages\\\":$PACKAGES}}\"\n"
  },
  {
    "path": ".github/workflows/publish_openapi.yml",
    "content": "name: Publish OpenAPI specification\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Release tag to update (e.g. llama-agents-server@v0.1.1)\"\n        required: true\n        type: string\n  repository_dispatch:\n    types: [publish-openapi]\n\njobs:\n  publish-openapi:\n    if: github.repository == 'run-llama/llama-agents'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n\n      - name: Resolve target tag\n        id: target\n        run: |\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            TAG=\"${{ github.event.inputs.tag }}\"\n          else\n            TAG=\"${{ github.event.client_payload.tag }}\"\n          fi\n          echo \"tag=${TAG}\" >> \"$GITHUB_OUTPUT\"\n          echo \"TARGET_TAG=${TAG}\" >> \"$GITHUB_ENV\"\n\n      - name: Compute release metadata\n        id: metadata\n        run: >\n          uv run dev compute-tag-metadata\n          --tag \"${{ env.TARGET_TAG }}\"\n          --output \"$GITHUB_OUTPUT\"\n\n      - name: Sync dependencies and build OpenAPI spec\n        working-directory: packages/llama-agents-server\n        run: |\n          uv sync\n          uv run hatch run openapi\n\n      - name: Upload OpenAPI to release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ env.TARGET_TAG }}\n          files: packages/llama-agents-server/openapi.json\n          fail_on_unmatched_files: true\n          append_body: false\n\n      - name: Generate GitHub App token\n        id: app-token\n        uses: actions/create-github-app-token@v1\n        with:\n          app-id: ${{ secrets.CI_BOT_APP_ID }}\n          private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }}\n          owner: run-llama\n\n      - name: Trigger SDK update\n        if: >\n          steps.metadata.outputs.change_type != '' &&\n          steps.metadata.outputs.change_type != 'none'\n        uses: peter-evans/repository-dispatch@v3\n        with:\n          token: ${{ steps.app-token.outputs.token }}\n          repository: run-llama/llama-ui\n          event-type: workflows-sdk-update\n          client-payload: >-\n            {\"version\": \"${{ steps.metadata.outputs.semver }}\",\n             \"openapi_url\": \"https://github.com/run-llama/llama-agents/releases/download/${{ env.TARGET_TAG }}/openapi.json\",\n             \"change_type\": \"${{ steps.metadata.outputs.change_type }}\",\n             \"change_description\": \"${{ steps.metadata.outputs.change_description }}\"}\n"
  },
  {
    "path": ".github/workflows/sync-docs.yml",
    "content": "name: Sync Docs to Developer Hub\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \"docs/**\"\n  workflow_dispatch:\n\njobs:\n  sync-docs:\n    if: github.repository == 'run-llama/llama-agents'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout source repo\n        uses: actions/checkout@v4\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v4\n\n      - name: Checkout docs repo\n        uses: actions/checkout@v4\n        with:\n          repository: run-llama/developers\n          token: ${{ secrets.DEVELOPER_HUB_TOKEN }}\n          path: developer-hub\n\n      - name: Sync docs\n        run: ./scripts/sync-docs-to-developer-hub.sh developer-hub\n\n      - name: Commit and push\n        working-directory: developer-hub\n        run: |\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n\n          git add src/content/docs/python/llamaagents/ api-reference/python/workflows/\n\n          if git diff --staged --quiet; then\n            echo \"No docs changes to sync.\"\n            exit 0\n          fi\n\n          SOURCE_SHA=\"${GITHUB_SHA::8}\"\n          git commit -m \"sync: llama-agents docs from run-llama/llama-agents@${SOURCE_SHA}\"\n          git push\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Unit Testing\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n\nenv:\n  # Which Python from the matrix will be used to run the coverage check\n  COV_PYTHON_VERSION: 3.14\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n        package:\n          [\n            llama-index-workflows,\n            llama-index-utils-workflow,\n            llama-agents-integration-tests,\n            llama-agents-dev,\n            llama-agents-client,\n            llama-agents-server,\n            llama-agents-dbos,\n            llama-agents-core,\n            llama-agents-agentcore,\n            llama-agents-appserver,\n            llama-agents-control-plane,\n            llamactl,\n          ]\n        exclude:\n          # Dev CLI only needs to run on 3.14\n          - package: llama-agents-dev\n            python-version: \"3.10\"\n          - package: llama-agents-dev\n            python-version: \"3.11\"\n          - package: llama-agents-dev\n            python-version: \"3.12\"\n          - package: llama-agents-dev\n            python-version: \"3.13\"\n        include:\n          - package: llama-index-workflows\n            package_dir: packages/llama-index-workflows\n          - package: llama-index-utils-workflow\n            package_dir: packages/llama-index-utils-workflow\n          - package: llama-agents-integration-tests\n            package_dir: packages/llama-agents-integration-tests\n          - package: llama-agents-dev\n            package_dir: .\n          - package: llama-agents-client\n            package_dir: packages/llama-agents-client\n          - package: llama-agents-server\n            package_dir: packages/llama-agents-server\n          - package: llama-agents-dbos\n            package_dir: packages/llama-agents-dbos\n          - package: llama-agents-core\n            package_dir: packages/llama-agents-core\n          - package: llama-agents-agentcore\n            package_dir: packages/llama-agents-agentcore\n          - package: llama-agents-appserver\n            package_dir: packages/llama-agents-appserver\n          - package: llama-agents-control-plane\n            package_dir: packages/llama-agents-control-plane\n          - package: llamactl\n            package_dir: packages/llamactl\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install uv and set the python version\n        uses: astral-sh/setup-uv@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n          enable-cache: true\n\n      - name: Run tests\n        if: matrix.python-version != env.COV_PYTHON_VERSION\n        run: |\n          uv sync --python ${{ matrix.python-version }} --all-extras --directory ${{ matrix.package_dir }}\n          uv run --python ${{ matrix.python-version }} --all-extras --directory ${{ matrix.package_dir }} -- pytest\n\n      - name: Run tests with coverage\n        if: matrix.python-version == env.COV_PYTHON_VERSION\n        run: uv run --all-extras --directory ${{ matrix.package_dir }} -- pytest --cov=src --cov-report=xml\n\n      - name: Report Coveralls\n        if: matrix.python-version == env.COV_PYTHON_VERSION\n        continue-on-error: true\n        uses: coverallsapp/github-action@v2\n        with:\n          file: ${{ matrix.package_dir }}/coverage.xml\n          flag-name: ${{ matrix.package }}\n          parallel: true\n        env:\n          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}\n\n  test-docker:\n    name: \"test docker (3.14 - all packages)\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install uv and set the python version\n        uses: astral-sh/setup-uv@v6\n        with:\n          python-version: \"3.14\"\n          enable-cache: true\n\n      - name: Run Docker tests with coverage (append)\n        # Use longer timeout for Docker tests since container startup can take 30-60s in CI\n        run: uv run --all-extras --all-packages dev --timeout=120 -m docker --cov --cov-append --cov-report=xml\n\n      - name: Report Coveralls\n        continue-on-error: true\n        uses: coverallsapp/github-action@v2\n        with:\n          file: packages/llama-agents-integration-tests/coverage.xml\n          flag-name: llama-agents-integration-tests\n          parallel: true\n        env:\n          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}\n\n  coveralls-finish:\n    needs: [test, test-docker]\n    runs-on: ubuntu-latest\n    if: github.repository == 'run-llama/llama-agents'\n    steps:\n      - name: Finalize Coveralls parallel jobs\n        continue-on-error: true\n        uses: coverallsapp/github-action@v2\n        with:\n          parallel-finished: true\n        env:\n          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/update_debugger_assets.yml",
    "content": "name: Update Debugger Assets\n\non:\n  repository_dispatch:\n    types: [debugger-assets-update]\n  workflow_dispatch:\n    inputs:\n      js_url:\n        description: \"JavaScript URL\"\n        required: true\n      css_url:\n        description: \"CSS URL\"\n        required: true\n\njobs:\n  update-assets:\n    if: github.repository == 'run-llama/llama-agents'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v6\n\n      - name: Extract payload data\n        id: payload\n        run: |\n          # Handle both repository_dispatch and workflow_dispatch\n          if [ -n \"${{ github.event.inputs.js_url }}\" ]; then\n            echo \"js_url=${{ github.event.inputs.js_url }}\" >> $GITHUB_OUTPUT\n            echo \"css_url=${{ github.event.inputs.css_url }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"js_url=${{ github.event.client_payload.js_url }}\" >> $GITHUB_OUTPUT\n            echo \"css_url=${{ github.event.client_payload.css_url }}\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Update index.html\n        run: >\n          uv run dev update-index-html\n          --js-url \"${{ steps.payload.outputs.js_url }}\"\n          --css-url \"${{ steps.payload.outputs.css_url }}\"\n\n      - name: Create changeset for patch version\n        run: |\n          # Use a deterministic filename so multiple updates accumulate into one changelog entry\n          CHANGESET_FILE=\".changeset/update-debugger-assets.md\"\n          cat > \"$CHANGESET_FILE\" << 'EOF'\n          ---\n          \"llama-agents-server\": patch\n          ---\n\n          Update debugger assets\n\n          - JavaScript: ${{ steps.payload.outputs.js_url }}\n          - CSS: ${{ steps.payload.outputs.css_url }}\n          EOF\n          echo \"Created changeset: $CHANGESET_FILE\"\n      - name: Generate GitHub App token\n        id: app-token\n        uses: actions/create-github-app-token@v1\n        with:\n          app-id: ${{ secrets.CI_BOT_APP_ID }}\n          private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }}\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v6\n        with:\n          token: ${{ steps.app-token.outputs.token }}\n          commit-message: \"chore: update debugger assets\"\n          branch: update-debugger-assets\n          delete-branch: true\n          title: \"Update debugger assets\"\n          body: |\n            ## Update Debugger Assets\n\n            This PR updates the workflow debugger assets and creates a changeset for a patch version bump.\n\n            ### Changes\n            - Updated `packages/llama-agents-server/src/llama_agents/server/static/index.html`\n              - JavaScript: ${{ steps.payload.outputs.js_url }}\n              - CSS: ${{ steps.payload.outputs.css_url }}\n\n            ---\n            *This PR was automatically created by the [update-debugger-assets workflow](https://github.com/${{ github.repository }}/actions/workflows/update_debugger_assets.yml)*\n          labels: |\n            dependencies\n            automated\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n.python-version\n*.sqlite\n*.sqlite3\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control\n.pdm.toml\n.pdm-python\n.pdm-build/\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# Ruff stuff:\n.ruff_cache/\n\n# PyPI configuration file\n.pypirc\n\n# IDEs\n.idea/\n.vscode/\n.zed/\n.claude/\n.cursorignore\n.cursorindexingignore\n\n# Generated files\nopenapi.json\nworkflow_all_flows.mermaid\nnode_modules/\n.cecli*\n\n# Cloned reference repositories\nllama_index/\n\n# Docs preview\n.docs-preview/\n\n# Operator / k8s dev\noperator/bin/\nbin/\ntilt/k8s-manifests/secrets.yaml\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "---\ndefault_language_version:\n  python: python3\n\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: check-byte-order-marker\n      - id: check-merge-conflict\n      - id: check-toml\n      - id: check-yaml\n        args: [--allow-multiple-documents]\n        exclude: \"(^charts|^operator)\"\n      - id: detect-private-key\n      - id: end-of-file-fixer\n      - id: mixed-line-ending\n      - id: trailing-whitespace\n\n  - repo: https://github.com/charliermarsh/ruff-pre-commit\n    rev: v0.14.5\n    hooks:\n      - id: ruff-format\n      - id: ruff-check\n        args: [--fix, --exit-non-zero-on-fix]\n\n  - repo: local\n    hooks:\n      - id: ty\n        name: ty\n        language: system\n        entry: uv run ty check packages/\n        pass_filenames: false\n  - repo: https://github.com/DetachHead/basedpyright-pre-commit-mirror\n    rev: 1.31.1\n    hooks:\n      - id: basedpyright\n        exclude: \"scripts/\"\n\n  - repo: https://github.com/codespell-project/codespell\n    rev: v2.3.0\n    hooks:\n      - id: codespell\n        additional_dependencies: [tomli]\n        exclude: ^examples/|^docs/|pnpm-lock.yaml\n\n  - repo: https://github.com/pappasam/toml-sort\n    rev: v0.23.1\n    hooks:\n      - id: toml-sort-fix\n        exclude: ^uv\\.lock$\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"overrides\": [\n    {\n      \"files\": [\"*.yaml\", \"*.yml\"],\n      \"options\": {\n        \"bracketSpacing\": false\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".taplo.toml",
    "content": "[formatting]\nalign_comments = false\nreorder_keys = false\n# Following are to be consistent with toml-sort\nindent_string = \"  \"\narray_trailing_comma = false\ncompact_arrays = true\ncompact_inline_tables = true\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# LlamaIndex Workflows - Claude Development Guide\n\n## Project Overview\nThis is the LlamaIndex Workflows library - an event-driven, async-first framework for orchestrating complex AI applications and multi-step processes.\n\n## Key Technologies\n- Python 3.9+\n- AsyncIO (async/await)\n- Pydantic for data models\n- Starlette for web server\n- Uvicorn for ASGI serving\n\n## Setup\n- Install uv: `curl -LsSf https://astral.sh/uv/install.sh | sh`\n- Install deps (dev): `uv sync --all-packages --all-extras`\n\n## Development Commands\n\n### Testing\n\nUse the `dev` CLI to run tests:\n\n```bash\n# Run all package tests\nuv run dev\n\n# Filter by substring match\nuv run dev -p workflows\nuv run dev -p server -p client\n\n# Pass pytest args after --\nuv run dev -- -k test_name\n```\n\nFor more advanced scenarios, you can always `cd packages/some-package` and use pytest directly. The dev tool just provides additional package level test parallelism, and more curated cross package test output to avoid context bloat.\n\nSeveral packages have Docker integration tests (requires Docker running) marked with `@pytest.mark.docker`. Run them with:\n\n```bash\ncd packages/<package> && uv run pytest -m docker -s -n0\n```\n\n\n### Linting & Formatting\n```bash\nuv run pre-commit run -a\n```\n\nIf type checkers (ty, basedpyright) fail with unresolved imports, you likely need all packages installed: `uv sync --all-packages --all-extras`\n\n## Project Structure\n- `packages/llama-index-workflows/src/workflows/` - Main library code\n- `packages/llama-index-workflows/src/workflows/server/` - Web server implementation\n- `packages/llama-index-workflows/tests/` - Test suite\n- `packages/llama-agents-core/` - Shared schemas and utilities for cloud components\n- `packages/llama-agents-control-plane/` - Control plane service (K8s management, deployment API)\n- `packages/llama-agents-appserver/` - Application server (runs workflows in pods)\n- `packages/llamactl/` - CLI tool for interacting with deployments\n- `packages/llama-agents-agentcore/` - Agent runtime and deployment logic\n- `operator/` - Go-based Kubernetes operator (see `operator/AGENTS.md`)\n- `charts/` - Helm charts (see `charts/AGENTS.md`)\n- `docker/` - Dockerfiles for container images\n- `operator/tilt/` - Tilt dev environment support files\n- `examples/` - Usage examples\n\n## Architecture\n\nSee `architecture-docs/` for high-level architectural overviews:\n- [`core-overview.md`](architecture-docs/core-overview.md) — Workflow, Context, Runtime, and event flow\n- [`control-loop.md`](architecture-docs/control-loop.md) — The reducer-based execution engine\n- [`server-architecture.md`](architecture-docs/server-architecture.md) — HTTP server, persistence, and runtime decorators\n- [`overall-architecture.md`](architecture-docs/overall-architecture.md) — Cloud platform architecture (operator, control plane, appserver)\n- [`build-api.md`](architecture-docs/build-api.md) — Build API for deployment creation\n- [`quick-reference.md`](architecture-docs/quick-reference.md) — Code navigation guide and key constants\n\nThe DBOS package has its own architecture doc explaining the distributed model (process boundaries, adapter rules, idle release):\n- [`packages/llama-agents-dbos/ARCHITECTURE.md`](packages/llama-agents-dbos/ARCHITECTURE.md)\n\n## Key Components\n- **Workflow** - Main orchestration class\n- **Context** - State management across workflow steps\n- **Events** - Event-driven communication between steps\n- **WorkflowServer** - HTTP server for serving workflows as web services\n\n## Versioning with Changesets\n\n```bash\nnpx changeset              # Add a changeset\nnpx changeset status       # Check pending changes\n```\n\nChangeset descriptions should be a single line in plain English. Keep it short and simple. Do not use conventional-commit prefixes (no `fix(scope):`, `feat(scope):`, etc.).\n\n## Development Environment\n\n```bash\nuv run operator/dev.py up             # Set up kind cluster and start development\nuv run operator/dev.py down           # Clean up deployed resources\nuv run operator/dev.py down --delete  # Delete the kind cluster\nuv run operator/dev.py status         # Show cluster status\n```\n\n## Other Components\n\nFor Operator/Helm details, see `operator/AGENTS.md` and `charts/AGENTS.md`.\n\n## Notes for Claude\n- Always run tests after making changes: `uv run dev`\n- Never use classes for tests, only use pytest functions\n- Always annotate with types function arguments and return values\n- The project uses async/await extensively\n- Context serialization requires specific JSON format for globals\n\n## Autonomous Operation\n\nThe following rules apply if you are running in an isolated sandbox environment and have tools to commit and push changes to git\n\nMake sure to install uv as the package manager. Development commands rely on it.\n\n```bash\ncurl -fsSL https://astral.sh/uv/install.sh | sh\n```\n\nAlways run tests and pre-commit before committing:\n\n```bash\nuv run dev\nuv run pre-commit run -a\n```\n\n## Testing Patterns\n\nWe use **pytest** with idiomatic pytest patterns. Follow these guidelines:\n\n- **No Test Classes**: Do not use test classes to organize tests. Write tests as standalone functions. Achieve organization through descriptive function names (e.g., `test_create_job_with_invalid_input_raises_error`) or by splitting into separate test files.\n- **Pytest Fixtures**: Use fixtures for setup/teardown and shared test dependencies. Prefer fixtures over manual setup code repeated across tests.\n- **Prefer Real Objects Over Mocks**: Use simple dataclasses and real objects directly when available rather than mocking them. Only mock external dependencies or things that are truly difficult to instantiate.\n- **DRY Test Setup**: Do not repeat patches or setup code. Create reusable abstractions—fixtures, helper functions, or module-level constants—that can be shared across tests. Tests can easily be overwhelmed with setup; start from a rich suite of testing utilities to enable many small, expressive tests.\n- **Simple Testing Utilities**: Testing utilities should be basic—just functions, fixtures, and global variables. Avoid over-engineering test infrastructure.\n\n## Coding Style\n\n- Always use `from __future__ import annotations` at the top of each test file. Never use string annotations.\n- Include the standard SPDX license header at the top of each file:\n  ```python\n  # SPDX-License-Identifier: MIT\n  # Copyright (c) 2026 LlamaIndex Inc.\n  ```\n- Comments are useful, but avoid fluff.\n- Import etiquette\n  - **Do not use inline imports.** This is the default rule. Do not move imports into functions just to make an edit work quickly, avoid a top-level import conflict, or silence a linter/type checker.\n  - Inline imports are allowed only for two accepted conditions: circular import chokepoints and known startup-time deferrals.\n  - Circular import deferrals must have a well-defined chokepoint that owns the inline import burden. Prefer the high-level orchestration module that closes the loop, such as `workflow.py`; keep low-level leaf modules on normal top-level imports.\n  - Startup-time deferrals are only acceptable for known, measured import costs on latency-sensitive surfaces, such as fast CLI startup. Do not invent new startup deferrals casually.\n  - If a change appears to need a new inline import, first look for a normal top-level import, a better module boundary, or an existing chokepoint. Treat adding an inline import as a design exception, not a convenience.\n  - When an inline import is truly warranted, it must carry a short comment explaining the deferral reason: which cycle it breaks, or what startup cost it avoids. No naked inline imports.\n  - Put inline imports at the very beginning of the function that uses them, before other executable logic.\n  - `if TYPE_CHECKING` imports may only be used alongside one of the accepted inline-import patterns above, so the runtime import stays deferred while annotations remain typed.\n  - Do not wrap deferred-only types in string annotations. Use `from __future__ import annotations` instead.\n- Only add `__init__.py` `__all__` exports when a file is legitimately needed for public library consumption. Module level imports should not be used internally. For the most part you should never do this unless explicitly requested to do so\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "@AGENTS.md\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Ideas for Contributing\n\nContributions are welcome! We do our best to keep an eye on the issues and PRs and keeping them moving. Never hesitate to open a PR or an issue!\n\nGenerally, we're looking for:\n\n- **New workflow features**: Improvements to the core workflow engine, step decorators, event handling, or context management\n- **Better error handling and validation**: Enhanced error messages, better validation of workflow configurations, improved debugging capabilities, and better retry mechanisms\n- **Performance optimizations**: Improvements to workflow execution speed, memory usage, or concurrent processing\n- **Documentation improvements**: Better examples, tutorials, API documentation, or architectural explanations\n- **Testing enhancements**: More comprehensive test coverage, performance tests, or integration tests\n- **Checkpointing and persistence**: Improvements to workflow state management and resumption capabilities\n- **Resource management**: Better handling of external resources and dependencies\n- **Bug fixes**: Any issues you've encountered while using the library\n\nIdeas beyond the above are welcome! This list is not exhaustive.\n\n## Setup\n\nThis section assumes you have `uv` installed.\n\nWhen developing locally, development works best with a virtual environment. You can create one with:\n\n```bash\nuv venv\n# On MacOS/Linux\nsource .venv/bin/activate\n# On Windows\n.venv\\Scripts\\activate\n```\n\nThe project is a monorepo with multiple closely related packages. You can install all dependencies with:\n\n```bash\nuv sync --all-extras --all-packages\n```\n\nThe `pyproject.toml` files contain the dependencies for each project along with other details like the package name, version, etc. Generally you won't have to edit this file.\n\nLinting is done automatically with `pre-commit`. Initialize it with:\n\n```bash\nuv run pre-commit install\n```\n\n## Run tests\n\nUse the `dev` CLI to run tests across packages:\n\n```bash\n# Run all package tests\nuv run dev\n\n# Filter by substring match\nuv run dev -p workflows\nuv run dev -p server -p client\n\n# Pass pytest args after --\nuv run dev -- -k test_name\n```\n\nGenerally, all features should be covered by robust tests. If you are adding a new feature or fixing a bug, please add tests for it.\n\n### Manually running linting\n\nWe use `pre-commit` to run linting and formatting on the codebase. You can run it manually with:\n\n```bash\nuv run pre-commit run -a\n```\n\nThe `pre-commit` config is located in the `.pre-commit-config.yaml` file.\n\n## Changesets and Releases\n\nDespite being a Python project, we use [Changesets](https://github.com/changesets/changesets) for version management because it provides an excellent workflow for managing releases in a monorepo. Changesets makes it easy to track which packages need version bumps and helps generate changelogs automatically.\n\n### Adding a Changeset\n\nWhen you make a change that should be included in the next release, you need to add a changeset by running the following command (requires Node.js):\n\n```bash\nnpx @changesets/cli\n```\n\nThis will prompt you to:\n1. Select which packages are affected by your changes\n2. Choose the version bump type (major, minor, or patch)\n3. Write a summary of your changes\n\n### How It Works\n\nWhen a PR with changesets is merged, the changeset bot will:\n1. Sync versions from `package.json` to `pyproject.toml` files\n2. Update package versions according to the changesets\n3. Generate/update CHANGELOG files\n4. Create a \"Version Packages\" PR that can be merged to trigger the release\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License\n\nCopyright (c) 2026 LlamaIndex Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# LlamaAgents\n\n[![Unit Testing](https://github.com/run-llama/workflows/actions/workflows/test.yml/badge.svg)](https://github.com/run-llama/workflows/actions/workflows/test.yml)\n[![Coverage Status](https://coveralls.io/repos/github/run-llama/workflows/badge.svg?branch=main)](https://coveralls.io/github/run-llama/workflows?branch=main)\n[![GitHub contributors](https://img.shields.io/github/contributors/run-llama/workflows)](https://github.com/run-llama/llama-index-workflows/graphs/contributors)\n\n\n[![PyPI - Downloads](https://img.shields.io/pypi/dm/llama-index-workflows)](https://pypi.org/project/llama-index-workflows/)\n[![Discord](https://img.shields.io/discord/1059199217496772688)](https://discord.gg/dGcwcsnxhU)\n[![Twitter](https://img.shields.io/twitter/follow/llama_index)](https://x.com/llama_index)\n[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/LlamaIndex?style=plastic&logo=reddit&label=r%2FLlamaIndex&labelColor=white)](https://www.reddit.com/r/LlamaIndex/)\n\nAn open-source framework for building and shipping document-centric agents in Python.\n\nDocument workflows are messy. You're stitching together OCR, LLMs, structured extraction, classification, custom validation, and human review into pipelines that have to run reliably in production. The steps are slow and the payloads are heavy. A lot of the work is in-process Python: embedding models, image analysis, vision calls, custom heuristics that don't want to be a microservice. Standing up durable orchestration for that kind of workload is a project on its own, so most teams end up shoving the pipeline into a side process nobody else wants to integrate with.\n\nLlamaAgents is built on [**Agent Workflows**](https://developers.llamaindex.ai/python/llamaagents/workflows/), an event-driven orchestration library where steps are async Python functions that emit and consume events. Branch, loop, parallelize, persist state, recover from failures, all in plain Python with no DSL.\n\n## Grows with you\n\nDocument workloads have a wide range of shapes. Sometimes you're parsing five contracts in a notebook to prove a point. Others you're running a million invoices a month behind a customer's firewall, or you're iterating on extraction quality and shipping a new version every day. Agent Workflows is built to follow you across all of that without a rewrite.\n\nStart as a function you call from a script. Wrap it in a server when you need an API. Connect a coordination backend when you need durability. Turn on replication when you need to scale.\n\nAnd because it's a library at its core, the same workflow code drops into wherever the work has to actually run: a notebook for prototyping, a FastAPI app for your product, or a customer's locked-down environment when their documents can't leave it.\n\nFor more ideas of what it can do, take a look at [the examples](https://github.com/run-llama/llama-agents/tree/main/examples).\n\n## Use it as a library\n\nThe simplest path. `pip install llama-index-workflows`, define your workflow, and `await workflow.run(...)`. It has minimal dependencies and embeds anywhere: scripts, notebooks, servers. Durability is pluggable too: save and resume runs from a file, or connect to a database.\n\n```python\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nclass HelloWorkflow(Workflow):\n    @step\n    async def greet(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=f\"Hello, {ev.name}\")\n```\n\nSee the [`llama-index-workflows` package](https://github.com/run-llama/llama-agents/tree/main/packages/llama-index-workflows) for more details.\n\n## Mount it inside an app you already have\n\n[`llama-agents-server`](https://developers.llamaindex.ai/python/llamaagents/workflows/deployment/) wraps any workflow as a REST API with streaming, persistence, and human-in-the-loop support. Drop it into an existing Starlette/FastAPI app, or run it standalone. [`llama-agents-client`](https://developers.llamaindex.ai/python/llamaagents/workflows/client/) is the matching async client for calling workflows from other services.\n\n```python\nfrom llama_agents.server import WorkflowServer\n\nserver = WorkflowServer()\nserver.add_workflow(\"greet\", HelloWorkflow())\n```\n\nSee the [`llama-agents-server` package](https://github.com/run-llama/llama-agents/tree/main/packages/llama-agents-server) and the [`llama-agents-client` package](https://github.com/run-llama/llama-agents/tree/main/packages/llama-agents-client) for more details.\n\n## Or ship it as a deployable agent\n\n[`llamactl`](https://developers.llamaindex.ai/python/llamaagents/llamactl/getting-started/) is the CLI for building and deploying agent apps end-to-end. Init from a starter, develop locally with hot reload, then deploy to LlamaParse, AWS Bedrock AgentCore, or your own infra. Agents can be headless workflow services, MCP servers, or full-stack apps with a UI.\n\n```bash\nuv tool install llamactl\nllamactl init\nllamactl serve\nllamactl deployments create\n```\n\nSee the [`llamactl` package](https://github.com/run-llama/llama-agents/tree/main/packages/llamactl) for more details.\n\n## Works with [LlamaParse](https://developers.llamaindex.ai/python/cloud/)\n\nThe heavy document primitives (OCR, structured extraction, classification, splitting) are what LlamaParse is for. Plug them into your workflow as steps, let LlamaParse handle the document understanding, and keep your agent code focused on orchestration, business logic, and review.\n\nCheck out our [prebuilt templates with llamactl](https://developers.llamaindex.ai/python/llamaagents/llamactl/agent-templates/) to get started.\n"
  },
  {
    "path": "architecture-docs/build-api.md",
    "content": "# Build API\n\nThe Build API is a secure proxy service that enables deployment pods to access external resources without exposing credentials directly. It runs on port 8001 alongside the management API.\n\n## Purpose\n\nDeployment pods need access to private repositories and build artifacts, but can't safely store credentials. The Build API solves this by:\n\n1. Authenticating pods using deployment-specific tokens\n2. Proxying requests with appropriate credentials\n3. Enforcing network isolation between pods and sensitive services\n\n## Authentication\n\n- Each deployment gets a unique token generated by the operator\n- Pods authenticate to the build API using `Authorization: Bearer <token>` or HTTP Basic Auth\n- Basic Auth supports using either username or password as the token (for Git compatibility)\n- The build API validates tokens against Kubernetes CRDs\n- Network policies restrict pods to only access the build API port\n\n## Current Capabilities\n\n### Git Repository Proxying\n\nThe Build API provides Git HTTP protocol support for secure repository access:\n\n- **Protocol Support**: Handles all Git HTTP operations (GET/POST)\n  - directly forwards all requests to '/deployments/{deployment_id}' to the deployment's git repository\n  - Uses basic auth with the deployment token, and re-authenticates with the proxy git repo\n## Future Capabilities\n\n- **S3 Repositories**: Access private S3-based git repositories without complex pod permissions\n- **Build Artifacts**: Push/pull container images and build artifacts\n- **Multi-Provider Git**: GitLab, Bitbucket, and other git providers\n- **Credential Rotation**: Automatic token refresh\n\n## Security Benefits\n\n- Pods don't need direct cloud credentials or git tokens\n- Network policies isolate pods from management APIs\n- Centralized credential management and audit logging\n- Fine-grained access control per deployment\n"
  },
  {
    "path": "architecture-docs/control-loop.md",
    "content": "# Control Loop Architecture\n\nThe control loop is the core execution engine for workflows. It follows a **reducer pattern** — pure state transitions with side effects expressed as commands:\n\n```\nState + Tick --> (NewState, Commands)\n```\n\n[`control_loop.py`](../packages/llama-index-workflows/src/workflows/runtime/control_loop.py)\n\n## Main Loop\n\n```mermaid\nflowchart TD\n    A[Initialize: queue StartEvent, schedule timeout] --> B[Drain tick buffer]\n    B --> C{Buffer empty?}\n    C -- No --> D[Reduce tick --> state + commands]\n    D --> E[Execute commands]\n    E --> B\n    C -- Yes --> F[Wait for next completion]\n    F --> G{What completed?}\n    G -- Timeout --> H[Pop due scheduled ticks into buffer]\n    G -- External tick --> I[Add to buffer]\n    G -- Worker result --> J[Add TickStepResult to buffer]\n    H --> B\n    I --> B\n    J --> B\n```\n\n1. **Initialize** — Queue `StartEvent`, schedule workflow timeout, rewind any in-progress work from a prior run.\n2. **Drain tick buffer** — Process all queued ticks synchronously. Each tick runs through the reducer and its commands execute before the next tick.\n3. **Wait for next completion** — Build a task set (worker tasks + one pull task), then wait for the first to complete. Workers have priority over pull tasks.\n4. **Process completed task** — Route the result back into the tick buffer and loop.\n\n## Ticks and Commands\n\n**Ticks** are inputs to the reducer. They represent things that happen: events arriving, steps completing, cancellation requests, timeouts, and publish requests from steps. Each tick type dispatches to a dedicated reducer function.\n\n[`types/ticks.py`](../packages/llama-index-workflows/src/workflows/runtime/types/ticks.py) — all tick types\n\n**Commands** are outputs from the reducer — the side effects the loop executes. They represent actions to take: spawning step workers, queuing events (with optional delays), completing or failing the run, and publishing events to the external stream.\n\n[`types/commands.py`](../packages/llama-index-workflows/src/workflows/runtime/types/commands.py) — all command types\n\n## Runtime Integration\n\nThe control loop is runtime-agnostic. It talks to the outside world exclusively through `InternalRunAdapter` (see [core-overview.md — Runtime and Adapters](./core-overview.md#runtime-and-adapters)). This is the extension point — runtime decorators wrap the adapter to add behavior like tick persistence, idle detection, or event recording.\n\n```mermaid\nsequenceDiagram\n    participant CL as Control Loop\n    participant A as InternalRunAdapter\n    participant Ext as External (handler/client)\n\n    Note over CL: Main loop iteration\n    CL->>A: wait_receive() [pull task]\n    Ext-->>A: send_event() delivers tick\n    A-->>CL: WaitResultTick\n\n    CL->>CL: reduce tick --> (state, commands)\n    CL->>A: on_tick(tick) [journaling hook]\n\n    Note over CL: Execute commands\n    CL->>A: write_to_event_stream(event)\n    CL->>CL: spawn worker task\n\n    CL->>A: wait_for_next_task(task_set, timeout)\n    A-->>CL: completed task (worker or pull)\n```\n\n[`plugin.py`](../packages/llama-index-workflows/src/workflows/runtime/types/plugin.py) — full adapter interface\n\n## Key Design Decisions\n\n- **Deterministic replay** — The reducer is pure. Adapters can record ticks and replay them to reconstruct state, and override time functions for deterministic timestamps.\n- **Priority ordering** — Worker tasks complete before pull tasks, ensuring in-flight work finishes before accepting new external events.\n- **Optimistic execution with retry** — Workers receive a snapshot of collected events. If new events arrive during execution, the worker re-runs with the updated snapshot.\n- **State rehydration** — On resume, in-progress events move back to the queue and worker IDs reset, allowing clean restart from stored ticks.\n- **Idle detection** — When all steps are waiting on external input, the loop publishes `WorkflowIdleEvent`. Runtime decorators can use this signal to release idle workflows from memory.\n- **Retry-exhaustion hook** — The `StepWorkerFailed` branch of `_process_step_result_tick` routes a `StepFailedEvent` to a registered `@catch_error` handler. Handlers can be scoped (`@catch_error(for_steps=[...])`) or wildcard, with a per-handler `max_recoveries` budget tracked per event lineage in `recovery_counts: dict[str, int]` on `EventAttempt` / `TickAddEvent` / `CommandQueueEvent`. Routing consults `BrokerConfig.handler_for_step` and `BrokerConfig.catch_error_handlers`; when the count exceeds `max_recoveries` or no handler owns the step, the loop publishes a `WorkflowFailedEvent` carrying the live exception and fails the run. The live `Exception` rides on `EventAttempt` / `TickAddEvent` / `CommandQueueEvent` between retries — annotated with `SerializableException` where it crosses a pydantic serialization boundary — and is exposed to step bodies via `Context.retry_info()`.\n"
  },
  {
    "path": "architecture-docs/core-overview.md",
    "content": "# Core Architecture: Workflow, Context, and Runtime\n\n## Overview\n\n```mermaid\ngraph LR\n    subgraph Client [\"Client (public API)\"]\n        WR[\"Workflow.run()\"]\n        WH[WorkflowHandler]\n        EC[\"Context\\n(ExternalContext)\"]\n    end\n\n    subgraph Runtime\n        EA[ExternalRunAdapter]\n        CL[Control Loop]\n        IA[InternalRunAdapter]\n    end\n\n    subgraph Worker [\"Worker (per step)\"]\n        IC[\"Context\\n(InternalContext)\"]\n        Steps[Step Functions]\n    end\n\n    WR -->|launches| CL\n    WH --- EA\n    EC --- EA\n    EA --- CL\n    CL --- IA\n    IA --- IC\n    IA --- Steps\n```\n\nThe system has three zones. **Client** code calls `Workflow.run()` and interacts through `WorkflowHandler` and `Context`. The **Runtime** sits in the middle — the control loop drives execution, with `ExternalRunAdapter` and `InternalRunAdapter` as its boundaries. **Workers** are step functions that see `Context` with a different face (InternalContext).\n\nThe adapters are the key abstraction boundary. Everything on the client side goes through `ExternalRunAdapter`; everything on the worker side goes through `InternalRunAdapter`. The runtime is swappable by replacing or decorating these adapters.\n\n## Workflow\n\nContainer for step definitions. `run()` selects a runtime, validates steps, and delegates to the runtime for execution.\n\n[`workflow.py`](../packages/llama-index-workflows/src/workflows/workflow.py)\n\n## Context Faces\n\nContext presents different interfaces depending on execution phase. Internally it holds a `_face` field that transitions through three types:\n\n```mermaid\ngraph LR\n    Pre[PreContext] -->|\"_workflow_run()\"| External[ExternalContext]\n    External -.->|\"per step worker\"| Internal[InternalContext]\n```\n\n| Face | When | Used By |\n|------|------|---------|\n| PreContext | Before `run()` | Setup code — configuration, serialization, state store init |\n| ExternalContext | After `run()` | Handler / caller — sending events, streaming |\n| InternalContext | During step execution | Step functions — collecting events, publishing to stream |\n\nEach face wraps an adapter from the runtime. Public methods on Context check the current face and raise `ContextStateError` if called in the wrong phase.\n\n[`context/`](../packages/llama-index-workflows/src/workflows/context/) — Context implementation and all face types\n\n## Runtime and Adapters\n\nThe `Runtime` ABC uses a dual-adapter pattern. Each workflow run produces two adapters sharing a `run_id`:\n\n```mermaid\ngraph TB\n    Runtime -->|\"run_workflow()\"| ExternalRunAdapter\n    Runtime -->|\"get_internal_adapter()\"| InternalRunAdapter\n    ExternalRunAdapter --- run_id\n    InternalRunAdapter --- run_id\n```\n\nThe **InternalRunAdapter** is used by the control loop — it handles receiving ticks, publishing events, timing, and task coordination. The **ExternalRunAdapter** is used by the WorkflowHandler — it handles sending events in, streaming published events out, getting results, and cancellation.\n\nMultiple base runtimes exist. `BasicRuntime` is the default in-memory asyncio runtime in the core package. `DBOSRuntime` provides durable distributed execution backed by a database. More runtimes can be added by implementing the `Runtime` ABC. Runtimes are also composable via decorators — see [server-architecture.md](./server-architecture.md#runtime-decorator-chain) for that pattern.\n\n[`plugin.py`](../packages/llama-index-workflows/src/workflows/runtime/types/plugin.py) — Runtime ABC, InternalRunAdapter, ExternalRunAdapter\n\n## Control Loop\n\nThe control loop is the core execution engine. It follows a reducer pattern where pure state transitions produce side effects as commands. The control loop is runtime-agnostic — it interacts with the outside world exclusively through `InternalRunAdapter`.\n\nSee [control-loop.md](./control-loop.md) for the full architecture.\n\n## Event Flow\n\n```mermaid\ngraph LR\n    subgraph \"Events In (external to internal)\"\n        H[WorkflowHandler] -->|send_event| EA[ExternalRunAdapter]\n        EA -->|receive queue| IA[InternalRunAdapter]\n        IA -->|tick| CL[Control Loop]\n    end\n```\n\n```mermaid\ngraph LR\n    subgraph \"Events Out (internal to external)\"\n        CL2[Control Loop] -->|command| IA2[InternalRunAdapter]\n        IA2 -->|publish queue| EA2[ExternalRunAdapter]\n        EA2 -->|stream_published_events| H2[WorkflowHandler]\n    end\n```\n\n## WorkflowHandler\n\nReturned by `Workflow.run()`. The user-facing handle for a running workflow.\n\n- `await handler` — blocks until StopEvent, returns the result\n- `handler.stream_events()` — async iterator of published events (single consumption)\n- `handler.send_event()` — send events into the running workflow\n- `handler.cancel_run()` — graceful cancellation\n\n[`handler.py`](../packages/llama-index-workflows/src/workflows/handler.py)\n"
  },
  {
    "path": "architecture-docs/overall-architecture.md",
    "content": "# Cloud Llama Deploy - Overall Architecture\n\nThis document outlines the architecture of the Cloud Llama Deploy system, which enables deployment and management of LlamaIndex workflows on Kubernetes.\n\n## High-Level System Overview\n\nThe system consists of several interconnected components that work together to provide a complete deployment platform:\n\n```mermaid\ngraph TD\n    CLI[\"CLI (llamactl)\"] --> CP[\"Control Plane (FastAPI backend)\"]\n    CP --> K8S[\"Kubernetes\"]\n    K8S --> OP[\"Operator (Go)\"]\n    OP --> POD[\"App Server (Python Pod)\"]\n    OP -.-> CRD[\"LlamaDeployment CRDs\"]\n    POD --> WF[\"User Workflows\"]\n```\n\n## Component Details\n\n### 1. Core Package (`llama-deploy-core`)\n**Purpose**: Shared data models and schemas used across all components.\n\n**Role**: Ensures consistent data structures across the entire system.\n\n### 2. Control Plane (`llama-deploy-control-plane`)\n**Purpose**: Main orchestration layer that manages deployments via Kubernetes.\n\n**Components**:\n- **K8s Client** (`k8s_client.py`): Interfaces with Kubernetes API to create/manage LlamaDeployment CRDs\n- **Git Integration**: Clones repositories, validates Git refs, handles GitHub authentication\n- **API Endpoints**: REST API for deployment and project management\n\n**Flow**:\n```mermaid\ngraph LR\n    UR[\"User Request\"] --> CP[\"Control Plane API\"]\n    CP --> KC[\"K8s Client\"]\n    KC --> CRD[\"LlamaDeployment CRD\"]\n    CRD --> K8S[\"Kubernetes\"]\n```\n\n### 3. Kubernetes Operator (`operator/`)\n**Purpose**: Kubernetes controller that reconciles LlamaDeployment custom resources.\n\n**Key Components**:\n- **CRD Definition**: `LlamaDeployment` custom resource with spec (repo URL, deployment file, git ref)\n- **Controller**: Watches for LlamaDeployment changes and creates/updates Kubernetes resources\n- **Reconciliation Loop**: Creates deployments, services, secrets, and ingresses\n\n**Managed Resources**:\n```mermaid\ngraph TD\n    CRD[\"LlamaDeployment CRD\"] --> DEP[\"Deployment<br/>(API Server pod)\"]\n    CRD --> SVC[\"Service<br/>(Load balancer)\"]\n    CRD --> SEC[\"Secret<br/>(PAT tokens, env vars)\"]\n    CRD --> SA[\"ServiceAccount\"]\n    CRD --> ING[\"Ingress<br/>(optional)\"]\n```\n\n**Phases**: `Syncing` → `Pending` → `Running` / `Failed` / `RollingOut`\n\n### 4. App Server (`appserver`)\n**Purpose**: Runtime environment that executes user workflows and provides APIs.\n\n**Key Components**:\n- **Deployment Manager**: Loads and manages workflow deployments from config\n- **Workflow Execution**: Runs LlamaIndex workflows with session management\n- **Source Managers**: Handle Git, local, and Docker sources for workflow code\n- **API Endpoints**: REST and WebSocket APIs for interacting with workflows\n\n**Configuration**:\n- **Primary**: Embedded in `pyproject.toml` under `[tool.llamadeploy]`\n- **Alternative**: `llama_deploy.toml` or `llama_deploy.yaml` with the same schema\n\nExample (TOML in `pyproject.toml`):\n```toml\n[tool.llamadeploy]\nname = \"my-deployment\"\napp = \"path.to.module:app\"\n\n[tool.llamadeploy.ui]\ndirectory = \"./ui\"\n```\n\n### 5. CLI Tool (`llamactl`)\n**Purpose**: Command-line interface for users to interact with the control plane.\n```bash\nllamactl environments use https://api.cloud.llamaindex.ai\nllamactl auth login\nllamactl auth token --project <PROJECT_ID>\nllamactl auth logout\nllamactl deployments template > deployment.yaml\nllamactl deployments apply -f deployment.yaml\nllamactl deployments create\nllamactl deployments get\nllamactl deployments get NAME\nllamactl deployments edit NAME\nllamactl deployments update NAME\nllamactl deployments logs NAME --follow\n```\n\n## Component Interaction Flow\n\n### Deployment Creation Flow\n```mermaid\nsequenceDiagram\n    participant User\n    participant CP as Control Plane\n    participant K8s as Kubernetes\n    participant Op as Operator\n    participant Pod as API Server Pod\n\n    User->>CP: Create deployment request\n    CP->>CP: Validate Git repository\n    CP->>K8s: Create LlamaDeployment CRD\n    K8s->>Op: CRD change detected\n    Op->>K8s: Create Deployment/Service/Secret\n    K8s->>Pod: Start API Server pod\n    Pod->>Pod: Clone workflow code\n    Pod->>Pod: Load workflow and expose APIs\n    Pod->>K8s: Update status\n    K8s->>CP: Status propagation\n    CP->>User: Deployment ready\n```\n\n### Data Flow\n**Configuration**: Git Repo → Control Plane → LlamaDeployment CRD → Operator → API Server Pod\n**Runtime**: User Request → API Server → Workflow Engine → Response\n**Monitoring**: API Server → Prometheus Metrics | Operator → K8s Events → Status Updates\n\n## Networking & Communication\n\n- **External Access**: Ingress → Service → API Server Pod\n- **Internal K8s**: Operator talks to K8s API server\n- **Control Plane**: Communicates with K8s via client libraries\n- **CLI**: HTTP calls to Control Plane API\n- **Workflow APIs**: Direct HTTP/WebSocket to API Server pods\n\n## Namespace Layout\n\nSet `apps.namespace` to run the control plane + operator in the release\nnamespace and put `LlamaDeployment` CRs and their child resources (Deployments,\nPods, Services, Secrets, ServiceAccounts, ConfigMaps, Ingresses, NetworkPolicies,\nbuild Jobs) in a separate namespace. Unset = everything in the release namespace.\n\nCRs stay co-located with their children (cross-namespace owner references are\nnot allowed). Only `WATCH_NAMESPACE` (operator) and `KUBERNETES_NAMESPACE`\n(control plane) point at the apps namespace; reconciler code is unchanged.\n\nRBAC in split mode: apps-namespace Role with every rule except\n`coordination.k8s.io/leases`, release-namespace Role with just that rule for\nleader election. Unset collapses to one Role.\n\n`imagePullSecrets` are not mirrored — provision them in the apps namespace, or\nuse node-level pull credentials.\n"
  },
  {
    "path": "architecture-docs/quick-reference.md",
    "content": "## Code Navigation Guide\n\n### Entry Points & Main Classes\n\n**Control Plane** (`packages/llama-deploy-control-plane/`)\n- Entry: `src/llama_deploy/control_plane/main.py` - FastAPI app\n- K8s management: `src/llama_deploy/control_plane/k8s_client.py` - `K8sClient` class\n- API endpoints: `src/llama_deploy/control_plane/endpoints/deployments.py`, `projects.py`\n\n**Operator** (`operator/`)\n- Entry: `cmd/main.go` - `func main()`\n- Controller: `internal/controller/llamadeployment_controller.go` - `LlamaDeploymentReconciler`\n- CRD types: `api/v1/llamadeployment_types.go` - `LlamaDeploymentSpec`, `LlamaDeploymentStatus`\n\n**API Server** (`packages/llama-deploy-appserver/`)\n- Entry: `src/llama_deploy/appserver/__main__.py` or `main.py`\n- Manager: `src/llama_deploy/appserver/deployment.py` - `Manager` class (orchestrates deployments)\n- Deployment: `src/llama_deploy/appserver/deployment.py` - `Deployment` class (runs workflows)\n- Config parser: `src/llama_deploy/appserver/deployment_config_parser.py` - `DeploymentConfig.from_yaml()`\n- Routers: `src/llama_deploy/appserver/routers/deployments.py`, `status.py`\n\n**CLI** (`packages/llamactl/`)\n- Entry: `src/llama_deploy/cli/__init__.py` - `main()` function\n- Client: `src/llama_deploy/cli/client.py` - control plane/project client helpers\n- Commands: `src/llama_deploy/cli/commands/*` - Click command definitions\n- Config: `src/llama_deploy/cli/config/_config.py` - `ConfigManager`, with env support\n\n**Core Schemas** (`packages/llama-deploy-core/`)\n- Base: `src/llama_deploy/core/schema/base.py` - `Base` model class\n- Deployments: `src/llama_deploy/core/schema/deployments.py` - `DeploymentResponse`, `LlamaDeploymentSpec`\n- Projects: `src/llama_deploy/core/schema/projects.py` - `ProjectSummary`\n\n### Key Configuration Files\n\n**Deployment Configuration (preferred)**: Embedded in `pyproject.toml` under `[tool.llamadeploy]`.\n```toml\n[tool.llamadeploy]\nname = \"my-deployment\"\napp = \"path.to.module:app\"  # or use `workflows = { my_workflow = \"path.to.module:workflow\" }`\n\n[tool.llamadeploy.ui]\ndirectory = \"./ui\"\n# Optional overrides:\n# build_output_dir = \"dist\"\n# package_manager = \"pnpm\"\n# build_command = \"build\"\n# serve_command = \"dev\"\n# proxy_port = 4502\n```\n\n**Alternative**: `llama_deploy.toml` or `llama_deploy.yaml` with the same schema as `[tool.llamadeploy]`.\n\n**Helm Chart**: `charts/llama-agents/values.yaml`\n**Kubernetes CRD**: `operator/config/crd/bases/deploy.llamaindex.ai_llamadeployments.yaml`\n**RBAC**: `operator/config/rbac/role.yaml`\n\n### Key API Endpoints\n\n**Control Plane** (port 8000):\n- `POST /{project_id}/deployments` - Create deployment\n- `GET /{project_id}/deployments` - List deployments\n- `GET /{project_id}/deployments/{id}` - Get deployment details\n- `POST /{project_id}/deployments/validate-repository` - Validate Git repo\n\n**Build API** (port 8001, token auth required):\n- `GET /health` - Health check\n\n**API Server** (port 8080 in pod):\n- `POST /deployments/{name}/tasks/run` - Execute workflow\n- `GET /deployments/{name}/tasks/{task_id}/results` - Get task results\n- `POST /deployments/{name}/sessions/create` - Create session\n- `GET /health` - Health check\n\n### CLI Commands Reference\n```bash\nllamactl environments get           # List environments\nllamactl environments use <URL>     # Switch current environment\nllamactl auth token --project <PROJECT_ID>  # Create/select profile via API key\nllamactl auth get                   # List profiles\nllamactl auth use <NAME>            # Switch profile\nllamactl projects get               # List projects\nllamactl projects use <PROJECT_ID>  # Switch active project\nllamactl deployments template > deployment.yaml  # Generate apply YAML\nllamactl deployments apply -f deployment.yaml    # Create or update from YAML\nllamactl deployments create          # Create new deployment in $EDITOR\nllamactl deployments get             # List deployments\nllamactl deployments get NAME        # Get deployment details\nllamactl deployments edit NAME       # Edit deployment in $EDITOR\nllamactl deployments update NAME     # Re-resolve current git ref and release it\nllamactl deployments logs NAME --follow  # Stream deployment logs\nllamactl deployments delete NAME     # Delete deployment\n```\n\n### Commands Reference\n\n**Development Setup:**\n```bash\nuv sync --all-packages --all-extras  # Install all dependencies including dev\nuv run pre-commit run -a             # Lint & format\nuv run dev                           # Run all package tests\n```\n\n**Development Environment:**\n```bash\nuv run operator/dev.py up             # Set up kind cluster and start development\nuv run operator/dev.py down           # Clean up deployed resources\nuv run operator/dev.py down --delete  # Delete the kind cluster\nuv run operator/dev.py status         # Show cluster status\n```\n\n**Operator Development (Makefile in `operator/`):**\n```bash\nmake -C operator operator-build      # Build operator binary\nmake -C operator operator-test       # Run operator tests\nmake -C operator operator-manifests  # Generate CRDs and RBAC\nmake -C operator operator-generate   # Generate DeepCopy methods\n```\n\n### Key Constants & Defaults\n- Default discovery order: `llama_deploy.toml` → `pyproject.toml` → `llama_deploy.yaml`\n- API Server port: `8080`\n- Control Plane port: `8000`\n- Build API port: `8001`\n- Kubernetes namespace: `llama-agents` (default)\n- CRD group: `deploy.llamaindex.ai`\n- Container image: `llamaindex/llama-deploy:main-autodeploy`\n"
  },
  {
    "path": "architecture-docs/server-architecture.md",
    "content": "# Server Architecture\n\n## Overview\n\nThe server wraps the core workflow engine (see [core-overview.md](./core-overview.md)) with HTTP access, persistence, and durability. Components are layered with clear boundaries:\n\n```mermaid\ngraph TD\n    Client -->|HTTP| API[\"_WorkflowAPI\"]\n    API --> Service[\"_WorkflowService\"]\n    Service --> Runtime[\"Runtime decorator chain\"]\n    Runtime --> ControlLoop[\"Control Loop\"]\n    Runtime -.->|reads/writes| Store[\"WorkflowStore\"]\n    Service -.->|queries| Store\n    API -.->|subscribes events| Store\n```\n\nThe **store** is the shared persistence layer — the runtime writes to it, the service queries it, and the API streams from it.\n\n## Components\n\n**`WorkflowServer`** — Entry point. Assembles the runtime chain, service, and API into a Starlette app. Registers workflows.\n[`server.py`](../packages/llama-agents-server/src/llama_agents/server/server.py)\n\n**`_WorkflowAPI`** — Starlette routes. Translates HTTP requests into service calls and streams events from the store to clients via SSE.\n[`_api.py`](../packages/llama-agents-server/src/llama_agents/server/_api.py)\n\n**`_WorkflowService`** — Application logic. Starts workflows, manages handler lifecycle, coordinates event sending and cancellation. Bridges the API to the runtime.\n[`_service.py`](../packages/llama-agents-server/src/llama_agents/server/_service.py)\n\n**Runtime decorators** — A chain of decorators that add server concerns (event recording, tick persistence, idle release, etc.) on top of a base runtime. See the section below.\n[`_runtime/`](../packages/llama-agents-server/src/llama_agents/server/_runtime/) — standard server decorators\n\n**`AbstractWorkflowStore`** — Persistence contract shared by all layers above.\n[`abstract_workflow_store.py`](../packages/llama-agents-server/src/llama_agents/server/_store/abstract_workflow_store.py)\n\n## Runtime Decorator Chain\n\nRuntimes compose via decoration. Each decorator wraps a `Runtime` and its adapters (see [core-overview.md — Runtime and Adapters](./core-overview.md#runtime-and-adapters)), overriding only the methods it needs to add a specific concern — event recording to a store, tick persistence for replay, idle detection and memory release, etc.\n\n```mermaid\ngraph LR\n    Outer[\"Server decorators\\n(one per concern)\"] -->|wraps| Inner[\"Base Runtime\\n(BasicRuntime, DBOSRuntime, etc.)\"]\n```\n\n`WorkflowServer` assembles a default decorator chain on top of whatever base runtime is provided. The base runtime is swappable — pass `runtime=` to `WorkflowServer` to use a different one (BasicRuntime, DBOSRuntime, or a custom implementation). See `server.py` for how the default chain is assembled.\n\n**Writing a decorator:** Extend `BaseRuntimeDecorator` and optionally `BaseInternalRunAdapterDecorator` / `BaseExternalRunAdapterDecorator`. These forward all methods to the inner runtime/adapter — override only what you need.\n[`runtime_decorators.py`](../packages/llama-agents-server/src/llama_agents/server/_runtime/runtime_decorators.py)\n\n## Persistence (WorkflowStore)\n\nThe store is the system's source of truth for anything that survives a restart or an idle-release cycle. All layers depend on it:\n\n| What's stored | Purpose |\n|---|---|\n| Handler records | Lifecycle tracking (status, timestamps, result) |\n| Event log | Resumable event streaming to clients |\n| Ticks | Rebuild workflow state after idle release or restart |\n| State stores | Persistent key-value state per run |\n\nTwo implementations:\n- **`MemoryWorkflowStore`** — In-process dicts. No persistence across restarts.\n  [`memory_workflow_store.py`](../packages/llama-agents-server/src/llama_agents/server/_store/memory_workflow_store.py)\n- **`SqliteWorkflowStore`** — SQLite-backed. Survives restarts.\n  [`sqlite_workflow_store.py`](../packages/llama-agents-server/src/llama_agents/server/_store/sqlite/sqlite_workflow_store.py)\n\n## Resumable Event Streams\n\nEvents flow from step functions to clients through the store, which acts as both a write-ahead log and a subscription source:\n\n```mermaid\ngraph LR\n    Step[\"Step function\"] -->|\"ctx.write_event_to_stream()\"| IA[\"InternalRunAdapter\"]\n    IA -->|decorator intercepts| Store[\"WorkflowStore\"]\n    Store -->|\"subscribe_events(cursor)\"| API[\"_WorkflowAPI\"]\n    API -->|SSE| Client\n```\n\nThe store assigns each event a monotonic **sequence number**. Clients track their position via this cursor:\n\n1. Client connects, receives events as SSE with `id: {sequence}`\n2. Client disconnects (network drop, restart, etc.)\n3. Client reconnects with `Last-Event-ID: {last_seen_sequence}`\n4. Store replays all events after that sequence, then continues live\n\nThis means the API layer never needs to hold event history in memory — it just opens a `subscribe_events(after_sequence=cursor)` iterator from the store each time a client connects.\n\nA special `\"now\"` cursor skips all historical events and streams only new ones.\n\n## Server Lifecycle\n\n**Start:** `WorkflowServer.start()` queries the store for handlers with `status=running` that aren't idle, and resumes each by rebuilding context from stored ticks.\n\n**Stop:** `WorkflowServer.stop()` aborts all active control loops. Handler records remain in the store — they'll resume on next start.\n\n**Idle release:** When the [control loop detects](./control-loop.md#key-design-decisions) all steps are waiting on external input, it publishes `WorkflowIdleEvent`. Runtime decorators can use this signal to release the workflow from memory. When a new event arrives for that workflow, it reloads from ticks transparently.\n"
  },
  {
    "path": "charts/AGENTS.md",
    "content": "## Helm Chart\n\nMakefile lives in `operator/`. Run targets with `make -C operator <target>` from repo root, or `make <target>` from within `operator/`.\n\nSetup:\n- Ensure kind context: `make -C operator kube-ensure-kind-context`\n- Install helm-unittest: `make -C operator helm-unittest-install`\n- Install Prometheus Operator CRDs: `make -C operator helm-crds-prom-operator`\n\nChecks:\n- Lint (default values): `make -C operator helm-lint`\n- Lint (dev values): `make -C operator helm-lint-dev`\n- Template (default/dev): `make -C operator helm-template` / `make -C operator helm-template-dev`\n- Server-side dry-run (default/dev): `make -C operator helm-dry-run` / `make -C operator helm-dry-run-dev`\n- Run Helm unit tests: `make -C operator helm-unittest`\n"
  },
  {
    "path": "charts/llama-agents/.helmignore",
    "content": "# Patterns to ignore when building packages.\n.DS_Store\n*.swp\n*.bak\n*.tmp\n*~\n.git\n.gitignore\n\n# Node modules symlink pulls in operator binaries which exceed Helm's 5MB file limit\nnode_modules\n"
  },
  {
    "path": "charts/llama-agents/CHANGELOG.md",
    "content": "# llama-agents\n\n## 0.12.3\n\n### Patch Changes\n\n- Updated dependencies [c3fac21]\n  - llama-agents-control-plane@0.12.2\n  - llama-agents-appserver@0.11.4\n\n## 0.12.2\n\n### Patch Changes\n\n- Updated dependencies [463c79d]\n  - llama-agents-control-plane@0.12.1\n  - llama-agents-appserver@0.11.3\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [2280e04]\n  - llama-agents-control-plane@0.12.0\n  - llama-agents-appserver@0.11.2\n\n## 0.12.0\n\n### Minor Changes\n\n- 3ced443: Optional s3proxy sidecar for non-AWS object storage, plus inline-or-BYO creds on both the sidecar and control plane S3.\n\n### Patch Changes\n\n- 9eda189: Document the compatible `llama-agents-crds` chart version via a new `crds.version` values field, auto-synced at release time and surfaced in the README.\n\n## 0.11.1\n\n### Patch Changes\n\n- Updated dependencies [916b157]\n  - llama-agents-appserver@0.11.1\n\n## 0.11.0\n\n### Minor Changes\n\n- de5bedc: Add `apps.namespace` to run `LlamaDeployment` CRs and their child resources in a separate namespace from the operator + control plane. Unset = everything in the release namespace.\n\n### Patch Changes\n\n- facbac4: New network policy values: `extraEgressRules`, configurable DNS selectors, and `blockPrivateRanges` toggle for reaching in-cluster services without disabling the policy.\n- Updated dependencies [facbac4]\n- Updated dependencies [64579a9]\n  - llama-agents-appserver@0.11.0\n  - llama-agents-control-plane@0.11.1\n\n## 0.10.12\n\n### Patch Changes\n\n- Updated dependencies [fdc1c48]\n  - llama-agents-operator@0.11.1\n\n## 0.10.11\n\n### Patch Changes\n\n- Updated dependencies [e8b8f47]\n  - llama-agents-control-plane@0.11.0\n  - llama-agents-appserver@0.10.5\n\n## 0.10.10\n\n### Patch Changes\n\n- Updated dependencies [7ad3049]\n  - llama-agents-control-plane@0.10.5\n  - llama-agents-appserver@0.10.4\n\n## 0.10.9\n\n### Patch Changes\n\n- Updated dependencies [286c91a]\n  - llama-agents-appserver@0.10.3\n\n## 0.10.8\n\n### Patch Changes\n\n- Updated dependencies [740ee9e]\n  - llama-agents-control-plane@0.10.4\n\n## 0.10.7\n\n### Patch Changes\n\n- llama-agents-appserver@0.10.2\n- llama-agents-control-plane@0.10.3\n\n## 0.10.6\n\n### Patch Changes\n\n- Updated dependencies [3f12660]\n  - llama-agents-control-plane@0.10.2\n  - llama-agents-appserver@0.10.1\n\n## 0.10.5\n\n### Patch Changes\n\n- Updated dependencies [3e2e7b8]\n  - llama-agents-appserver@0.10.0\n  - llama-agents-operator@0.11.0\n\n## 0.10.4\n\n### Patch Changes\n\n- Updated dependencies [46f2675]\n  - llama-agents-control-plane@0.10.1\n  - llama-agents-appserver@0.9.1\n\n## 0.10.3\n\n### Patch Changes\n\n- Updated dependencies [782939b]\n  - llama-agents-operator@0.10.2\n\n## 0.10.2\n\n### Patch Changes\n\n- Updated dependencies [de92a8b]\n  - llama-agents-operator@0.10.1\n\n## 0.10.1\n\n### Patch Changes\n\n- Updated dependencies [58e7942]\n- Updated dependencies [ea577a1]\n  - llama-agents-control-plane@0.10.0\n  - llama-agents-appserver@0.9.0\n  - llama-agents-operator@0.10.0\n\n## 0.10.0\n\n### Minor Changes\n\n- 7025b30: Rename Helm chart from cloud-llama-deploy-chart to llama-agents-chart with standardized resource naming\n"
  },
  {
    "path": "charts/llama-agents/Chart.yaml",
    "content": "apiVersion: v2\nname: llama-agents\ndescription: A Helm chart for deploying Llama Agents (control plane + operator)\ntype: application\nversion: \"0.12.3\"\n"
  },
  {
    "path": "charts/llama-agents/README.md",
    "content": "# llama-agents\n\nA Helm chart for deploying Llama Agents (control plane + operator)\n\n## Architecture\n\nThis chart deploys two components:\n\n- **Control plane** — API server for managing deployments, builds, and backups\n- **Operator** — Kubernetes controller that reconciles `LlamaDeployment` custom resources into running pods\n\nCRDs (`LlamaDeployment`, `LlamaDeploymentTemplate`) are included in the chart's `crds/` directory and installed automatically on first `helm install`. They are **not** modified on upgrade or removed on uninstall (standard Helm CRD behavior).\n\nFor managed CRD upgrades, use the companion [`llama-agents-crds`](../llama-agents-crds/) chart. Each release of `llama-agents` pins the compatible CRD chart version in `crds.version` (see the values table below) — use that version when installing or upgrading the CRD chart.\n\n## Prerequisites\n\n- Kubernetes 1.26+\n- Helm 3.x\n- S3-compatible object storage (for build artifacts and backups)\n\n## Installation\n\n### Fresh install\n\n```bash\nhelm install llama-agents oci://docker.io/llamaindex/llama-agents \\\n  --set controlPlane.objectStorage.s3.bucket=my-bucket \\\n  --set controlPlane.objectStorage.s3.region=us-east-1\n```\n\nCRDs are installed automatically from the `crds/` directory.\n\n### With separate CRD management\n\nIf you prefer explicit CRD lifecycle management (recommended for production):\n\n```bash\n# Install CRD chart first — pin to the compatible version from `crds.version` below\nhelm install llama-agents-crds oci://docker.io/llamaindex/llama-agents-crds --version <crds.version>\n\n# Install main chart, skipping bundled CRDs\nhelm install llama-agents oci://docker.io/llamaindex/llama-agents --skip-crds \\\n  --set controlPlane.objectStorage.s3.bucket=my-bucket\n```\n\n## Upgrading\n\n```bash\n# If CRD schema has changed, upgrade CRDs first — pin to `crds.version` from the values table\nhelm upgrade --install llama-agents-crds oci://docker.io/llamaindex/llama-agents-crds --version <crds.version>\n\n# Then upgrade the main chart\nhelm upgrade llama-agents oci://docker.io/llamaindex/llama-agents\n```\n\n## Apps namespace\n\nSet `apps.namespace` to isolate `LlamaDeployment` CRs and their child resources\nin a separate namespace. The operator + control plane stay in the release\nnamespace and target the apps namespace for all app resources.\n\n```bash\nkubectl create namespace llama-agents-apps\nhelm install llama-agents oci://docker.io/llamaindex/llama-agents \\\n  --namespace llama-agents \\\n  --set apps.namespace=llama-agents-apps \\\n  --set controlPlane.objectStorage.s3.bucket=my-bucket\n```\n\n`imagePullSecrets` are not mirrored — provision them in the apps namespace\nyourself, or use node-level pull credentials. Switching modes on an existing\ninstall requires draining and recreating `LlamaDeployment` CRs.\n\n## Non-S3 object storage\n\nSet `s3proxy.enabled=true` to run an\n[s3proxy](https://github.com/gaul/s3proxy) sidecar alongside the control\nplane. When enabled, `S3_ENDPOINT_URL` points at the sidecar on localhost and\n`S3_UNSIGNED` defaults to `true`; explicit overrides still win.\n\nCredentials take one of two forms:\n\n```yaml\n# Inline — chart renders llama-agents-s3proxy Secret\ns3proxy:\n  enabled: true\n  config:\n    JCLOUDS_PROVIDER: <provider>\n    JCLOUDS_IDENTITY: <id>\n    JCLOUDS_CREDENTIAL: <secret>\n    # ...any other JCLOUDS_* vars the backend needs\n```\n\n```yaml\n# BYO — point at an existing Secret whose keys are the sidecar env vars\ns3proxy:\n  enabled: true\n  secret: my-existing-s3proxy-secret\n```\n\nPick `JCLOUDS_*` vars from the\n[s3proxy storage-backend examples](https://github.com/gaul/s3proxy/wiki/Storage-backend-examples).\nIf both `config` and `secret` are set, `secret` wins.\n\n## Control plane S3 credentials\n\nThree mutually-exclusive forms, listed in precedence order:\n\n```yaml\n# BYO — envFroms an existing Secret (keys: S3_ACCESS_KEY, S3_SECRET_KEY)\ncontrolPlane:\n  objectStorage:\n    s3:\n      bucket: my-bucket\n      secret: my-s3-creds\n```\n\n```yaml\n# Inline — chart renders llama-agents-controlplane-s3 Secret\ncontrolPlane:\n  objectStorage:\n    s3:\n      bucket: my-bucket\n      accessKey: AKIA...\n      secretKey: ...\n```\n\n```yaml\n# Neither — control plane relies on IRSA / workload identity\ncontrolPlane:\n  objectStorage:\n    s3:\n      bucket: my-bucket\n```\n\nPartial inline (one of `accessKey`/`secretKey` set) is a template error.\n\n## Values\n\n### Metrics\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| metrics.enabled | bool | `false` | Enable Prometheus ServiceMonitors |\n| metrics.scrapeInterval | string | `\"30s\"` | Scrape interval for ServiceMonitors |\n| metrics.scrapeTimeout | string | `\"10s\"` | Scrape timeout for ServiceMonitors |\n| metrics.additionalMonitorLabels | object | `{}` | Extra labels added to ServiceMonitors for Prometheus discovery (e.g., `release: prometheus`) |\n\n### Images\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| images.controlPlane.repository | string | `\"llamaindex/llama-agents-control-plane\"` | Control plane image repository |\n| images.controlPlane.tag | string | `\"0.12.2\"` | Control plane image tag |\n| images.controlPlane.pullPolicy | string | `\"IfNotPresent\"` | Control plane image pull policy |\n| images.operator.repository | string | `\"llamaindex/llama-agents-operator\"` | Operator image repository |\n| images.operator.tag | string | `\"0.11.1\"` | Operator image tag |\n| images.operator.pullPolicy | string | `\"IfNotPresent\"` | Operator image pull policy |\n| images.appserver.repository | string | `\"llamaindex/llama-agents-appserver\"` | Appserver image repository (used by operator for managed pods) |\n| images.appserver.tag | string | `\"0.11.4\"` | Appserver image tag |\n| images.appserver.pullPolicy | string | `\"IfNotPresent\"` | Appserver image pull policy |\n| images.nginx.repository | string | `\"nginxinc/nginx-unprivileged\"` | Nginx sidecar image repository |\n| images.nginx.tag | string | `\"1.27-alpine\"` | Nginx sidecar image tag |\n| images.nginx.pullPolicy | string | `\"IfNotPresent\"` | Nginx sidecar image pull policy |\n\n### Control Plane\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| controlPlane.replicas | int | `1` | Number of control plane replicas |\n| controlPlane.container.port | int | `8000` | Control plane API port |\n| controlPlane.container.env | list | `[]` | Extra environment variables for the control plane container |\n| controlPlane.container.envFrom | list | `[]` | Extra envFrom sources (secretRef, configMapRef) for the control plane container |\n| controlPlane.container.resources | object | `{requests: {cpu: 100m, memory: 256Mi, ephemeral-storage: 500Mi}}` | Resource requests/limits for the control plane container |\n| controlPlane.container.startupProbe | object | `{}` | Startup probe configuration |\n| controlPlane.container.livenessProbe | object | `{}` | Liveness probe configuration |\n| controlPlane.deployment.annotations | object | `{}` | Annotations for the control plane Deployment |\n| controlPlane.deployment.podAnnotations | object | `{}` | Annotations for the control plane pod template |\n| controlPlane.service.type | string | `\"ClusterIP\"` | Control plane Service type |\n| controlPlane.service.port | int | `80` | Control plane Service port |\n| controlPlane.service.annotations | object | `{}` | Annotations for the control plane Service |\n| controlPlane.service.metricsPath | string | `\"/metrics\"` | Metrics path for the control plane Service |\n| controlPlane.buildApi.port | int | `8001` | Build API port (git proxy and token validation) |\n| controlPlane.buildApi.metricsPath | string | `\"/metrics\"` | Metrics path for the build API |\n| controlPlane.hpa.enabled | bool | `false` | Enable HPA for the control plane |\n| controlPlane.hpa.minReplicas | int | `1` | Minimum replicas |\n| controlPlane.hpa.maxReplicas | int | `3` | Maximum replicas |\n| controlPlane.hpa.targetCPUUtilizationPercentage | int | `80` | Target average CPU utilization percentage |\n\n### Object Storage\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| controlPlane.objectStorage.s3.endpointUrl | string | `\"\"` | S3 endpoint URL (leave empty for AWS) |\n| controlPlane.objectStorage.s3.bucket | string | `\"\"` | S3 bucket name (**required**) |\n| controlPlane.objectStorage.s3.region | string | `\"\"` | S3 region |\n| controlPlane.objectStorage.s3.unsigned | string | `nil` | Send S3 requests unsigned (no Authorization header). Leave unset/`false` for any auth-requiring S3-compatible backend. For non-S3 object/blob storage, see `s3proxy.enabled` below — when that's on, this defaults to `true` unless you override it here. |\n| controlPlane.objectStorage.s3.accessKey | string | `\"\"` | Inline S3 access key. When set alongside `secretKey`, the chart renders a Secret and wires it into the control plane. Mutually exclusive with `s3.secret` (which wins silently). |\n| controlPlane.objectStorage.s3.secretKey | string | `\"\"` | Inline S3 secret key. Must be set together with `accessKey`; partial setting is an error. |\n| controlPlane.objectStorage.s3.secret | string | `\"\"` | Name of an existing K8s Secret supplying `S3_ACCESS_KEY` and `S3_SECRET_KEY`. Takes precedence over `accessKey`/`secretKey`. |\n| controlPlane.objectStorage.buildKeyPrefix | string | `\"builds\"` | Key prefix for build artifacts in the bucket |\n| controlPlane.objectStorage.backupKeyPrefix | string | `\"backups\"` | Key prefix for backup archives in the bucket |\n| controlPlane.objectStorage.codeRepoKeyPrefix | string | `\"git\"` | Key prefix for code repositories in the bucket |\n| controlPlane.objectStorage.backupEncryptionSecretRef | string | `\"\"` | K8s Secret name containing `BACKUP_ENCRYPTION_PASSWORD` |\n\n### Apps\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| apps.namespace | string | `\"\"` | Namespace where LlamaDeployment CRs and all operator-managed child resources live. Empty = release namespace. When set, the operator + control plane stay in the release namespace and target this namespace for all app resources. |\n\n### CRDs\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| crds.version | string | `\"0.7.2\"` | Compatible `llama-agents-crds` chart version for this release. Documentation only; not read by templates. Auto-synced at release time. |\n\n### Operator\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| operator.enabled | bool | `true` | Deploy the operator |\n| operator.replicas | int | `1` | Number of operator replicas |\n| operator.annotations | object | `{}` | Annotations for the operator Deployment |\n| operator.podAnnotations | object | `{}` | Annotations for the operator pod template |\n| operator.defaultAppRequests.cpu | string | `\"750m\"` | Default CPU request for managed app containers |\n| operator.defaultAppRequests.memory | string | `\"2Gi\"` | Default memory request for managed app containers |\n| operator.defaultAppLimits.cpu | string | `\"\"` | Default CPU limit for managed app containers (empty = no limit) |\n| operator.defaultAppLimits.memory | string | `\"4096Mi\"` | Default memory limit for managed app containers |\n| operator.resources | object | `{limits: {cpu: 500m, memory: 128Mi}, requests: {cpu: 10m, memory: 64Mi}}` | Resource requests/limits for the operator container |\n| operator.maxConcurrentRollouts | int | `10` | Max simultaneous LlamaDeployment rollouts (0 = unlimited) |\n| operator.maxDeployments | int | `0` | Max active LlamaDeployments per namespace (0 = unlimited) |\n| operator.env | list | `[]` | Extra environment variables for the operator container |\n| operator.rolloutTimeoutSeconds | int | `1800` | Rollout timeout in seconds for managed deployments |\n| operator.llamaDeploymentTemplate.enabled | bool | `false` | Create a default LlamaDeploymentTemplate in the namespace |\n| operator.llamaDeploymentTemplate.name | string | `\"default\"` | Template resource name |\n| operator.llamaDeploymentTemplate.metadata | object | `{}` | Metadata for the template (labels, annotations) |\n| operator.llamaDeploymentTemplate.spec | object | `{\"podSpec\":{}}` | Template spec (podSpec with nodeSelector, tolerations, affinity, container overrides) |\n| operator.hpa.enabled | bool | `false` | Enable HPA for the operator |\n| operator.hpa.minReplicas | int | `1` | Minimum replicas |\n| operator.hpa.maxReplicas | int | `3` | Maximum replicas |\n| operator.hpa.targetCPUUtilizationPercentage | int | `80` | Target average CPU utilization percentage |\n\n### Local Development\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| localDev.enabled | bool | `false` | Enable local dev ingress for deployed apps |\n| localDev.ingressDomain | string | `\"127.0.0.1.nip.io\"` | Ingress domain for local dev |\n\n### RBAC\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| rbac.create | bool | `true` | Create Role and RoleBinding |\n| rbac.roleAnnotations | object | `{}` | Annotations for the Role |\n| rbac.roleBindingAnnotations | object | `{}` | Annotations for the RoleBinding |\n\n### Service Account\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| serviceAccount.create | bool | `true` | Create a ServiceAccount |\n| serviceAccount.name | string | `\"llama-agents\"` | ServiceAccount name |\n| serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount |\n\n### s3proxy\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| s3proxy.enabled | bool | `false` | Run an [s3proxy](https://github.com/gaul/s3proxy) sidecar alongside the control plane to translate S3 API calls to non-AWS backends (Azure Blob, GCS, etc.). When enabled, `S3_ENDPOINT_URL` and `S3_UNSIGNED` default to localhost and `true` unless explicitly overridden. Fill in `s3proxy.config` with the JCLOUDS_* environment variables for your cloud. |\n| s3proxy.image | string | `\"docker.io/andrewgaul/s3proxy:3.1.0\"` | s3proxy container image |\n| s3proxy.imagePullPolicy | string | `\"IfNotPresent\"` | s3proxy image pull policy |\n| s3proxy.containerPort | int | `8080` | Port s3proxy listens on inside the pod (control plane reaches it over localhost) |\n| s3proxy.logLevel | string | `\"info\"` | s3proxy log level (passed as LOG_LEVEL and S3PROXY_LOG_LEVEL) |\n| s3proxy.securityContext | object | `{}` | securityContext for the s3proxy container |\n| s3proxy.resources | object | `{requests: {cpu: 50m, memory: 256Mi}, limits: {cpu: 500m, memory: 512Mi}}` | Resource requests/limits for the s3proxy sidecar |\n| s3proxy.config | object | `{}` | Raw passthrough to the s3proxy Secret. Keys become environment variables on the sidecar. Typically `JCLOUDS_PROVIDER`, `JCLOUDS_IDENTITY`, `JCLOUDS_CREDENTIAL`, `JCLOUDS_ENDPOINT`, `JCLOUDS_REGION`. See https://github.com/gaul/s3proxy/wiki/Storage-backend-examples. |\n| s3proxy.secret | string | `\"\"` | Name of an existing K8s Secret supplying the sidecar's env vars. Takes precedence over `config` (which is skipped if this is set). |\n\n### Network Policy\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| networkPolicy.enabled | bool | `true` | Enable egress NetworkPolicy for operator-managed pods |\n| networkPolicy.extraMatchExpressions | list | `[]` | Additional pod selector matchExpressions |\n| networkPolicy.extraEgressRules | list | `[]` | Extra egress rules appended to the NetworkPolicy |\n| networkPolicy.blockPrivateRanges | bool | `true` | Block private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) in internet egress rule |\n| networkPolicy.dns.namespaceSelector | object | `{\"kubernetes.io/metadata.name\":\"kube-system\"}` | Namespace selector for DNS pods. Defaults to kube-system |\n| networkPolicy.dns.podSelector | object | `{\"k8s-app\":\"kube-dns\"}` | Pod selector for DNS pods. Defaults to kube-dns |\n\n## Uninstalling\n\n```bash\nhelm uninstall llama-agents\n```\n\nCRDs are **not** removed on uninstall. To remove them (this deletes all LlamaDeployment resources):\n\n```bash\nkubectl delete crd llamadeployments.deploy.llamaindex.ai llamadeploymenttemplates.deploy.llamaindex.ai\n```\n"
  },
  {
    "path": "charts/llama-agents/README.md.gotmpl",
    "content": "{{ template \"chart.header\" . }}\n\n{{ template \"chart.description\" . }}\n\n## Architecture\n\nThis chart deploys two components:\n\n- **Control plane** — API server for managing deployments, builds, and backups\n- **Operator** — Kubernetes controller that reconciles `LlamaDeployment` custom resources into running pods\n\nCRDs (`LlamaDeployment`, `LlamaDeploymentTemplate`) are included in the chart's `crds/` directory and installed automatically on first `helm install`. They are **not** modified on upgrade or removed on uninstall (standard Helm CRD behavior).\n\nFor managed CRD upgrades, use the companion [`llama-agents-crds`](../llama-agents-crds/) chart. Each release of `llama-agents` pins the compatible CRD chart version in `crds.version` (see the values table below) — use that version when installing or upgrading the CRD chart.\n\n## Prerequisites\n\n- Kubernetes 1.26+\n- Helm 3.x\n- S3-compatible object storage (for build artifacts and backups)\n\n## Installation\n\n### Fresh install\n\n```bash\nhelm install llama-agents oci://docker.io/llamaindex/llama-agents \\\n  --set controlPlane.objectStorage.s3.bucket=my-bucket \\\n  --set controlPlane.objectStorage.s3.region=us-east-1\n```\n\nCRDs are installed automatically from the `crds/` directory.\n\n### With separate CRD management\n\nIf you prefer explicit CRD lifecycle management (recommended for production):\n\n```bash\n# Install CRD chart first — pin to the compatible version from `crds.version` below\nhelm install llama-agents-crds oci://docker.io/llamaindex/llama-agents-crds --version <crds.version>\n\n# Install main chart, skipping bundled CRDs\nhelm install llama-agents oci://docker.io/llamaindex/llama-agents --skip-crds \\\n  --set controlPlane.objectStorage.s3.bucket=my-bucket\n```\n\n## Upgrading\n\n```bash\n# If CRD schema has changed, upgrade CRDs first — pin to `crds.version` from the values table\nhelm upgrade --install llama-agents-crds oci://docker.io/llamaindex/llama-agents-crds --version <crds.version>\n\n# Then upgrade the main chart\nhelm upgrade llama-agents oci://docker.io/llamaindex/llama-agents\n```\n\n## Apps namespace\n\nSet `apps.namespace` to isolate `LlamaDeployment` CRs and their child resources\nin a separate namespace. The operator + control plane stay in the release\nnamespace and target the apps namespace for all app resources.\n\n```bash\nkubectl create namespace llama-agents-apps\nhelm install llama-agents oci://docker.io/llamaindex/llama-agents \\\n  --namespace llama-agents \\\n  --set apps.namespace=llama-agents-apps \\\n  --set controlPlane.objectStorage.s3.bucket=my-bucket\n```\n\n`imagePullSecrets` are not mirrored — provision them in the apps namespace\nyourself, or use node-level pull credentials. Switching modes on an existing\ninstall requires draining and recreating `LlamaDeployment` CRs.\n\n## Non-S3 object storage\n\nSet `s3proxy.enabled=true` to run an\n[s3proxy](https://github.com/gaul/s3proxy) sidecar alongside the control\nplane. When enabled, `S3_ENDPOINT_URL` points at the sidecar on localhost and\n`S3_UNSIGNED` defaults to `true`; explicit overrides still win.\n\nCredentials take one of two forms:\n\n```yaml\n# Inline — chart renders llama-agents-s3proxy Secret\ns3proxy:\n  enabled: true\n  config:\n    JCLOUDS_PROVIDER: <provider>\n    JCLOUDS_IDENTITY: <id>\n    JCLOUDS_CREDENTIAL: <secret>\n    # ...any other JCLOUDS_* vars the backend needs\n```\n\n```yaml\n# BYO — point at an existing Secret whose keys are the sidecar env vars\ns3proxy:\n  enabled: true\n  secret: my-existing-s3proxy-secret\n```\n\nPick `JCLOUDS_*` vars from the\n[s3proxy storage-backend examples](https://github.com/gaul/s3proxy/wiki/Storage-backend-examples).\nIf both `config` and `secret` are set, `secret` wins.\n\n## Control plane S3 credentials\n\nThree mutually-exclusive forms, listed in precedence order:\n\n```yaml\n# BYO — envFroms an existing Secret (keys: S3_ACCESS_KEY, S3_SECRET_KEY)\ncontrolPlane:\n  objectStorage:\n    s3:\n      bucket: my-bucket\n      secret: my-s3-creds\n```\n\n```yaml\n# Inline — chart renders llama-agents-controlplane-s3 Secret\ncontrolPlane:\n  objectStorage:\n    s3:\n      bucket: my-bucket\n      accessKey: AKIA...\n      secretKey: ...\n```\n\n```yaml\n# Neither — control plane relies on IRSA / workload identity\ncontrolPlane:\n  objectStorage:\n    s3:\n      bucket: my-bucket\n```\n\nPartial inline (one of `accessKey`/`secretKey` set) is a template error.\n\n{{ template \"chart.valuesSection\" . }}\n\n## Uninstalling\n\n```bash\nhelm uninstall llama-agents\n```\n\nCRDs are **not** removed on uninstall. To remove them (this deletes all LlamaDeployment resources):\n\n```bash\nkubectl delete crd llamadeployments.deploy.llamaindex.ai llamadeploymenttemplates.deploy.llamaindex.ai\n```\n"
  },
  {
    "path": "charts/llama-agents/crds/deploy.llamaindex.ai_llamadeployments.yaml",
    "content": "---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  annotations:\n    controller-gen.kubebuilder.io/version: v0.18.0\n  name: llamadeployments.deploy.llamaindex.ai\nspec:\n  group: deploy.llamaindex.ai\n  names:\n    kind: LlamaDeployment\n    listKind: LlamaDeploymentList\n    plural: llamadeployments\n    singular: llamadeployment\n  scope: Namespaced\n  versions:\n  - additionalPrinterColumns:\n    - jsonPath: .spec.projectId\n      name: Project ID\n      type: string\n    - jsonPath: .spec.displayName\n      name: Name\n      type: string\n    - jsonPath: .spec.repoUrl\n      name: Repo\n      type: string\n    - jsonPath: .status.phase\n      name: Phase\n      type: string\n    - jsonPath: .metadata.creationTimestamp\n      name: Age\n      type: date\n    name: v1\n    schema:\n      openAPIV3Schema:\n        description: LlamaDeployment is the Schema for the llamadeployments API.\n        properties:\n          apiVersion:\n            description: |-\n              APIVersion defines the versioned schema of this representation of an object.\n              Servers should convert recognized schemas to the latest internal value, and\n              may reject unrecognized values.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\n            type: string\n          kind:\n            description: |-\n              Kind is a string value representing the REST resource this object represents.\n              Servers may infer this from the endpoint the client submits requests to.\n              Cannot be updated.\n              In CamelCase.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\n            type: string\n          metadata:\n            type: object\n          spec:\n            description: LlamaDeploymentSpec defines the desired state of LlamaDeployment.\n            properties:\n              buildGeneration:\n                description: |-\n                  BuildGeneration is a monotonically increasing counter that forces a new\n                  build when incremented, even if all other inputs (gitSha, imageTag, etc.)\n                  are unchanged. This allows retrying a failed build caused by transient\n                  errors (e.g. network failures) without requiring a new git commit.\n                format: int64\n                type: integer\n              deploymentFilePath:\n                default: llama_deployment.yml\n                description: DeploymentFilePath is the path to the deployment file\n                  within the repository\n                type: string\n              displayName:\n                description: DisplayName is the user-facing deployment label\n                type: string\n              gitRef:\n                description: GitRef is the git reference (commit SHA, branch, or tag)\n                  to deploy\n                type: string\n              gitSha:\n                description: A resolved git sha for the git ref\n                type: string\n              image:\n                description: |-\n                  Image is the container image registry and name (e.g., \"llamaindex/llama-agents-appserver\")\n                  If not specified, defaults to environment variable or \"llamaindex/llama-agents-appserver\"\n                type: string\n              imageTag:\n                description: |-\n                  ImageTag is the container image tag\n                  If not specified, defaults to environment variable or \"latest\"\n                type: string\n              name:\n                description: 'Name is the deployment name (DEPRECATED: use DisplayName)'\n                type: string\n              projectId:\n                description: ProjectId is the project ID\n                type: string\n              repoUrl:\n                description: RepoUrl is the URL of the repository to deploy\n                type: string\n              secretName:\n                description: SecretName is the name of the Kubernetes Secret containing\n                  PAT and deployment secrets\n                type: string\n              staticAssetsPath:\n                description: |-\n                  StaticAssetsPath is an optional path (relative to /opt/app) containing\n                  prebuilt UI assets to be served under /deployments/<deployment-id>/ui\n                type: string\n              suspended:\n                description: |-\n                  Suspended scales the underlying Deployment to 0 replicas when true.\n                  Setting suspended to false (or removing the field) restores replicas to 1.\n                type: boolean\n              templateName:\n                description: |-\n                  TemplateName optionally specifies a LlamaDeploymentTemplate to apply.\n                  When empty, the operator will look up a template named \"default\".\n                type: string\n            required:\n            - projectId\n            - repoUrl\n            type: object\n          status:\n            description: LlamaDeploymentStatus defines the observed state of LlamaDeployment.\n            properties:\n              authToken:\n                description: AuthToken is a cryptographically secure token for this\n                  deployment\n                type: string\n              buildId:\n                description: BuildId is the content-addressed identifier for the current\n                  build artifact\n                type: string\n              buildStatus:\n                description: BuildStatus tracks the state of the current build job\n                enum:\n                - Pending\n                - Running\n                - Succeeded\n                - Failed\n                type: string\n              failedRolloutGeneration:\n                description: |-\n                  FailedRolloutGeneration records the LlamaDeployment generation whose rollout\n                  timed out. This prevents the operator from re-attempting the same failing rollout.\n                format: int64\n                type: integer\n              lastBuiltGeneration:\n                description: |-\n                  LastBuiltGeneration is the spec.buildGeneration value that was last\n                  successfully built. When spec.buildGeneration differs from this value,\n                  a new build is triggered even if the deployment is suspended.\n                format: int64\n                type: integer\n              lastReconciledGeneration:\n                description: LastReconciledGeneration tracks the generation that was\n                  last successfully reconciled\n                format: int64\n                type: integer\n              lastUpdated:\n                description: LastUpdated is the timestamp of the last status update\n                format: date-time\n                type: string\n              message:\n                description: Message is a human-readable message indicating details\n                  about the current status\n                type: string\n              phase:\n                description: Phase represents the current phase of the deployment\n                enum:\n                - Pending\n                - Running\n                - Failed\n                - RollingOut\n                - RolloutFailed\n                - Suspended\n                - Building\n                - BuildFailed\n                - AwaitingCode\n                type: string\n              releaseHistory:\n                description: ReleaseHistory keeps the last 20 released git shas with\n                  timestamps\n                items:\n                  description: ReleaseHistoryEntry represents a single released version\n                    entry\n                  properties:\n                    gitSha:\n                      description: GitSha is the released git commit SHA\n                      type: string\n                    imageTag:\n                      description: ImageTag is the appserver image tag used for this\n                        release\n                      type: string\n                    releasedAt:\n                      description: ReleasedAt is the timestamp when this version was\n                        released\n                      format: date-time\n                      type: string\n                  required:\n                  - gitSha\n                  - releasedAt\n                  type: object\n                type: array\n              rolloutStartedAt:\n                description: |-\n                  RolloutStartedAt is the timestamp when the current rollout began.\n                  Set when the phase transitions to Pending or RollingOut, cleared on Running or failure.\n                format: date-time\n                type: string\n              schemaVersion:\n                description: SchemaVersion is the version of the CRD schema used when\n                  this resource was last reconciled\n                type: string\n              secretCheckRetries:\n                description: |-\n                  SecretCheckRetries tracks how many times we've retried finding the Secret.\n                  This handles informer cache lag when the Secret is created just before the CR.\n                format: int32\n                type: integer\n            type: object\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n"
  },
  {
    "path": "charts/llama-agents/crds/deploy.llamaindex.ai_llamadeploymenttemplates.yaml",
    "content": "---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  annotations:\n    controller-gen.kubebuilder.io/version: v0.18.0\n  name: llamadeploymenttemplates.deploy.llamaindex.ai\nspec:\n  group: deploy.llamaindex.ai\n  names:\n    kind: LlamaDeploymentTemplate\n    listKind: LlamaDeploymentTemplateList\n    plural: llamadeploymenttemplates\n    singular: llamadeploymenttemplate\n  scope: Namespaced\n  versions:\n  - additionalPrinterColumns:\n    - jsonPath: .metadata.creationTimestamp\n      name: Age\n      type: date\n    name: v1\n    schema:\n      openAPIV3Schema:\n        description: |-\n          LlamaDeploymentTemplate configures default Pod template fields for LlamaDeployments.\n          The resource name is referenced by LlamaDeployment.spec.templateName. A special name\n          \"default\" is used as a fallback when no templateName is provided.\n        properties:\n          apiVersion:\n            description: |-\n              APIVersion defines the versioned schema of this representation of an object.\n              Servers should convert recognized schemas to the latest internal value, and\n              may reject unrecognized values.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\n            type: string\n          kind:\n            description: |-\n              Kind is a string value representing the REST resource this object represents.\n              Servers may infer this from the endpoint the client submits requests to.\n              Cannot be updated.\n              In CamelCase.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\n            type: string\n          metadata:\n            type: object\n          spec:\n            description: |-\n              LlamaDeploymentTemplateSpec defines the desired overlay for a LlamaDeployment's PodTemplate.\n              This is intended to carry scheduling-related fields like node selectors, tolerations, and\n              affinity, but supports any partial PodTemplateSpec. Fields set here will take precedence\n              over the operator-computed defaults when merged.\n            properties:\n              podSpec:\n                description: PodSpec holds a partial PodTemplateSpec to be merged\n                  into the generated PodTemplate.\n                x-kubernetes-preserve-unknown-fields: true\n            type: object\n          status:\n            type: string\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n"
  },
  {
    "path": "charts/llama-agents/package.json",
    "content": "{\n  \"name\": \"llama-agents\",\n  \"version\": \"0.12.3\",\n  \"dependencies\": {\n    \"llama-agents-appserver\": \"workspace:*\",\n    \"llama-agents-control-plane\": \"workspace:*\",\n    \"llama-agents-operator\": \"workspace:*\"\n  },\n  \"helm\": {\n    \"registry\": \"oci://docker.io/llamaindex\"\n  },\n  \"syncValues\": {\n    \"values.yaml\": {\n      \"images.controlPlane.tag\": \"{llama-agents-control-plane:dockerTag}\",\n      \"images.operator.tag\": \"{llama-agents-operator:dockerTag}\",\n      \"images.appserver.tag\": \"{llama-agents-appserver:dockerTag}\",\n      \"crds.version\": \"{llama-agents-crds:version}\"\n    }\n  },\n  \"postVersion\": [\n    \"helm-docs --chart-search-root . --sort-values-order file\"\n  ]\n}\n"
  },
  {
    "path": "charts/llama-agents/templates/_helpers.tpl",
    "content": "{{/*\nCommon name helpers for the llama-agents chart.\nAll resource names derive from these so a single rename propagates everywhere.\n*/}}\n\n{{/* Chart name */}}\n{{- define \"llama-agents.name\" -}}\nllama-agents\n{{- end -}}\n\n{{/* Control plane deployment and related resources */}}\n{{- define \"llama-agents.controlplane.name\" -}}\nllama-agents-control-plane\n{{- end -}}\n\n{{/* Operator deployment and related resources */}}\n{{- define \"llama-agents.operator.name\" -}}\nllama-agents-operator\n{{- end -}}\n\n{{/* Main API service */}}\n{{- define \"llama-agents.service.name\" -}}\nllama-agents-service\n{{- end -}}\n\n{{/* Build API service */}}\n{{- define \"llama-agents.build.name\" -}}\nllama-agents-build\n{{- end -}}\n\n{{/* s3proxy ConfigMap/Secret name (shared by both resources) */}}\n{{- define \"llama-agents.s3proxy.name\" -}}\nllama-agents-s3proxy\n{{- end -}}\n\n{{/* Chart-rendered Secret holding inline control plane S3 creds */}}\n{{- define \"llama-agents.controlplane.s3secret.name\" -}}\nllama-agents-controlplane-s3\n{{- end -}}\n\n{{/* Service account name — use value from values.yaml if set, otherwise chart name */}}\n{{- define \"llama-agents.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.name -}}\n{{ .Values.serviceAccount.name }}\n{{- else -}}\n{{ include \"llama-agents.name\" . }}\n{{- end -}}\n{{- end -}}\n\n{{/* Namespace for LlamaDeployment CRs and child resources. Defaults to release namespace. */}}\n{{- define \"llama-agents.apps.namespace\" -}}\n{{- if and .Values.apps .Values.apps.namespace -}}\n{{ .Values.apps.namespace }}\n{{- else -}}\n{{ .Release.Namespace }}\n{{- end -}}\n{{- end -}}\n\n{{/* True when apps namespace differs from release namespace. */}}\n{{- define \"llama-agents.apps.splitNamespace\" -}}\n{{- if ne (include \"llama-agents.apps.namespace\" .) .Release.Namespace -}}\ntrue\n{{- end -}}\n{{- end -}}\n"
  },
  {
    "path": "charts/llama-agents/templates/_s3proxy.tpl",
    "content": "{{/*\ns3proxy sidecar container definition.\nRendered into the control plane pod when `.Values.s3proxy.enabled` is true.\n*/}}\n{{- define \"llama-agents.s3proxy.container\" -}}\n- name: s3proxy\n  image: {{ .Values.s3proxy.image | quote }}\n  imagePullPolicy: {{ .Values.s3proxy.imagePullPolicy | default \"IfNotPresent\" }}\n  {{- with .Values.s3proxy.securityContext }}\n  securityContext:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n  ports:\n  - name: s3proxy\n    containerPort: {{ int .Values.s3proxy.containerPort }}\n    protocol: TCP\n  env:\n  - name: LOG_LEVEL\n    value: {{ .Values.s3proxy.logLevel | default \"info\" | quote }}\n  - name: S3PROXY_LOG_LEVEL\n    value: {{ .Values.s3proxy.logLevel | default \"info\" | quote }}\n  envFrom:\n  - configMapRef:\n      name: {{ include \"llama-agents.s3proxy.name\" . }}\n  {{- include \"llama-agents.secrets.s3proxy\" . | nindent 2 }}\n  {{- with .Values.s3proxy.resources }}\n  resources:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n  volumeMounts:\n  - name: s3proxy-tmp\n    mountPath: /tmp\n    subPath: tmp-dir\n{{- end -}}\n\n{{/*\ns3proxy ConfigMap data (non-secret config).\n*/}}\n{{- define \"llama-agents.s3proxy.configMapData\" -}}\nS3PROXY_AUTHORIZATION: \"none\"\nS3PROXY_CORS_ALLOW_ORIGINS: \"*\"\nS3PROXY_ENDPOINT: {{ printf \"http://0.0.0.0:%d\" (int .Values.s3proxy.containerPort) | quote }}\nS3PROXY_IGNORE_UNKNOWN_HEADERS: \"true\"\n{{- end -}}\n\n{{/*\ns3proxy Secret data (passthrough of .Values.s3proxy.config, b64-encoded).\n*/}}\n{{- define \"llama-agents.s3proxy.secretData\" -}}\n{{- range $key, $value := .Values.s3proxy.config }}\n{{ $key }}: {{ $value | toString | b64enc | quote }}\n{{- end }}\n{{- end -}}\n\n{{/*\nEndpoint URL the control plane should use to reach the sidecar on localhost.\n*/}}\n{{- define \"llama-agents.s3proxy.localEndpoint\" -}}\n{{- printf \"http://localhost:%d\" (int .Values.s3proxy.containerPort) -}}\n{{- end -}}\n"
  },
  {
    "path": "charts/llama-agents/templates/_secrets.tpl",
    "content": "{{/*\nenvFrom helpers. Each emits zero-or-one `- secretRef:` entries with silent\nBYO-over-inline precedence. Model: llamacloud's `_secrets.tpl`.\n*/}}\n\n{{/*\ns3proxy sidecar: user-supplied `.secret` wins over chart-rendered Secret.\nEmits nothing when neither is set (sidecar boots without creds).\n*/}}\n{{- define \"llama-agents.secrets.s3proxy\" -}}\n{{- if .Values.s3proxy.secret }}\n- secretRef:\n    name: {{ .Values.s3proxy.secret }}\n{{- else if .Values.s3proxy.config }}\n- secretRef:\n    name: {{ include \"llama-agents.s3proxy.name\" . }}\n{{- end }}\n{{- end -}}\n\n{{/*\nControl plane S3 creds. Precedence: s3.secret > outer secretRef (legacy alias)\n> chart-rendered-from-inline. Emits nothing when all three are unset.\n*/}}\n{{- define \"llama-agents.secrets.controlplaneS3\" -}}\n{{- $os := .Values.controlPlane.objectStorage }}\n{{- if $os.s3.secret }}\n- secretRef:\n    name: {{ $os.s3.secret }}\n{{- else if $os.secretRef }}\n- secretRef:\n    name: {{ $os.secretRef }}\n{{- else if and $os.s3.accessKey $os.s3.secretKey }}\n- secretRef:\n    name: {{ include \"llama-agents.controlplane.s3secret.name\" . }}\n{{- end }}\n{{- end -}}\n"
  },
  {
    "path": "charts/llama-agents/templates/controlplane-hpa.yaml",
    "content": "{{- if .Values.controlPlane.hpa.enabled }}\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: {{ include \"llama-agents.controlplane.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: {{ include \"llama-agents.controlplane.name\" . }}\n  minReplicas: {{ .Values.controlPlane.hpa.minReplicas }}\n  maxReplicas: {{ .Values.controlPlane.hpa.maxReplicas }}\n  metrics:\n  {{- $hasMetric := false }}\n  {{- if .Values.controlPlane.hpa.targetCPUUtilizationPercentage }}\n  - type: Resource\n    resource:\n      name: cpu\n      target:\n        type: Utilization\n        averageUtilization: {{ .Values.controlPlane.hpa.targetCPUUtilizationPercentage }}\n  {{- $hasMetric = true }}\n  {{- end }}\n  {{- if .Values.controlPlane.hpa.targetMemoryUtilizationPercentage }}\n  - type: Resource\n    resource:\n      name: memory\n      target:\n        type: Utilization\n        averageUtilization: {{ .Values.controlPlane.hpa.targetMemoryUtilizationPercentage }}\n  {{- $hasMetric = true }}\n  {{- end }}\n  {{- if not $hasMetric }}\n  - type: Resource\n    resource:\n      name: cpu\n      target:\n        type: Utilization\n        averageUtilization: 80\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/controlplane-s3-secret.yaml",
    "content": "{{- $os := .Values.controlPlane.objectStorage }}\n{{- $s3 := $os.s3 }}\n{{- if or $s3.accessKey $s3.secretKey }}\n{{- if not (and $s3.accessKey $s3.secretKey) }}\n{{- fail \"controlPlane.objectStorage.s3.accessKey and controlPlane.objectStorage.s3.secretKey must both be set\" }}\n{{- end }}\n{{- if or $s3.secret $os.secretRef }}\n{{- /* BYO wins silently: skip chart Secret. */ -}}\n{{- else }}\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"llama-agents.controlplane.s3secret.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\ntype: Opaque\ndata:\n  S3_ACCESS_KEY: {{ $s3.accessKey | b64enc | quote }}\n  S3_SECRET_KEY: {{ $s3.secretKey | b64enc | quote }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"llama-agents.controlplane.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n  {{- with .Values.controlPlane.deployment.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  replicas: {{ .Values.controlPlane.replicas }}\n  strategy:\n    type: Recreate\n  selector:\n    matchLabels:\n      app: {{ include \"llama-agents.controlplane.name\" . }}\n  template:\n    metadata:\n      labels:\n        app: {{ include \"llama-agents.controlplane.name\" . }}\n      {{- with .Values.controlPlane.deployment.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n    spec:\n      {{- if .Values.serviceAccount.create }}\n      serviceAccountName: {{ include \"llama-agents.serviceAccountName\" . }}\n      {{- end }}\n      securityContext:\n        runAsNonRoot: true\n        runAsUser: 1001\n        runAsGroup: 1001\n        fsGroup: 1001\n      containers:\n      - name: app\n        image: {{ .Values.images.controlPlane.repository }}:{{ .Values.images.controlPlane.tag }}\n        imagePullPolicy: {{ .Values.images.controlPlane.pullPolicy }}\n        securityContext:\n          allowPrivilegeEscalation: false\n          capabilities:\n            drop:\n            - \"ALL\"\n        ports:\n        - containerPort: {{ .Values.controlPlane.container.port }}\n          name: http\n        - containerPort: {{ .Values.controlPlane.buildApi.port }}\n          name: build-api\n        env:\n        - name: KUBERNETES_NAMESPACE\n          value: {{ include \"llama-agents.apps.namespace\" . | quote }}\n        - name: DEFAULT_APPSERVER_IMAGE_TAG\n          value: {{ .Values.controlPlane.defaultAppserverImageTag | default .Values.images.appserver.tag | quote }}\n        - name: S3_ENDPOINT_URL\n          {{- if .Values.controlPlane.objectStorage.s3.endpointUrl }}\n          value: {{ .Values.controlPlane.objectStorage.s3.endpointUrl | quote }}\n          {{- else if .Values.s3proxy.enabled }}\n          value: {{ include \"llama-agents.s3proxy.localEndpoint\" . | quote }}\n          {{- else }}\n          value: \"\"\n          {{- end }}\n        - name: S3_BUCKET\n          value: {{ required \"controlPlane.objectStorage.s3.bucket is required\" .Values.controlPlane.objectStorage.s3.bucket | quote }}\n        - name: S3_REGION\n          value: {{ .Values.controlPlane.objectStorage.s3.region | quote }}\n        - name: S3_UNSIGNED\n          {{- if kindIs \"bool\" .Values.controlPlane.objectStorage.s3.unsigned }}\n          value: {{ .Values.controlPlane.objectStorage.s3.unsigned | quote }}\n          {{- else if .Values.s3proxy.enabled }}\n          value: \"true\"\n          {{- else }}\n          value: \"false\"\n          {{- end }}\n        - name: BUILD_S3_KEY_PREFIX\n          value: {{ .Values.controlPlane.objectStorage.buildKeyPrefix | quote }}\n        - name: BACKUP_S3_KEY_PREFIX\n          value: {{ .Values.controlPlane.objectStorage.backupKeyPrefix | quote }}\n        - name: CODE_REPO_S3_KEY_PREFIX\n          value: {{ .Values.controlPlane.objectStorage.codeRepoKeyPrefix | quote }}\n        {{- if .Values.controlPlane.container.env }}\n          {{- toYaml .Values.controlPlane.container.env | nindent 8 }}\n        {{- end }}\n        {{- $os := .Values.controlPlane.objectStorage }}\n        {{- $s3Secret := include \"llama-agents.secrets.controlplaneS3\" . | trim }}\n        {{- if or .Values.controlPlane.container.envFrom $s3Secret $os.backupEncryptionSecretRef }}\n        envFrom:\n        {{- if .Values.controlPlane.container.envFrom }}\n        {{- toYaml .Values.controlPlane.container.envFrom | nindent 8 }}\n        {{- end }}\n        {{- if $s3Secret }}\n        {{- include \"llama-agents.secrets.controlplaneS3\" . | nindent 8 }}\n        {{- end }}\n        {{- if $os.backupEncryptionSecretRef }}\n        - secretRef:\n            name: {{ $os.backupEncryptionSecretRef }}\n        {{- end }}\n        {{- end }}\n\n        {{- if .Values.controlPlane.container.startupProbe }}\n        startupProbe:\n          {{- toYaml .Values.controlPlane.container.startupProbe | nindent 10 }}\n        {{- end }}\n        {{- if .Values.controlPlane.container.livenessProbe }}\n        livenessProbe:\n          {{- toYaml .Values.controlPlane.container.livenessProbe | nindent 10 }}\n        {{- end }}\n        {{- if .Values.controlPlane.container.resources }}\n        resources:\n          {{- toYaml .Values.controlPlane.container.resources | nindent 10 }}\n        {{- end }}\n      {{- if .Values.s3proxy.enabled }}\n      {{- include \"llama-agents.s3proxy.container\" . | nindent 6 }}\n      volumes:\n      - name: s3proxy-tmp\n        emptyDir: {}\n      {{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/networkpolicy.yaml",
    "content": "{{- if .Values.networkPolicy.enabled }}\n# Egress restrictions for app pods. Lives in the apps namespace (NetworkPolicy\n# can only select pods in its own namespace).\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n  name: llama-deployment-pods\n  namespace: {{ include \"llama-agents.apps.namespace\" . }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n    component: deployment-pods\nspec:\n  podSelector:\n    matchLabels:\n      app.kubernetes.io/managed-by: llama-deploy-operator\n    {{- with .Values.networkPolicy.extraMatchExpressions }}\n    matchExpressions:\n    {{- toYaml . | nindent 4 }}\n    {{- end }}\n  policyTypes:\n  - Egress\n  egress:\n  # Control plane build API (in the release namespace).\n  - to:\n    - namespaceSelector:\n        matchLabels:\n          kubernetes.io/metadata.name: {{ .Release.Namespace }}\n      podSelector:\n        matchLabels:\n          app: {{ include \"llama-agents.controlplane.name\" . }}\n    ports:\n    - protocol: TCP\n      port: {{ .Values.controlPlane.buildApi.port }}\n  # Allow DNS resolution\n  - to:\n    - namespaceSelector:\n        matchLabels:\n          {{- toYaml .Values.networkPolicy.dns.namespaceSelector | nindent 10 }}\n      podSelector:\n        matchLabels:\n          {{- toYaml .Values.networkPolicy.dns.podSelector | nindent 10 }}\n    ports:\n    - protocol: UDP\n      port: 53\n    - protocol: TCP\n      port: 53\n  # Allow Internet access\n  - to:\n    - ipBlock:\n        cidr: 0.0.0.0/0\n        {{- if .Values.networkPolicy.blockPrivateRanges }}\n        except:\n        - 169.254.169.254/32  # AWS IMDS\n        - 10.0.0.0/8          # Private ranges\n        - 172.16.0.0/12\n        - 192.168.0.0/16\n        - 100.64.0.0/10       # Carrier-grade NAT\n        {{- end }}\n  {{- with .Values.networkPolicy.extraEgressRules }}\n  {{- toYaml . | nindent 2 }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/operator-deployment-template.yaml",
    "content": "{{- if and .Values.operator.enabled .Values.operator.llamaDeploymentTemplate.enabled }}\napiVersion: deploy.llamaindex.ai/v1\nkind: LlamaDeploymentTemplate\nmetadata:\n  {{- with .Values.operator.llamaDeploymentTemplate.metadata }}\n  {{- toYaml . | nindent 2 }}\n  {{- end }}\n  name: {{ .Values.operator.llamaDeploymentTemplate.name | default \"default\" }}\n  namespace: {{ include \"llama-agents.apps.namespace\" . }}\nspec:\n  {{- with .Values.operator.llamaDeploymentTemplate.spec }}\n  {{- toYaml . | nindent 2 }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/operator-deployment.yaml",
    "content": "{{- if .Values.operator.enabled }}\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"llama-agents.operator.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.operator.name\" . }}\n    component: operator\n  {{- with .Values.operator.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  replicas: {{ .Values.operator.replicas }}\n  selector:\n    matchLabels:\n      app: {{ include \"llama-agents.operator.name\" . }}\n  template:\n    metadata:\n      labels:\n        app: {{ include \"llama-agents.operator.name\" . }}\n        component: operator\n      {{- with .Values.operator.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n    spec:\n      {{- if .Values.serviceAccount.create }}\n      serviceAccountName: {{ include \"llama-agents.serviceAccountName\" . }}\n      {{- end }}\n      securityContext:\n        runAsNonRoot: true\n        runAsUser: 65532\n        runAsGroup: 65532\n        fsGroup: 65532\n      containers:\n      - name: manager\n        image: {{ .Values.images.operator.repository }}:{{ .Values.images.operator.tag }}\n        imagePullPolicy: {{ .Values.images.operator.pullPolicy }}\n        command:\n        - /manager\n        args:\n        - --leader-elect\n        {{- if .Values.metrics.enabled }}\n        - --metrics-bind-address=:8080\n        - --metrics-secure=false\n        {{- else }}\n        - --metrics-bind-address=0\n        {{- end }}\n        env:\n        - name: WATCH_NAMESPACE\n          value: {{ include \"llama-agents.apps.namespace\" . | quote }}\n        - name: LLAMA_DEPLOY_SYSTEM_NAMESPACE\n          valueFrom:\n            fieldRef:\n              fieldPath: metadata.namespace\n        # Configure the default appserver image that operator will use for LlamaDeployments\n        - name: LLAMA_DEPLOY_IMAGE\n          value: {{ .Values.images.appserver.repository }}\n        - name: LLAMA_DEPLOY_IMAGE_TAG\n          value: {{ .Values.images.appserver.tag | quote }}\n        - name: LLAMA_DEPLOY_IMAGE_PULL_POLICY\n          value: {{ .Values.images.appserver.pullPolicy }}\n        # Configure nginx sidecar image used in per-deployment pods\n        - name: LLAMA_DEPLOY_NGINX_IMAGE\n          value: {{ .Values.images.nginx.repository }}\n        - name: LLAMA_DEPLOY_NGINX_IMAGE_TAG\n          value: {{ .Values.images.nginx.tag }}\n        - name: LLAMA_DEPLOY_NGINX_IMAGE_PULL_POLICY\n          value: {{ .Values.images.nginx.pullPolicy }}\n        # Default resource request overrides for app containers managed by operator\n        - name: LLAMA_DEPLOY_DEFAULT_CPU_REQUEST\n          value: {{ .Values.operator.defaultAppRequests.cpu | default \"750m\" | quote }}\n        - name: LLAMA_DEPLOY_DEFAULT_MEMORY_REQUEST\n          value: {{ .Values.operator.defaultAppRequests.memory | default \"2Gi\" | quote }}\n        # Default resource limit overrides for app containers managed by operator\n        - name: LLAMA_DEPLOY_DEFAULT_CPU_LIMIT\n          value: {{ .Values.operator.defaultAppLimits.cpu | default \"\" | quote }}\n        - name: LLAMA_DEPLOY_DEFAULT_MEMORY_LIMIT\n          value: {{ .Values.operator.defaultAppLimits.memory | default \"4096Mi\" | quote }}\n        # Rollout timeout for managed deployments\n        - name: LLAMA_DEPLOY_ROLLOUT_TIMEOUT_SECONDS\n          value: {{ .Values.operator.rolloutTimeoutSeconds | default \"1800\" | quote }}\n        # Max concurrent rollouts (0 = unlimited, default: 10)\n        - name: LLAMA_DEPLOY_MAX_CONCURRENT_ROLLOUTS\n          value: {{ .Values.operator.maxConcurrentRollouts | default \"10\" | quote }}\n        # Max total active deployments per namespace (0 = unlimited)\n        - name: LLAMA_DEPLOY_MAX_DEPLOYMENTS\n          value: {{ .Values.operator.maxDeployments | default \"0\" | quote }}\n        # Build API service address for git proxy\n        - name: LLAMA_DEPLOY_BUILD_API_HOST\n          value: \"{{ include \"llama-agents.build.name\" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.controlPlane.buildApi.port | default 8001 }}\"\n        {{- if .Values.operator.env }}\n        # Additional custom environment variables\n          {{- toYaml .Values.operator.env | nindent 8 }}\n        {{- end }}\n        livenessProbe:\n          httpGet:\n            path: /healthz\n            port: 8081\n          initialDelaySeconds: 15\n          periodSeconds: 20\n        readinessProbe:\n          httpGet:\n            path: /readyz\n            port: 8081\n          initialDelaySeconds: 5\n          periodSeconds: 10\n        ports:\n        - name: metrics\n          containerPort: 8080\n          protocol: TCP\n        resources:\n          {{- toYaml .Values.operator.resources | nindent 10 }}\n        securityContext:\n          allowPrivilegeEscalation: false\n          capabilities:\n            drop:\n            - \"ALL\"\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/operator-hpa.yaml",
    "content": "{{- if and .Values.operator.enabled .Values.operator.hpa.enabled }}\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: {{ include \"llama-agents.operator.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.operator.name\" . }}\n    component: operator\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: {{ include \"llama-agents.operator.name\" . }}\n  minReplicas: {{ .Values.operator.hpa.minReplicas }}\n  maxReplicas: {{ .Values.operator.hpa.maxReplicas }}\n  metrics:\n  {{- $hasMetric := false }}\n  {{- if .Values.operator.hpa.targetCPUUtilizationPercentage }}\n  - type: Resource\n    resource:\n      name: cpu\n      target:\n        type: Utilization\n        averageUtilization: {{ .Values.operator.hpa.targetCPUUtilizationPercentage }}\n  {{- $hasMetric = true }}\n  {{- end }}\n  {{- if .Values.operator.hpa.targetMemoryUtilizationPercentage }}\n  - type: Resource\n    resource:\n      name: memory\n      target:\n        type: Utilization\n        averageUtilization: {{ .Values.operator.hpa.targetMemoryUtilizationPercentage }}\n  {{- $hasMetric = true }}\n  {{- end }}\n  {{- if not $hasMetric }}\n  - type: Resource\n    resource:\n      name: cpu\n      target:\n        type: Utilization\n        averageUtilization: 80\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/rbac.yaml",
    "content": "{{- if .Values.rbac.create }}\n{{- $appsNs := include \"llama-agents.apps.namespace\" . -}}\n{{- $releaseNs := .Release.Namespace -}}\n{{- $split := include \"llama-agents.apps.splitNamespace\" . -}}\n{{- $saName := include \"llama-agents.serviceAccountName\" . -}}\n{{- /*\nSplit mode (apps != release): apps-ns Role with everything except leases,\nrelease-ns Role with only leases (leader-election Lease lives in the\noperator pod's namespace). Single-namespace mode: one combined Role.\n*/ -}}\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {{ $saName }}\n  namespace: {{ $appsNs }}\n  {{- with .Values.rbac.roleAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nrules:\n- apiGroups: ['']\n  resources: ['configmaps', 'secrets', 'serviceaccounts', 'services']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n- apiGroups: ['']\n  resources: ['events']\n  verbs: ['create', 'get', 'list', 'patch', 'watch']\n- apiGroups: ['']\n  resources: ['pods']\n  verbs: ['get', 'list', 'watch']\n- apiGroups: ['']\n  resources: ['pods/log']\n  verbs: ['get']\n- apiGroups: ['apps']\n  resources: ['deployments']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n- apiGroups: ['apps']\n  resources: ['replicasets']\n  verbs: ['get', 'list', 'patch', 'update', 'watch']\n- apiGroups: ['batch']\n  resources: ['jobs']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n{{- if not $split }}\n- apiGroups: ['coordination.k8s.io']\n  resources: ['leases']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n{{- end }}\n- apiGroups: ['deploy.llamaindex.ai']\n  resources: ['llamadeployments']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n- apiGroups: ['deploy.llamaindex.ai']\n  resources: ['llamadeployments/finalizers']\n  verbs: ['update']\n- apiGroups: ['deploy.llamaindex.ai']\n  resources: ['llamadeployments/status']\n  verbs: ['get', 'patch', 'update']\n- apiGroups: ['deploy.llamaindex.ai']\n  resources: ['llamadeploymenttemplates']\n  verbs: ['get', 'list', 'watch']\n- apiGroups: ['networking.k8s.io']\n  resources: ['ingresses', 'networkpolicies']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {{ $saName }}\n  namespace: {{ $appsNs }}\n  {{- with .Values.rbac.roleBindingAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: {{ $saName }}\nsubjects:\n- kind: ServiceAccount\n  name: {{ $saName }}\n  namespace: {{ $releaseNs }}\n{{- if $split }}\n---\n# Leader-election Lease lives in the operator pod's namespace.\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {{ $saName }}-leader-election\n  namespace: {{ $releaseNs }}\n  {{- with .Values.rbac.roleAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nrules:\n- apiGroups: ['coordination.k8s.io']\n  resources: ['leases']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {{ $saName }}-leader-election\n  namespace: {{ $releaseNs }}\n  {{- with .Values.rbac.roleBindingAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: {{ $saName }}-leader-election\nsubjects:\n- kind: ServiceAccount\n  name: {{ $saName }}\n  namespace: {{ $releaseNs }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/s3proxy-configmap.yaml",
    "content": "{{- if .Values.s3proxy.enabled }}\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {{ include \"llama-agents.s3proxy.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\ndata:\n  {{- include \"llama-agents.s3proxy.configMapData\" . | nindent 2 }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/s3proxy-secret.yaml",
    "content": "{{- if and .Values.s3proxy.enabled (not .Values.s3proxy.secret) .Values.s3proxy.config }}\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"llama-agents.s3proxy.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\ntype: Opaque\ndata:\n  {{- include \"llama-agents.s3proxy.secretData\" . | nindent 2 }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"llama-agents.service.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n  {{- with .Values.controlPlane.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.controlPlane.service.type }}\n  ports:\n  - port: {{ .Values.controlPlane.service.port }}\n    targetPort: {{ .Values.controlPlane.container.port }}\n    protocol: TCP\n    name: http\n  selector:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n---\n# Build API service for git proxy - accessible only within cluster\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"llama-agents.build.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n    component: build-api\nspec:\n  type: ClusterIP\n  ports:\n  - port: {{ .Values.controlPlane.buildApi.port | default 8001 }}\n    targetPort: {{ .Values.controlPlane.buildApi.port | default 8001 }}\n    protocol: TCP\n    name: build-api\n  selector:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"llama-agents.operator.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.operator.name\" . }}\n    component: operator\nspec:\n  type: ClusterIP\n  selector:\n    app: {{ include \"llama-agents.operator.name\" . }}\n  ports:\n  - name: metrics\n    port: 8080\n    targetPort: 8080\n    protocol: TCP\n"
  },
  {
    "path": "charts/llama-agents/templates/serviceaccount.yaml",
    "content": "{{- if .Values.serviceAccount.create }}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"llama-agents.serviceAccountName\" . }}\n  namespace: {{ .Release.Namespace }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/templates/servicemonitor.yaml",
    "content": "{{- if .Values.metrics.enabled }}\napiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n  name: {{ include \"llama-agents.controlplane.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n{{- with .Values.metrics.additionalMonitorLabels }}\n{{ toYaml . | indent 4 }}\n{{- end }}\nspec:\n  selector:\n    matchLabels:\n      app: {{ include \"llama-agents.controlplane.name\" . }}\n  endpoints:\n    - port: http\n      path: /metrics\n      interval: {{ .Values.metrics.scrapeInterval | default \"30s\" }}\n      scrapeTimeout: {{ .Values.metrics.scrapeTimeout | default \"10s\" }}\n---\napiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n  name: {{ include \"llama-agents.build.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.controlplane.name\" . }}\n    component: build-api\n{{- with .Values.metrics.additionalMonitorLabels }}\n{{ toYaml . | indent 4 }}\n{{- end }}\nspec:\n  selector:\n    matchLabels:\n      app: {{ include \"llama-agents.controlplane.name\" . }}\n      component: build-api\n  endpoints:\n    - port: build-api\n      path: /metrics\n      interval: {{ .Values.metrics.scrapeInterval | default \"30s\" }}\n      scrapeTimeout: {{ .Values.metrics.scrapeTimeout | default \"10s\" }}\n---\napiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n  name: llama-deploy-appservers\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app.kubernetes.io/part-of: {{ include \"llama-agents.name\" . }}\n{{- with .Values.metrics.additionalMonitorLabels }}\n{{ toYaml . | indent 4 }}\n{{- end }}\nspec:\n  namespaceSelector:\n    any: true\n  selector:\n    matchLabels:\n      app.kubernetes.io/managed-by: llama-deploy-operator\n      component: appserver\n  endpoints:\n    - port: http\n      path: /metrics\n      interval: {{ .Values.metrics.scrapeInterval | default \"30s\" }}\n      scrapeTimeout: {{ .Values.metrics.scrapeTimeout | default \"10s\" }}\n---\napiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n  name: {{ include \"llama-agents.operator.name\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    app: {{ include \"llama-agents.operator.name\" . }}\n    component: operator\n{{- with .Values.metrics.additionalMonitorLabels }}\n{{ toYaml . | indent 4 }}\n{{- end }}\nspec:\n  selector:\n    matchLabels:\n      app: {{ include \"llama-agents.operator.name\" . }}\n  endpoints:\n    - port: metrics\n      interval: {{ .Values.metrics.scrapeInterval | default \"30s\" }}\n      scrapeTimeout: {{ .Values.metrics.scrapeTimeout | default \"10s\" }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents/tests/apps_namespace_test.yaml",
    "content": "suite: apps.namespace — single- and split-namespace modes\ntemplates:\n  - templates/operator-deployment.yaml\n  - templates/deployment.yaml\n  - templates/rbac.yaml\n  - templates/networkpolicy.yaml\n  - templates/operator-deployment-template.yaml\nset:\n  controlPlane:\n    objectStorage:\n      s3:\n        bucket: \"test-bucket\"\ntests:\n  # ---------- Unset apps.namespace — single-namespace mode ----------\n  - it: WATCH_NAMESPACE defaults to release namespace\n    release:\n      namespace: llama-agents\n    template: templates/operator-deployment.yaml\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: WATCH_NAMESPACE\n            value: \"llama-agents\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: LLAMA_DEPLOY_SYSTEM_NAMESPACE\n            valueFrom:\n              fieldRef:\n                fieldPath: metadata.namespace\n\n  - it: control plane KUBERNETES_NAMESPACE defaults to release namespace\n    release:\n      namespace: llama-agents\n    template: templates/deployment.yaml\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: KUBERNETES_NAMESPACE\n            value: \"llama-agents\"\n\n  - it: single combined Role in release namespace\n    release:\n      namespace: llama-agents\n    template: templates/rbac.yaml\n    asserts:\n      - hasDocuments:\n          count: 2  # Role + RoleBinding only\n      - documentIndex: 0\n        equal:\n          path: kind\n          value: Role\n      - documentIndex: 0\n        equal:\n          path: metadata.namespace\n          value: llama-agents\n      - documentIndex: 0\n        contains:\n          path: rules\n          content:\n            apiGroups: ['coordination.k8s.io']\n            resources: ['leases']\n            verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n\n  - it: NetworkPolicy lives in release namespace\n    release:\n      namespace: llama-agents\n    template: templates/networkpolicy.yaml\n    asserts:\n      - equal:\n          path: metadata.namespace\n          value: llama-agents\n\n  # ---------- apps.namespace set — split mode ----------\n  - it: split WATCH_NAMESPACE points at apps namespace\n    release:\n      namespace: llama-agents\n    set:\n      apps:\n        namespace: llama-agents-apps\n    template: templates/operator-deployment.yaml\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: WATCH_NAMESPACE\n            value: \"llama-agents-apps\"\n\n  - it: split control plane KUBERNETES_NAMESPACE points at apps namespace\n    release:\n      namespace: llama-agents\n    set:\n      apps:\n        namespace: llama-agents-apps\n    template: templates/deployment.yaml\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: KUBERNETES_NAMESPACE\n            value: \"llama-agents-apps\"\n\n  - it: split renders two Roles — apps Role sans leases, release Role with only leases\n    release:\n      namespace: llama-agents\n    set:\n      apps:\n        namespace: llama-agents-apps\n    template: templates/rbac.yaml\n    asserts:\n      - hasDocuments:\n          count: 4  # apps Role + RB, leader-election Role + RB\n      - documentIndex: 0\n        equal:\n          path: kind\n          value: Role\n      - documentIndex: 0\n        equal:\n          path: metadata.namespace\n          value: llama-agents-apps\n      - documentIndex: 0\n        notContains:\n          path: rules\n          content:\n            apiGroups: ['coordination.k8s.io']\n            resources: ['leases']\n            verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n      - documentIndex: 1\n        equal:\n          path: kind\n          value: RoleBinding\n      - documentIndex: 1\n        equal:\n          path: metadata.namespace\n          value: llama-agents-apps\n      - documentIndex: 1\n        equal:\n          path: subjects[0].namespace\n          value: llama-agents\n      - documentIndex: 2\n        equal:\n          path: kind\n          value: Role\n      - documentIndex: 2\n        equal:\n          path: metadata.namespace\n          value: llama-agents\n      - documentIndex: 2\n        contains:\n          path: rules\n          content:\n            apiGroups: ['coordination.k8s.io']\n            resources: ['leases']\n            verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n      - documentIndex: 3\n        equal:\n          path: kind\n          value: RoleBinding\n      - documentIndex: 3\n        equal:\n          path: metadata.namespace\n          value: llama-agents\n\n  - it: split NetworkPolicy lives in apps namespace with namespaceSelector to release ns\n    release:\n      namespace: llama-agents\n    set:\n      apps:\n        namespace: llama-agents-apps\n    template: templates/networkpolicy.yaml\n    asserts:\n      - equal:\n          path: metadata.namespace\n          value: llama-agents-apps\n      - equal:\n          path: spec.egress[0].to[0].namespaceSelector.matchLabels[\"kubernetes.io/metadata.name\"]\n          value: llama-agents\n\n  - it: split LlamaDeploymentTemplate lands in apps namespace\n    release:\n      namespace: llama-agents\n    set:\n      apps:\n        namespace: llama-agents-apps\n      operator:\n        llamaDeploymentTemplate:\n          enabled: true\n    template: templates/operator-deployment-template.yaml\n    asserts:\n      - equal:\n          path: metadata.namespace\n          value: llama-agents-apps\n\n  # ---------- apps.namespace equal to release namespace — single-ns output ----------\n  - it: apps.namespace equal to release namespace collapses to single Role\n    release:\n      namespace: llama-agents\n    set:\n      apps:\n        namespace: llama-agents\n    template: templates/rbac.yaml\n    asserts:\n      - hasDocuments:\n          count: 2\n"
  },
  {
    "path": "charts/llama-agents/tests/object_storage_test.yaml",
    "content": "suite: Control plane object storage configuration\ntemplates:\n  - templates/deployment.yaml\n  - templates/controlplane-s3-secret.yaml\ntests:\n  - it: fails when objectStorage bucket is not set (default)\n    template: templates/deployment.yaml\n    asserts:\n      - failedTemplate:\n          errorMessage: \"controlPlane.objectStorage.s3.bucket is required\"\n\n  - it: sets S3 env vars when objectStorage bucket is configured\n    template: templates/deployment.yaml\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            endpointUrl: \"https://s3.example.com\"\n            bucket: \"my-bucket\"\n            region: \"us-west-2\"\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_ENDPOINT_URL\n            value: \"https://s3.example.com\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_BUCKET\n            value: \"my-bucket\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_REGION\n            value: \"us-west-2\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_UNSIGNED\n            value: \"false\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: BACKUP_S3_KEY_PREFIX\n            value: \"backups\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: CODE_REPO_S3_KEY_PREFIX\n            value: \"git\"\n\n  - it: sets S3_UNSIGNED to true when unsigned is enabled\n    template: templates/deployment.yaml\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            unsigned: true\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_UNSIGNED\n            value: \"true\"\n\n  - it: sets envFrom with objectStorage secretRef\n    template: templates/deployment.yaml\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            endpointUrl: \"https://s3.example.com\"\n            bucket: \"my-bucket\"\n            region: \"us-west-2\"\n          secretRef: \"s3-credentials\"\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"s3-credentials\"\n\n  - it: sets envFrom with backup encryptionSecretRef\n    template: templates/deployment.yaml\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n          backupEncryptionSecretRef: \"backup-encryption\"\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"backup-encryption\"\n\n  - it: does not set envFrom when no secretRefs configured\n    template: templates/deployment.yaml\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            endpointUrl: \"https://s3.example.com\"\n            bucket: \"my-bucket\"\n            region: \"us-west-2\"\n    asserts:\n      - notExists:\n          path: spec.template.spec.containers[0].envFrom\n\n  - it: merges all envFrom sources\n    template: templates/deployment.yaml\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            endpointUrl: \"https://s3.example.com\"\n            bucket: \"my-bucket\"\n            region: \"us-west-2\"\n          secretRef: \"s3-credentials\"\n          backupEncryptionSecretRef: \"backup-encryption\"\n        container:\n          envFrom:\n            - configMapRef:\n                name: \"my-config\"\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            configMapRef:\n              name: \"my-config\"\n      - contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"s3-credentials\"\n      - contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"backup-encryption\"\n\n  # --- inline S3 creds ---\n\n  - it: renders a chart Secret with inline accessKey and secretKey\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            accessKey: \"AKIAEXAMPLE\"\n            secretKey: \"SECRETEXAMPLE\"\n    template: templates/controlplane-s3-secret.yaml\n    asserts:\n      - isKind:\n          of: Secret\n      - equal:\n          path: metadata.name\n          value: llama-agents-controlplane-s3\n      - equal:\n          path: type\n          value: Opaque\n      - equal:\n          path: data.S3_ACCESS_KEY\n          value: QUtJQUVYQU1QTEU=\n      - equal:\n          path: data.S3_SECRET_KEY\n          value: U0VDUkVURVhBTVBMRQ==\n\n  - it: wires envFrom to the chart-rendered Secret when inline creds are set\n    template: templates/deployment.yaml\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            accessKey: \"AKIAEXAMPLE\"\n            secretKey: \"SECRETEXAMPLE\"\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"llama-agents-controlplane-s3\"\n\n  - it: does not render the chart Secret when inline creds are unset\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n    template: templates/controlplane-s3-secret.yaml\n    asserts:\n      - hasDocuments:\n          count: 0\n\n  - it: fails when only accessKey is set\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            accessKey: \"AKIAEXAMPLE\"\n    template: templates/controlplane-s3-secret.yaml\n    asserts:\n      - failedTemplate:\n          errorMessage: \"controlPlane.objectStorage.s3.accessKey and controlPlane.objectStorage.s3.secretKey must both be set\"\n\n  - it: fails when only secretKey is set\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            secretKey: \"SECRETEXAMPLE\"\n    template: templates/controlplane-s3-secret.yaml\n    asserts:\n      - failedTemplate:\n          errorMessage: \"controlPlane.objectStorage.s3.accessKey and controlPlane.objectStorage.s3.secretKey must both be set\"\n\n  # --- s3.secret BYO ---\n\n  - it: skips chart Secret and envFroms the user Secret when s3.secret is set\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            secret: \"my-s3-creds\"\n    asserts:\n      - template: templates/controlplane-s3-secret.yaml\n        hasDocuments:\n          count: 0\n      - template: templates/deployment.yaml\n        contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"my-s3-creds\"\n\n  # --- precedence ---\n\n  - it: prefers s3.secret over inline creds\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            accessKey: \"AKIAEXAMPLE\"\n            secretKey: \"SECRETEXAMPLE\"\n            secret: \"my-s3-creds\"\n    asserts:\n      - template: templates/controlplane-s3-secret.yaml\n        hasDocuments:\n          count: 0\n      - template: templates/deployment.yaml\n        contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"my-s3-creds\"\n      - template: templates/deployment.yaml\n        notContains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"llama-agents-controlplane-s3\"\n\n  - it: prefers s3.secret over legacy secretRef\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            secret: \"my-s3-creds\"\n          secretRef: \"legacy-creds\"\n    asserts:\n      - template: templates/deployment.yaml\n        contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"my-s3-creds\"\n      - template: templates/deployment.yaml\n        notContains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"legacy-creds\"\n\n  - it: prefers legacy secretRef over inline creds\n    set:\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            accessKey: \"AKIAEXAMPLE\"\n            secretKey: \"SECRETEXAMPLE\"\n          secretRef: \"legacy-creds\"\n    asserts:\n      - template: templates/controlplane-s3-secret.yaml\n        hasDocuments:\n          count: 0\n      - template: templates/deployment.yaml\n        contains:\n          path: spec.template.spec.containers[0].envFrom\n          content:\n            secretRef:\n              name: \"legacy-creds\"\n"
  },
  {
    "path": "charts/llama-agents/tests/operator_image_tag_env_test.yaml",
    "content": "suite: Operator env - LLAMA_DEPLOY_IMAGE_TAG\ntemplates:\n  - templates/operator-deployment.yaml\ntests:\n  - it: defaults LLAMA_DEPLOY_IMAGE_TAG to appVersion\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: LLAMA_DEPLOY_IMAGE_TAG\n          any: true\n  - it: sets LLAMA_DEPLOY_IMAGE_TAG when images.appserver.tag is provided\n    set:\n      images:\n        appserver:\n          tag: v9.9.9\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: LLAMA_DEPLOY_IMAGE_TAG\n            value: \"v9.9.9\"\n"
  },
  {
    "path": "charts/llama-agents/tests/s3proxy_test.yaml",
    "content": "suite: s3proxy sidecar configuration\ntemplates:\n  - templates/deployment.yaml\n  - templates/s3proxy-configmap.yaml\n  - templates/s3proxy-secret.yaml\nset:\n  controlPlane:\n    objectStorage:\n      s3:\n        bucket: \"my-bucket\"\ntests:\n  # --- disabled (default) ---\n\n  - it: does not render s3proxy ConfigMap or Secret when disabled\n    asserts:\n      - hasDocuments:\n          count: 0\n        template: templates/s3proxy-configmap.yaml\n      - hasDocuments:\n          count: 0\n        template: templates/s3proxy-secret.yaml\n\n  - it: deployment has a single container and no s3proxy volume when disabled\n    template: templates/deployment.yaml\n    asserts:\n      - lengthEqual:\n          path: spec.template.spec.containers\n          count: 1\n      - equal:\n          path: spec.template.spec.containers[0].name\n          value: app\n      - notExists:\n          path: spec.template.spec.volumes\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_ENDPOINT_URL\n            value: \"\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_UNSIGNED\n            value: \"false\"\n\n  # --- enabled: ConfigMap / Secret ---\n\n  - it: renders the s3proxy ConfigMap when enabled\n    set:\n      s3proxy:\n        enabled: true\n    template: templates/s3proxy-configmap.yaml\n    asserts:\n      - isKind:\n          of: ConfigMap\n      - equal:\n          path: metadata.name\n          value: llama-agents-s3proxy\n      - equal:\n          path: data.S3PROXY_AUTHORIZATION\n          value: \"none\"\n      - equal:\n          path: data.S3PROXY_CORS_ALLOW_ORIGINS\n          value: \"*\"\n      - equal:\n          path: data.S3PROXY_ENDPOINT\n          value: \"http://0.0.0.0:8080\"\n      - equal:\n          path: data.S3PROXY_IGNORE_UNKNOWN_HEADERS\n          value: \"true\"\n\n  - it: renders the s3proxy Secret with b64-encoded config entries\n    set:\n      s3proxy:\n        enabled: true\n        config:\n          JCLOUDS_PROVIDER: azureblob\n          JCLOUDS_IDENTITY: my-id\n          JCLOUDS_CREDENTIAL: my-secret\n    template: templates/s3proxy-secret.yaml\n    asserts:\n      - isKind:\n          of: Secret\n      - equal:\n          path: metadata.name\n          value: llama-agents-s3proxy\n      - equal:\n          path: type\n          value: Opaque\n      - equal:\n          path: data.JCLOUDS_PROVIDER\n          value: YXp1cmVibG9i\n      - equal:\n          path: data.JCLOUDS_IDENTITY\n          value: bXktaWQ=\n      - equal:\n          path: data.JCLOUDS_CREDENTIAL\n          value: bXktc2VjcmV0\n\n  # --- enabled: deployment injection ---\n\n  - it: injects s3proxy sidecar and volume when enabled\n    set:\n      s3proxy:\n        enabled: true\n    template: templates/deployment.yaml\n    asserts:\n      - lengthEqual:\n          path: spec.template.spec.containers\n          count: 2\n      - equal:\n          path: spec.template.spec.containers[1].name\n          value: s3proxy\n      - equal:\n          path: spec.template.spec.containers[1].image\n          value: docker.io/andrewgaul/s3proxy:3.1.0\n      - equal:\n          path: spec.template.spec.containers[1].ports[0].containerPort\n          value: 8080\n      - contains:\n          path: spec.template.spec.containers[1].envFrom\n          content:\n            configMapRef:\n              name: llama-agents-s3proxy\n      - lengthEqual:\n          path: spec.template.spec.containers[1].envFrom\n          count: 1\n      - equal:\n          path: spec.template.spec.containers[1].volumeMounts[0].name\n          value: s3proxy-tmp\n      - equal:\n          path: spec.template.spec.containers[1].volumeMounts[0].mountPath\n          value: /tmp\n      - equal:\n          path: spec.template.spec.containers[1].volumeMounts[0].subPath\n          value: tmp-dir\n      - contains:\n          path: spec.template.spec.volumes\n          content:\n            name: s3proxy-tmp\n            emptyDir: {}\n\n  - it: does not render the sidecar Secret when no config or secret is set\n    set:\n      s3proxy:\n        enabled: true\n    template: templates/s3proxy-secret.yaml\n    asserts:\n      - hasDocuments:\n          count: 0\n\n  - it: renders chart Secret and envFrom at chart name when only config is set\n    set:\n      s3proxy:\n        enabled: true\n        config:\n          JCLOUDS_PROVIDER: azureblob\n    asserts:\n      - template: templates/s3proxy-secret.yaml\n        hasDocuments:\n          count: 1\n      - template: templates/deployment.yaml\n        contains:\n          path: spec.template.spec.containers[1].envFrom\n          content:\n            secretRef:\n              name: llama-agents-s3proxy\n\n  - it: skips chart Secret and envFroms the user-supplied Secret when only secret is set\n    set:\n      s3proxy:\n        enabled: true\n        secret: my-s3proxy-secret\n    asserts:\n      - template: templates/s3proxy-secret.yaml\n        hasDocuments:\n          count: 0\n      - template: templates/deployment.yaml\n        contains:\n          path: spec.template.spec.containers[1].envFrom\n          content:\n            secretRef:\n              name: my-s3proxy-secret\n\n  - it: prefers secret over config (silent precedence)\n    set:\n      s3proxy:\n        enabled: true\n        config:\n          JCLOUDS_PROVIDER: azureblob\n        secret: my-s3proxy-secret\n    asserts:\n      - template: templates/s3proxy-secret.yaml\n        hasDocuments:\n          count: 0\n      - template: templates/deployment.yaml\n        contains:\n          path: spec.template.spec.containers[1].envFrom\n          content:\n            secretRef:\n              name: my-s3proxy-secret\n      - template: templates/deployment.yaml\n        notContains:\n          path: spec.template.spec.containers[1].envFrom\n          content:\n            secretRef:\n              name: llama-agents-s3proxy\n\n  - it: defaults S3_ENDPOINT_URL to localhost and S3_UNSIGNED to true when enabled\n    set:\n      s3proxy:\n        enabled: true\n    template: templates/deployment.yaml\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_ENDPOINT_URL\n            value: \"http://localhost:8080\"\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_UNSIGNED\n            value: \"true\"\n\n  - it: honors explicit endpointUrl override when s3proxy is enabled\n    set:\n      s3proxy:\n        enabled: true\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            endpointUrl: \"https://s3.example.com\"\n    template: templates/deployment.yaml\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_ENDPOINT_URL\n            value: \"https://s3.example.com\"\n\n  - it: honors explicit unsigned=false override when s3proxy is enabled\n    set:\n      s3proxy:\n        enabled: true\n      controlPlane:\n        objectStorage:\n          s3:\n            bucket: \"my-bucket\"\n            unsigned: false\n    template: templates/deployment.yaml\n    asserts:\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_UNSIGNED\n            value: \"false\"\n\n  # --- sidecar overrides ---\n\n  - it: uses a custom containerPort for the sidecar and the localhost URL\n    set:\n      s3proxy:\n        enabled: true\n        containerPort: 9000\n    template: templates/deployment.yaml\n    asserts:\n      - equal:\n          path: spec.template.spec.containers[1].ports[0].containerPort\n          value: 9000\n      - contains:\n          path: spec.template.spec.containers[0].env\n          content:\n            name: S3_ENDPOINT_URL\n            value: \"http://localhost:9000\"\n\n  - it: uses custom containerPort in the ConfigMap's S3PROXY_ENDPOINT\n    set:\n      s3proxy:\n        enabled: true\n        containerPort: 9000\n    template: templates/s3proxy-configmap.yaml\n    asserts:\n      - equal:\n          path: data.S3PROXY_ENDPOINT\n          value: \"http://0.0.0.0:9000\"\n\n  - it: uses a custom image when specified\n    set:\n      s3proxy:\n        enabled: true\n        image: custom-registry/s3proxy:custom-tag\n    template: templates/deployment.yaml\n    asserts:\n      - equal:\n          path: spec.template.spec.containers[1].image\n          value: custom-registry/s3proxy:custom-tag\n\n  - it: applies custom resources to the sidecar\n    set:\n      s3proxy:\n        enabled: true\n        resources:\n          requests:\n            cpu: 250m\n            memory: 256Mi\n          limits:\n            cpu: \"2\"\n            memory: 2Gi\n    template: templates/deployment.yaml\n    asserts:\n      - equal:\n          path: spec.template.spec.containers[1].resources.requests.cpu\n          value: 250m\n      - equal:\n          path: spec.template.spec.containers[1].resources.requests.memory\n          value: 256Mi\n      - equal:\n          path: spec.template.spec.containers[1].resources.limits.cpu\n          value: \"2\"\n      - equal:\n          path: spec.template.spec.containers[1].resources.limits.memory\n          value: 2Gi\n\n  - it: omits securityContext when not specified\n    set:\n      s3proxy:\n        enabled: true\n    template: templates/deployment.yaml\n    asserts:\n      - isNull:\n          path: spec.template.spec.containers[1].securityContext\n\n  - it: renders securityContext when specified\n    set:\n      s3proxy:\n        enabled: true\n        securityContext:\n          allowPrivilegeEscalation: false\n          capabilities:\n            drop:\n              - ALL\n    template: templates/deployment.yaml\n    asserts:\n      - equal:\n          path: spec.template.spec.containers[1].securityContext.allowPrivilegeEscalation\n          value: false\n      - equal:\n          path: spec.template.spec.containers[1].securityContext.capabilities.drop[0]\n          value: ALL\n"
  },
  {
    "path": "charts/llama-agents/tests/servicemonitor_test.yaml",
    "content": "suite: ServiceMonitor metadata labels\ntemplates:\n  - templates/servicemonitor.yaml\ntests:\n  - it: puts additionalMonitorLabels under metadata.labels across all ServiceMonitors\n    set:\n      metrics:\n        enabled: true\n        additionalMonitorLabels:\n          release: prometheus\n    asserts:\n      - equal:\n          path: metadata.labels.release\n          value: prometheus\n        documentIndex: 0\n      - equal:\n          path: metadata.labels.release\n          value: prometheus\n        documentIndex: 1\n      - equal:\n          path: metadata.labels.release\n          value: prometheus\n        documentIndex: 2\n      - equal:\n          path: metadata.labels.release\n          value: prometheus\n        documentIndex: 3\n"
  },
  {
    "path": "charts/llama-agents/values.yaml",
    "content": "metrics:\n  # -- Enable Prometheus ServiceMonitors\n  # @section -- Metrics\n  enabled: false\n  # -- Scrape interval for ServiceMonitors\n  # @section -- Metrics\n  scrapeInterval: 30s\n  # -- Scrape timeout for ServiceMonitors\n  # @section -- Metrics\n  scrapeTimeout: 10s\n  # -- Extra labels added to ServiceMonitors for Prometheus discovery (e.g., `release: prometheus`)\n  # @section -- Metrics\n  additionalMonitorLabels: {}\n\nimages:\n  controlPlane:\n    # -- Control plane image repository\n    # @section -- Images\n    repository: llamaindex/llama-agents-control-plane\n    # -- Control plane image tag\n    # @section -- Images\n    tag: \"0.12.2\"\n    # -- Control plane image pull policy\n    # @section -- Images\n    pullPolicy: IfNotPresent\n  operator:\n    # -- Operator image repository\n    # @section -- Images\n    repository: llamaindex/llama-agents-operator\n    # -- Operator image tag\n    # @section -- Images\n    tag: \"0.11.1\"\n    # -- Operator image pull policy\n    # @section -- Images\n    pullPolicy: IfNotPresent\n  appserver:\n    # -- Appserver image repository (used by operator for managed pods)\n    # @section -- Images\n    repository: llamaindex/llama-agents-appserver\n    # -- Appserver image tag\n    # @section -- Images\n    tag: \"0.11.4\"\n    # -- Appserver image pull policy\n    # @section -- Images\n    pullPolicy: IfNotPresent\n  nginx:\n    # -- Nginx sidecar image repository\n    # @section -- Images\n    repository: nginxinc/nginx-unprivileged\n    # -- Nginx sidecar image tag\n    # @section -- Images\n    tag: \"1.27-alpine\"\n    # -- Nginx sidecar image pull policy\n    # @section -- Images\n    pullPolicy: IfNotPresent\n\ncontrolPlane:\n  # -- Number of control plane replicas\n  # @section -- Control Plane\n  replicas: 1\n  container:\n    # -- Control plane API port\n    # @section -- Control Plane\n    port: 8000\n    # -- Extra environment variables for the control plane container\n    # @section -- Control Plane\n    env: []\n    # -- Extra envFrom sources (secretRef, configMapRef) for the control plane container\n    # @section -- Control Plane\n    envFrom: []\n    # -- Resource requests/limits for the control plane container\n    # @default -- `{requests: {cpu: 100m, memory: 256Mi, ephemeral-storage: 500Mi}}`\n    # @section -- Control Plane\n    resources:\n      requests:\n        cpu: 100m\n        memory: 256Mi\n        ephemeral-storage: 500Mi\n    # -- Startup probe configuration\n    # @section -- Control Plane\n    startupProbe: {}\n    # -- Liveness probe configuration\n    # @section -- Control Plane\n    livenessProbe: {}\n  deployment:\n    # -- Annotations for the control plane Deployment\n    # @section -- Control Plane\n    annotations: {}\n    # -- Annotations for the control plane pod template\n    # @section -- Control Plane\n    podAnnotations: {}\n\n  service:\n    # -- Control plane Service type\n    # @section -- Control Plane\n    type: ClusterIP\n    # -- Control plane Service port\n    # @section -- Control Plane\n    port: 80\n    # -- Annotations for the control plane Service\n    # @section -- Control Plane\n    annotations: {}\n    # -- Metrics path for the control plane Service\n    # @section -- Control Plane\n    metricsPath: /metrics\n\n  buildApi:\n    # -- Build API port (git proxy and token validation)\n    # @section -- Control Plane\n    port: 8001\n    # -- Metrics path for the build API\n    # @section -- Control Plane\n    metricsPath: /metrics\n\n  objectStorage:\n    s3:\n      # -- S3 endpoint URL (leave empty for AWS)\n      # @section -- Object Storage\n      endpointUrl: \"\"\n      # -- S3 bucket name (**required**)\n      # @section -- Object Storage\n      bucket: \"\"\n      # -- S3 region\n      # @section -- Object Storage\n      region: \"\"\n      # -- Send S3 requests unsigned (no Authorization header). Leave unset/`false` for any auth-requiring S3-compatible backend. For non-S3 object/blob storage, see `s3proxy.enabled` below — when that's on, this defaults to `true` unless you override it here.\n      # @section -- Object Storage\n      unsigned:\n      # -- Inline S3 access key. When set alongside `secretKey`, the chart renders\n      # a Secret and wires it into the control plane. Mutually exclusive with\n      # `s3.secret` (which wins silently).\n      # @section -- Object Storage\n      accessKey: \"\"\n      # -- Inline S3 secret key. Must be set together with `accessKey`; partial\n      # setting is an error.\n      # @section -- Object Storage\n      secretKey: \"\"\n      # -- Name of an existing K8s Secret supplying `S3_ACCESS_KEY` and\n      # `S3_SECRET_KEY`. Takes precedence over `accessKey`/`secretKey`.\n      # @section -- Object Storage\n      secret: \"\"\n    # Silent back-compat alias for s3.secret. Preferred surface is s3.secret.\n    # @ignored\n    secretRef: \"\"\n    # -- Key prefix for build artifacts in the bucket\n    # @section -- Object Storage\n    buildKeyPrefix: \"builds\"\n    # -- Key prefix for backup archives in the bucket\n    # @section -- Object Storage\n    backupKeyPrefix: \"backups\"\n    # -- Key prefix for code repositories in the bucket\n    # @section -- Object Storage\n    codeRepoKeyPrefix: \"git\"\n    # -- K8s Secret name containing `BACKUP_ENCRYPTION_PASSWORD`\n    # @section -- Object Storage\n    backupEncryptionSecretRef: \"\"\n\n  hpa:\n    # -- Enable HPA for the control plane\n    # @section -- Control Plane\n    enabled: false\n    # -- Minimum replicas\n    # @section -- Control Plane\n    minReplicas: 1\n    # -- Maximum replicas\n    # @section -- Control Plane\n    maxReplicas: 3\n    # -- Target average CPU utilization percentage\n    # @section -- Control Plane\n    targetCPUUtilizationPercentage: 80\n    # -- Target average memory utilization percentage\n    # @section -- Control Plane\n    # targetMemoryUtilizationPercentage: 80\n\napps:\n  # -- Namespace where LlamaDeployment CRs and all operator-managed child\n  # resources live. Empty = release namespace. When set, the operator + control\n  # plane stay in the release namespace and target this namespace for all app\n  # resources.\n  # @section -- Apps\n  namespace: \"\"\n\ncrds:\n  # -- Compatible `llama-agents-crds` chart version for this release. Documentation only; not read by templates. Auto-synced at release time.\n  # @section -- CRDs\n  version: \"0.7.2\"\n\noperator:\n  # -- Deploy the operator\n  # @section -- Operator\n  enabled: true\n  # -- Number of operator replicas\n  # @section -- Operator\n  replicas: 1\n  # -- Annotations for the operator Deployment\n  # @section -- Operator\n  annotations: {}\n  # -- Annotations for the operator pod template\n  # @section -- Operator\n  podAnnotations: {}\n  defaultAppRequests:\n    # -- Default CPU request for managed app containers\n    # @section -- Operator\n    cpu: \"750m\"\n    # -- Default memory request for managed app containers\n    # @section -- Operator\n    memory: \"2Gi\"\n  defaultAppLimits:\n    # -- Default CPU limit for managed app containers (empty = no limit)\n    # @section -- Operator\n    cpu: \"\"\n    # -- Default memory limit for managed app containers\n    # @section -- Operator\n    memory: \"4096Mi\"\n  # -- Resource requests/limits for the operator container\n  # @default -- `{limits: {cpu: 500m, memory: 128Mi}, requests: {cpu: 10m, memory: 64Mi}}`\n  # @section -- Operator\n  resources:\n    limits:\n      cpu: 500m\n      memory: 128Mi\n    requests:\n      cpu: 10m\n      memory: 64Mi\n  # -- Max simultaneous LlamaDeployment rollouts (0 = unlimited)\n  # @section -- Operator\n  maxConcurrentRollouts: 10\n  # -- Max active LlamaDeployments per namespace (0 = unlimited)\n  # @section -- Operator\n  maxDeployments: 0\n  # -- Extra environment variables for the operator container\n  # @section -- Operator\n  env: []\n  # -- Rollout timeout in seconds for managed deployments\n  # @section -- Operator\n  rolloutTimeoutSeconds: 1800\n\n  llamaDeploymentTemplate:\n    # -- Create a default LlamaDeploymentTemplate in the namespace\n    # @section -- Operator\n    enabled: false\n    # -- Template resource name\n    # @section -- Operator\n    name: \"default\"\n    # -- Metadata for the template (labels, annotations)\n    # @section -- Operator\n    metadata: {}\n    # -- Template spec (podSpec with nodeSelector, tolerations, affinity, container overrides)\n    # @section -- Operator\n    spec:\n      podSpec: {}\n\n  hpa:\n    # -- Enable HPA for the operator\n    # @section -- Operator\n    enabled: false\n    # -- Minimum replicas\n    # @section -- Operator\n    minReplicas: 1\n    # -- Maximum replicas\n    # @section -- Operator\n    maxReplicas: 3\n    # -- Target average CPU utilization percentage\n    # @section -- Operator\n    targetCPUUtilizationPercentage: 80\n    # -- Target average memory utilization percentage\n    # @section -- Operator\n    # targetMemoryUtilizationPercentage: 80\n\nlocalDev:\n  # -- Enable local dev ingress for deployed apps\n  # @section -- Local Development\n  enabled: false\n  # -- Ingress domain for local dev\n  # @section -- Local Development\n  ingressDomain: \"127.0.0.1.nip.io\"\n\nrbac:\n  # -- Create Role and RoleBinding\n  # @section -- RBAC\n  create: true\n  # -- Annotations for the Role\n  # @section -- RBAC\n  roleAnnotations: {}\n  # -- Annotations for the RoleBinding\n  # @section -- RBAC\n  roleBindingAnnotations: {}\n\nserviceAccount:\n  # -- Create a ServiceAccount\n  # @section -- Service Account\n  create: true\n  # -- ServiceAccount name\n  # @section -- Service Account\n  name: llama-agents\n  # -- Annotations for the ServiceAccount\n  # @section -- Service Account\n  annotations: {}\n\ns3proxy:\n  # -- Run an [s3proxy](https://github.com/gaul/s3proxy) sidecar alongside the\n  # control plane to translate S3 API calls to non-AWS backends (Azure Blob,\n  # GCS, etc.). When enabled, `S3_ENDPOINT_URL` and `S3_UNSIGNED` default to\n  # localhost and `true` unless explicitly overridden. Fill in `s3proxy.config`\n  # with the JCLOUDS_* environment variables for your cloud.\n  # @section -- s3proxy\n  enabled: false\n  # -- s3proxy container image\n  # @section -- s3proxy\n  image: \"docker.io/andrewgaul/s3proxy:3.1.0\"\n  # -- s3proxy image pull policy\n  # @section -- s3proxy\n  imagePullPolicy: IfNotPresent\n  # -- Port s3proxy listens on inside the pod (control plane reaches it over localhost)\n  # @section -- s3proxy\n  containerPort: 8080\n  # -- s3proxy log level (passed as LOG_LEVEL and S3PROXY_LOG_LEVEL)\n  # @section -- s3proxy\n  logLevel: info\n  # -- securityContext for the s3proxy container\n  # @section -- s3proxy\n  securityContext: {}\n  # -- Resource requests/limits for the s3proxy sidecar\n  # @default -- `{requests: {cpu: 50m, memory: 256Mi}, limits: {cpu: 500m, memory: 512Mi}}`\n  # @section -- s3proxy\n  resources:\n    requests:\n      cpu: 50m\n      memory: 256Mi\n    limits:\n      cpu: 500m\n      memory: 512Mi\n  # -- Raw passthrough to the s3proxy Secret. Keys become environment variables\n  # on the sidecar. Typically `JCLOUDS_PROVIDER`, `JCLOUDS_IDENTITY`,\n  # `JCLOUDS_CREDENTIAL`, `JCLOUDS_ENDPOINT`, `JCLOUDS_REGION`. See\n  # https://github.com/gaul/s3proxy/wiki/Storage-backend-examples.\n  # @section -- s3proxy\n  config: {}\n  # -- Name of an existing K8s Secret supplying the sidecar's env vars. Takes\n  # precedence over `config` (which is skipped if this is set).\n  # @section -- s3proxy\n  secret: \"\"\n\nnetworkPolicy:\n  # -- Enable egress NetworkPolicy for operator-managed pods\n  # @section -- Network Policy\n  enabled: true\n  # -- Additional pod selector matchExpressions\n  # @section -- Network Policy\n  extraMatchExpressions: []\n  # -- Extra egress rules appended to the NetworkPolicy\n  # @section -- Network Policy\n  extraEgressRules: []\n  # -- Block private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) in internet egress rule\n  # @section -- Network Policy\n  blockPrivateRanges: true\n  dns:\n    # -- Namespace selector for DNS pods. Defaults to kube-system\n    # @section -- Network Policy\n    namespaceSelector:\n      kubernetes.io/metadata.name: kube-system\n    # -- Pod selector for DNS pods. Defaults to kube-dns\n    # @section -- Network Policy\n    podSelector:\n      k8s-app: kube-dns\n"
  },
  {
    "path": "charts/llama-agents-crds/.helmignore",
    "content": "# Patterns to ignore when building packages.\n.DS_Store\n*.swp\n*.bak\n*.tmp\n*~\n.git\n.gitignore\n\n# Node modules symlink pulls in operator binaries which exceed Helm's 5MB file limit\nnode_modules\n"
  },
  {
    "path": "charts/llama-agents-crds/CHANGELOG.md",
    "content": "# llama-agents-crds\n\n## 0.7.2\n\n### Patch Changes\n\n- Updated dependencies [58e7942]\n- Updated dependencies [ea577a1]\n  - llama-agents-operator@0.10.0\n"
  },
  {
    "path": "charts/llama-agents-crds/Chart.yaml",
    "content": "apiVersion: v2\nname: llama-agents-crds\ndescription: CRDs for llama-agents (LlamaDeployment, LlamaDeploymentTemplate)\ntype: application\nversion: \"0.7.2\"\nappVersion: \"0.7.1\"\n"
  },
  {
    "path": "charts/llama-agents-crds/README.md",
    "content": "# llama-agents-crds\n\nHelm chart that manages the CRD lifecycle for llama-agents (`LlamaDeployment`, `LlamaDeploymentTemplate`).\n\n## Why a separate CRD chart?\n\nThe main `llama-agents` chart places CRDs in the Helm `crds/` directory, which means Helm installs them on `helm install` but **never touches them on `helm upgrade` or `helm uninstall`**. This is the safest default — accidental CRD deletion cascades to all custom resources.\n\nHowever, when CRD schemas change (e.g., the operator adds new fields), you need a way to upgrade them. This chart puts CRDs in `templates/` so they participate in `helm upgrade`, and uses the `helm.sh/resource-policy: keep` annotation so `helm uninstall` leaves them in place.\n\n## Usage\n\nInstall before upgrading the main chart if CRD schema has changed:\n\n```bash\nhelm upgrade --install llama-agents-crds charts/llama-agents-crds\n```\n\n## Uninstall behavior\n\n`helm uninstall llama-agents-crds` will **NOT** delete the CRDs thanks to `helm.sh/resource-policy: keep`.\n\nTo truly remove CRDs (WARNING: this deletes all CRs of these types):\n\n```bash\nkubectl delete crd llamadeployments.deploy.llamaindex.ai llamadeploymenttemplates.deploy.llamaindex.ai\n```\n"
  },
  {
    "path": "charts/llama-agents-crds/files/deploy.llamaindex.ai_llamadeployments.yaml",
    "content": "---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  annotations:\n    controller-gen.kubebuilder.io/version: v0.18.0\n  name: llamadeployments.deploy.llamaindex.ai\nspec:\n  group: deploy.llamaindex.ai\n  names:\n    kind: LlamaDeployment\n    listKind: LlamaDeploymentList\n    plural: llamadeployments\n    singular: llamadeployment\n  scope: Namespaced\n  versions:\n  - additionalPrinterColumns:\n    - jsonPath: .spec.projectId\n      name: Project ID\n      type: string\n    - jsonPath: .spec.displayName\n      name: Name\n      type: string\n    - jsonPath: .spec.repoUrl\n      name: Repo\n      type: string\n    - jsonPath: .status.phase\n      name: Phase\n      type: string\n    - jsonPath: .metadata.creationTimestamp\n      name: Age\n      type: date\n    name: v1\n    schema:\n      openAPIV3Schema:\n        description: LlamaDeployment is the Schema for the llamadeployments API.\n        properties:\n          apiVersion:\n            description: |-\n              APIVersion defines the versioned schema of this representation of an object.\n              Servers should convert recognized schemas to the latest internal value, and\n              may reject unrecognized values.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\n            type: string\n          kind:\n            description: |-\n              Kind is a string value representing the REST resource this object represents.\n              Servers may infer this from the endpoint the client submits requests to.\n              Cannot be updated.\n              In CamelCase.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\n            type: string\n          metadata:\n            type: object\n          spec:\n            description: LlamaDeploymentSpec defines the desired state of LlamaDeployment.\n            properties:\n              buildGeneration:\n                description: |-\n                  BuildGeneration is a monotonically increasing counter that forces a new\n                  build when incremented, even if all other inputs (gitSha, imageTag, etc.)\n                  are unchanged. This allows retrying a failed build caused by transient\n                  errors (e.g. network failures) without requiring a new git commit.\n                format: int64\n                type: integer\n              deploymentFilePath:\n                default: llama_deployment.yml\n                description: DeploymentFilePath is the path to the deployment file\n                  within the repository\n                type: string\n              displayName:\n                description: DisplayName is the user-facing deployment label\n                type: string\n              gitRef:\n                description: GitRef is the git reference (commit SHA, branch, or tag)\n                  to deploy\n                type: string\n              gitSha:\n                description: A resolved git sha for the git ref\n                type: string\n              image:\n                description: |-\n                  Image is the container image registry and name (e.g., \"llamaindex/llama-agents-appserver\")\n                  If not specified, defaults to environment variable or \"llamaindex/llama-agents-appserver\"\n                type: string\n              imageTag:\n                description: |-\n                  ImageTag is the container image tag\n                  If not specified, defaults to environment variable or \"latest\"\n                type: string\n              name:\n                description: 'Name is the deployment name (DEPRECATED: use DisplayName)'\n                type: string\n              projectId:\n                description: ProjectId is the project ID\n                type: string\n              repoUrl:\n                description: RepoUrl is the URL of the repository to deploy\n                type: string\n              secretName:\n                description: SecretName is the name of the Kubernetes Secret containing\n                  PAT and deployment secrets\n                type: string\n              staticAssetsPath:\n                description: |-\n                  StaticAssetsPath is an optional path (relative to /opt/app) containing\n                  prebuilt UI assets to be served under /deployments/<deployment-id>/ui\n                type: string\n              suspended:\n                description: |-\n                  Suspended scales the underlying Deployment to 0 replicas when true.\n                  Setting suspended to false (or removing the field) restores replicas to 1.\n                type: boolean\n              templateName:\n                description: |-\n                  TemplateName optionally specifies a LlamaDeploymentTemplate to apply.\n                  When empty, the operator will look up a template named \"default\".\n                type: string\n            required:\n            - projectId\n            - repoUrl\n            type: object\n          status:\n            description: LlamaDeploymentStatus defines the observed state of LlamaDeployment.\n            properties:\n              authToken:\n                description: AuthToken is a cryptographically secure token for this\n                  deployment\n                type: string\n              buildId:\n                description: BuildId is the content-addressed identifier for the current\n                  build artifact\n                type: string\n              buildStatus:\n                description: BuildStatus tracks the state of the current build job\n                enum:\n                - Pending\n                - Running\n                - Succeeded\n                - Failed\n                type: string\n              failedRolloutGeneration:\n                description: |-\n                  FailedRolloutGeneration records the LlamaDeployment generation whose rollout\n                  timed out. This prevents the operator from re-attempting the same failing rollout.\n                format: int64\n                type: integer\n              lastBuiltGeneration:\n                description: |-\n                  LastBuiltGeneration is the spec.buildGeneration value that was last\n                  successfully built. When spec.buildGeneration differs from this value,\n                  a new build is triggered even if the deployment is suspended.\n                format: int64\n                type: integer\n              lastReconciledGeneration:\n                description: LastReconciledGeneration tracks the generation that was\n                  last successfully reconciled\n                format: int64\n                type: integer\n              lastUpdated:\n                description: LastUpdated is the timestamp of the last status update\n                format: date-time\n                type: string\n              message:\n                description: Message is a human-readable message indicating details\n                  about the current status\n                type: string\n              phase:\n                description: Phase represents the current phase of the deployment\n                enum:\n                - Pending\n                - Running\n                - Failed\n                - RollingOut\n                - RolloutFailed\n                - Suspended\n                - Building\n                - BuildFailed\n                - AwaitingCode\n                type: string\n              releaseHistory:\n                description: ReleaseHistory keeps the last 20 released git shas with\n                  timestamps\n                items:\n                  description: ReleaseHistoryEntry represents a single released version\n                    entry\n                  properties:\n                    gitSha:\n                      description: GitSha is the released git commit SHA\n                      type: string\n                    imageTag:\n                      description: ImageTag is the appserver image tag used for this\n                        release\n                      type: string\n                    releasedAt:\n                      description: ReleasedAt is the timestamp when this version was\n                        released\n                      format: date-time\n                      type: string\n                  required:\n                  - gitSha\n                  - releasedAt\n                  type: object\n                type: array\n              rolloutStartedAt:\n                description: |-\n                  RolloutStartedAt is the timestamp when the current rollout began.\n                  Set when the phase transitions to Pending or RollingOut, cleared on Running or failure.\n                format: date-time\n                type: string\n              schemaVersion:\n                description: SchemaVersion is the version of the CRD schema used when\n                  this resource was last reconciled\n                type: string\n              secretCheckRetries:\n                description: |-\n                  SecretCheckRetries tracks how many times we've retried finding the Secret.\n                  This handles informer cache lag when the Secret is created just before the CR.\n                format: int32\n                type: integer\n            type: object\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n"
  },
  {
    "path": "charts/llama-agents-crds/files/deploy.llamaindex.ai_llamadeploymenttemplates.yaml",
    "content": "---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  annotations:\n    controller-gen.kubebuilder.io/version: v0.18.0\n  name: llamadeploymenttemplates.deploy.llamaindex.ai\nspec:\n  group: deploy.llamaindex.ai\n  names:\n    kind: LlamaDeploymentTemplate\n    listKind: LlamaDeploymentTemplateList\n    plural: llamadeploymenttemplates\n    singular: llamadeploymenttemplate\n  scope: Namespaced\n  versions:\n  - additionalPrinterColumns:\n    - jsonPath: .metadata.creationTimestamp\n      name: Age\n      type: date\n    name: v1\n    schema:\n      openAPIV3Schema:\n        description: |-\n          LlamaDeploymentTemplate configures default Pod template fields for LlamaDeployments.\n          The resource name is referenced by LlamaDeployment.spec.templateName. A special name\n          \"default\" is used as a fallback when no templateName is provided.\n        properties:\n          apiVersion:\n            description: |-\n              APIVersion defines the versioned schema of this representation of an object.\n              Servers should convert recognized schemas to the latest internal value, and\n              may reject unrecognized values.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\n            type: string\n          kind:\n            description: |-\n              Kind is a string value representing the REST resource this object represents.\n              Servers may infer this from the endpoint the client submits requests to.\n              Cannot be updated.\n              In CamelCase.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\n            type: string\n          metadata:\n            type: object\n          spec:\n            description: |-\n              LlamaDeploymentTemplateSpec defines the desired overlay for a LlamaDeployment's PodTemplate.\n              This is intended to carry scheduling-related fields like node selectors, tolerations, and\n              affinity, but supports any partial PodTemplateSpec. Fields set here will take precedence\n              over the operator-computed defaults when merged.\n            properties:\n              podSpec:\n                description: PodSpec holds a partial PodTemplateSpec to be merged\n                  into the generated PodTemplate.\n                x-kubernetes-preserve-unknown-fields: true\n            type: object\n          status:\n            type: string\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n"
  },
  {
    "path": "charts/llama-agents-crds/package.json",
    "content": "{\n  \"name\": \"llama-agents-crds\",\n  \"version\": \"0.7.2\",\n  \"dependencies\": {},\n  \"helm\": {\n    \"registry\": \"oci://docker.io/llamaindex\"\n  }\n}\n"
  },
  {
    "path": "charts/llama-agents-crds/templates/crds.yaml",
    "content": "{{- range $path, $_ := .Files.Glob \"files/*.yaml\" }}\n{{- $crd := $.Files.Get $path | fromYaml }}\n{{- if $.Values.keep }}\n{{- $annotations := $crd.metadata.annotations | default dict }}\n{{- $_ := set $annotations \"helm.sh/resource-policy\" \"keep\" }}\n{{- $_ := set $crd.metadata \"annotations\" $annotations }}\n{{- end }}\n---\n{{ toYaml $crd }}\n{{- end }}\n"
  },
  {
    "path": "charts/llama-agents-crds/values.yaml",
    "content": "# When true, adds helm.sh/resource-policy: keep annotation to CRDs\n# so that helm uninstall does not delete them (and cascade-delete all CRs).\nkeep: true\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "# Base stage with common dependencies\nFROM python:3.12-slim AS base\n\nCOPY --from=ghcr.io/astral-sh/uv:0.7.20 /uv /uvx /bin/\n\n# Create a non-root user\nRUN groupadd --gid 1001 appgroup && \\\n    useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser\n\n# Create data directory and set permissions\nRUN mkdir -p /app && chown -R appuser:appgroup /app\n\n# Set working directory\nWORKDIR /app\n\n# Copy workspace configuration files\nCOPY pyproject.toml uv.lock README.md ./\n\nENV PATH=/app/.venv/bin:$PATH\n\n\n##############################################################################\n##############################################################################\n############################## CONTROL PLANE #################################\n##############################################################################\n##############################################################################\n\n##############################################################################\n###                    Control Plane - BUILDER                             ###\n##############################################################################\n#\n# pulls in all packages, and installs as non-editable\nFROM base AS controlplane-builder\n\nARG PACKAGE_NAME=llama-agents-control-plane\n\n# installs the 3rd party deps for the package\nRUN uv sync --frozen --no-dev --package $PACKAGE_NAME --no-install-workspace\n# Copy the 1st party deps\nCOPY packages/ packages/\n\n# Now install the 1st party deps into the venv (non-editable)\nRUN uv sync --frozen --no-dev --package $PACKAGE_NAME --no-editable\n\n##############################################################################\n###                   Control Plane - RUNNER                               ###\n##############################################################################\n\nFROM base AS controlplane\n\n# Copy the builder stage\nCOPY --from=controlplane-builder /app/.venv /app/.venv\n\nUSER appuser\n\n# Expose ports\nEXPOSE 8000 8001\n\n# Production command without reload\nCMD [\"python\", \"-m\", \"llama_agents.control_plane.main\", \"--host\", \"0.0.0.0\", \"--manage-api-port\", \"8000\", \"--build-api-port\", \"8001\", \"--log-level\", \"info\", \"--log-format\", \"json\"]\n\n\n\n##############################################################################\n##############################################################################\n################################# APP SERVER #################################\n##############################################################################\n##############################################################################\n\n\n##############################################################################\n###                       App Server - BUILDER                             ###\n##############################################################################\n#\n# pulls in all packages, and installs as non-editable\nFROM base AS appserver-builder\n\nARG PACKAGE_NAME=llama-agents-appserver\n\n# install to system python. Makes things easier for the cloned repo to \"inherit\" the appserver dependencies\n# installs the 3rd party deps for the package\nRUN uv sync --frozen --no-dev --package $PACKAGE_NAME --no-install-workspace\n# Copy the 1st party deps\nCOPY packages/ packages/\n\n# Now install the 1st party deps into the venv (non-editable)\nRUN uv sync --frozen --no-dev --package $PACKAGE_NAME --no-editable\nRUN uv build --package $PACKAGE_NAME --sdist\nRUN uv build --package llama-agents-core --sdist\n\n##############################################################################\n###                   App Server - RUNNER                                  ###\n##############################################################################\n\nFROM base AS appserver\n\n# Install Node.js, pnpm, and git for bootstrap-time git+https dependencies.\nRUN apt-get update && apt-get install -y \\\n    git \\\n    curl \\\n    && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \\\n    && apt-get install -y nodejs \\\n    && corepack enable \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy the builder stage\nCOPY --from=appserver-builder /app/.venv /app/.venv\nCOPY --from=appserver-builder /app/dist/ /app/dist\nENV LLAMA_DEPLOY_BOOTSTRAP_SDISTS=/app/dist/\n\nUSER appuser\n\nENV LLAMA_DEPLOY_APISERVER_PORT=8080\nENV LLAMA_DEPLOY_APISERVER_HOST=0.0.0.0\n\n# Expose ports\nEXPOSE 8080\n"
  },
  {
    "path": "docker/Dockerfile.mitm-test",
    "content": "# Container to recreate corporate MITM proxy scenario for testing native TLS mode\n# Simulates an environment where:\n# - HTTPS traffic is intercepted by a MITM proxy (common in corporate networks)\n# - The MITM CA is installed in system trust store\n# - Python tools fail because they use certifi instead of system certs\n# - Node tools fail as well\nFROM debian:stable-slim\n\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    ca-certificates \\\n    curl \\\n    openssl \\\n    python3 \\\n    python3-dev \\\n    build-essential \\\n    git \\\n    nodejs \\\n    npm \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Install mitmproxy\nRUN uv tool install mitmproxy\nRUN corepack enable\n\nENV PATH=/root/.local/bin:$PATH\n\nWORKDIR /app\n\n# Copy workspace configuration files\nCOPY pyproject.toml uv.lock README.md ./\nCOPY packages/ packages/\nRUN uv sync --all-packages\n\n\nCOPY docker/mitm-proxy-test/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\n\nENV PATH=/app/.venv/bin:$PATH\n\nWORKDIR /workspace\n# RUN llamactl init --no-interactive --template extraction-review --force\n# WORKDIR /workspace/extraction-review\n# RUN uv sync && cd ui && pnpm install\n\nEXPOSE 8080\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "docker/mitm-proxy-test/entrypoint.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# Generate a local CA if not present and install it into the OS trust store\n# mitmproxy provides its own CA in ~/.mitmproxy\nmkdir -p /root/.mitmproxy\n\n# Start and stop mitmproxy once to ensure CA exists\nif [ ! -f /root/.mitmproxy/mitmproxy-ca-cert.pem ]; then\n  # Run mitmdump briefly to generate the CA, then kill it\n  timeout 2s mitmdump >/dev/null 2>&1 || true\nfi\n\nCA_PEM=/root/.mitmproxy/mitmproxy-ca-cert.pem\n\nif [ -f \"$CA_PEM\" ]; then\n  echo \"Installing mitmproxy CA into system trust store...\"\n  cp \"$CA_PEM\" /usr/local/share/ca-certificates/mitmproxy-ca.crt\n  update-ca-certificates\nelse\n  echo \"mitmproxy CA not found; continuing without installing\"\nfi\n\nPORT=${PORT:-8080}\n\n# Run mitmdump (non-interactive) in the background\n# mitmdump logs to stdout by default\nif [ -n \"${TARGET:-}\" ]; then\n  echo \"Starting mitmdump in reverse proxy mode to $TARGET on port $PORT...\"\n  mitmdump --mode reverse:\"$TARGET\" --listen-port \"$PORT\" &\nelse\n  echo \"Starting mitmdump on port $PORT...\"\n  mitmdump --listen-port \"$PORT\" &\nfi\n\nMITM_PID=$!\necho \"mitmdump started with PID $MITM_PID\"\n\nexport HTTPS_PROXY=http://localhost:$PORT\n# Drop into a shell, or run any command passed as arguments\nif [ $# -eq 0 ]; then\n  exec bash\nelse\n  exec \"$@\"\nfi\n"
  },
  {
    "path": "docker/operator.Dockerfile",
    "content": "# Build the operator binary\nFROM golang:1.24 AS builder\nARG TARGETOS\nARG TARGETARCH\n\nWORKDIR /workspace\n\n# Copy the Go Modules manifests\nCOPY operator/go.mod operator/go.sum ./\nRUN go mod download\n\n# Copy the go source\nCOPY operator/cmd/ cmd/\nCOPY operator/api/ api/\nCOPY operator/internal/ internal/\n\n# Build\nRUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags=\"-s -w\" -o manager cmd/main.go\n\n# Use distroless as minimal base image to package the manager binary\nFROM gcr.io/distroless/static:nonroot\nWORKDIR /\nCOPY --from=builder /workspace/manager .\nUSER 65532:65532\n\nENTRYPOINT [\"/manager\"]\n"
  },
  {
    "path": "docs/api_docs/.gitignore",
    "content": "/site\n"
  },
  {
    "path": "docs/api_docs/README.md",
    "content": "# LlamaDeploy Documentation\n\nThis repository contains the documentation for LlamaIndex Workflows, built using MkDocs with Material theme.\n\n## Setup\n\n### Prerequisites\n- Python 3.10 or higher\n- uv (for dependency management)\n\n### Installation\n\n1. Clone the repository\n2. Install dependencies using uv:\n```bash\nuv sync\n```\n\n## Development\n\nTo start the documentation server locally:\n```bash\nuv run mkdocs serve\n```\n\nThis will start a development server at `http://127.0.0.1:8000`.\n\n## Building\n\nLlamaDeploy is part of LlamaIndex [documentation portal](https://docs.llamaindex.ai/)\nso the build is performed from the [main repository](https://github.com/run-llama/llama_index).\n\n> [!WARNING]\n> When a documentation change is merged here, the change won't be visible until a new\n> build is triggered from the LlamaIndex repository.\n\n\n## Contributing\n\nContributions are very welcome!\n\n1. Create a new branch for your changes\n2. Make your changes to the documentation\n3. Test locally using `uv run mkdocs serve`\n4. Submit a pull request\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/_static/css/algolia.css",
    "content": "/**\n * Skipped minification because the original files appears to be already minified.\n * Original file: /npm/@docsearch/css@3.6.1/dist/style.css\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n/*! @docsearch/css 3.6.1 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */\n:root {\n  --docsearch-primary-color: #5468ff;\n  --docsearch-text-color: #1c1e21;\n  --docsearch-spacing: 12px;\n  --docsearch-icon-stroke-width: 1.4;\n  --docsearch-highlight-color: var(--docsearch-primary-color);\n  --docsearch-muted-color: #969faf;\n  --docsearch-container-background: rgba(101, 108, 133, 0.8);\n  --docsearch-logo-color: #5468ff;\n  --docsearch-modal-width: 560px;\n  --docsearch-modal-height: 600px;\n  --docsearch-modal-background: #f5f6f7;\n  --docsearch-modal-shadow: inset 1px 1px 0 0 hsla(0, 0%, 100%, 0.5),\n    0 3px 8px 0 #555a64;\n  --docsearch-searchbox-height: 56px;\n  --docsearch-searchbox-background: #ebedf0;\n  --docsearch-searchbox-focus-background: #fff;\n  --docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);\n  --docsearch-hit-height: 56px;\n  --docsearch-hit-color: #444950;\n  --docsearch-hit-active-color: #fff;\n  --docsearch-hit-background: #fff;\n  --docsearch-hit-shadow: 0 1px 3px 0 #d4d9e1;\n  --docsearch-key-gradient: linear-gradient(-225deg, #d5dbe4, #f8f8f8);\n  --docsearch-key-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff,\n    0 1px 2px 1px rgba(30, 35, 90, 0.4);\n  --docsearch-key-pressed-shadow: inset 0 -2px 0 0 #cdcde6,\n    inset 0 0 1px 1px #fff, 0 1px 1px 0 rgba(30, 35, 90, 0.4);\n  --docsearch-footer-height: 44px;\n  --docsearch-footer-background: #fff;\n  --docsearch-footer-shadow: 0 -1px 0 0 #e0e3e8,\n    0 -3px 6px 0 rgba(69, 98, 155, 0.12);\n}\nhtml[data-theme=\"dark\"] {\n  --docsearch-text-color: #f5f6f7;\n  --docsearch-container-background: rgba(9, 10, 17, 0.8);\n  --docsearch-modal-background: #15172a;\n  --docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;\n  --docsearch-searchbox-background: #090a11;\n  --docsearch-searchbox-focus-background: #000;\n  --docsearch-hit-color: #bec3c9;\n  --docsearch-hit-shadow: none;\n  --docsearch-hit-background: #090a11;\n  --docsearch-key-gradient: linear-gradient(-26.5deg, #565872, #31355b);\n  --docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d,\n    0 2px 2px 0 rgba(3, 4, 9, 0.3);\n  --docsearch-key-pressed-shadow: inset 0 -2px 0 0 #282d55,\n    inset 0 0 1px 1px #51577d, 0 1px 1px 0 rgba(3, 4, 9, 0.30196078431372547);\n  --docsearch-footer-background: #1e2136;\n  --docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),\n    0 -4px 8px 0 rgba(0, 0, 0, 0.2);\n  --docsearch-logo-color: #fff;\n  --docsearch-muted-color: #7f8497;\n}\n.DocSearch-Button {\n  align-items: center;\n  background: var(--docsearch-searchbox-background);\n  border: 0;\n  border-radius: 40px;\n  color: var(--docsearch-muted-color);\n  cursor: pointer;\n  display: flex;\n  font-weight: 500;\n  height: 36px;\n  justify-content: space-between;\n  margin: 0 0 0 16px;\n  padding: 0 8px;\n  user-select: none;\n}\n.DocSearch-Button:active,\n.DocSearch-Button:focus,\n.DocSearch-Button:hover {\n  background: var(--docsearch-searchbox-focus-background);\n  box-shadow: var(--docsearch-searchbox-shadow);\n  color: var(--docsearch-text-color);\n  outline: none;\n}\n.DocSearch-Button-Container {\n  align-items: center;\n  display: flex;\n}\n.DocSearch-Search-Icon {\n  stroke-width: 1.6;\n}\n.DocSearch-Button .DocSearch-Search-Icon {\n  color: var(--docsearch-text-color);\n}\n.DocSearch-Button-Placeholder {\n  font-size: 1rem;\n  padding: 0 12px 0 6px;\n}\n.DocSearch-Button-Keys {\n  display: flex;\n  min-width: calc(40px + 0.8em);\n}\n.DocSearch-Button-Key {\n  align-items: center;\n  background: var(--docsearch-key-gradient);\n  border-radius: 3px;\n  box-shadow: var(--docsearch-key-shadow);\n  color: var(--docsearch-muted-color);\n  display: flex;\n  height: 18px;\n  justify-content: center;\n  margin-right: 0.4em;\n  position: relative;\n  padding: 0 0 2px;\n  border: 0;\n  top: -1px;\n  width: 20px;\n}\n.DocSearch-Button-Key--pressed {\n  transform: translate3d(0, 1px, 0);\n  box-shadow: var(--docsearch-key-pressed-shadow);\n}\n@media (max-width: 768px) {\n  .DocSearch-Button-Keys,\n  .DocSearch-Button-Placeholder {\n    display: none;\n  }\n}\n.DocSearch--active {\n  overflow: hidden !important;\n}\n.DocSearch-Container,\n.DocSearch-Container * {\n  box-sizing: border-box;\n}\n.DocSearch-Container {\n  background-color: var(--docsearch-container-background);\n  height: 100vh;\n  left: 0;\n  position: fixed;\n  top: 0;\n  width: 100vw;\n  z-index: 200;\n}\n.DocSearch-Container a {\n  text-decoration: none;\n}\n.DocSearch-Link {\n  appearance: none;\n  background: none;\n  border: 0;\n  color: var(--docsearch-highlight-color);\n  cursor: pointer;\n  font: inherit;\n  margin: 0;\n  padding: 0;\n}\n.DocSearch-Modal {\n  background: var(--docsearch-modal-background);\n  border-radius: 6px;\n  box-shadow: var(--docsearch-modal-shadow);\n  flex-direction: column;\n  margin: 60px auto auto;\n  max-width: var(--docsearch-modal-width);\n  position: relative;\n}\n.DocSearch-SearchBar {\n  display: flex;\n  padding: var(--docsearch-spacing) var(--docsearch-spacing) 0;\n}\n.DocSearch-Form {\n  align-items: center;\n  background: var(--docsearch-searchbox-focus-background);\n  border-radius: 4px;\n  box-shadow: var(--docsearch-searchbox-shadow);\n  display: flex;\n  height: var(--docsearch-searchbox-height);\n  margin: 0;\n  padding: 0 var(--docsearch-spacing);\n  position: relative;\n  width: 100%;\n}\n.DocSearch-Input {\n  appearance: none;\n  background: transparent;\n  border: 0;\n  color: var(--docsearch-text-color);\n  flex: 1;\n  font: inherit;\n  font-size: 1.2em;\n  height: 100%;\n  outline: none;\n  padding: 0 0 0 8px;\n  width: 80%;\n}\n.DocSearch-Input::placeholder {\n  color: var(--docsearch-muted-color);\n  opacity: 1;\n}\n.DocSearch-Input::-webkit-search-cancel-button,\n.DocSearch-Input::-webkit-search-decoration,\n.DocSearch-Input::-webkit-search-results-button,\n.DocSearch-Input::-webkit-search-results-decoration {\n  display: none;\n}\n.DocSearch-LoadingIndicator,\n.DocSearch-MagnifierLabel,\n.DocSearch-Reset {\n  margin: 0;\n  padding: 0;\n}\n.DocSearch-MagnifierLabel,\n.DocSearch-Reset {\n  align-items: center;\n  color: var(--docsearch-highlight-color);\n  display: flex;\n  justify-content: center;\n}\n.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,\n.DocSearch-LoadingIndicator {\n  display: none;\n}\n.DocSearch-Container--Stalled .DocSearch-LoadingIndicator {\n  align-items: center;\n  color: var(--docsearch-highlight-color);\n  display: flex;\n  justify-content: center;\n}\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Reset {\n    animation: none;\n    appearance: none;\n    background: none;\n    border: 0;\n    border-radius: 50%;\n    color: var(--docsearch-icon-color);\n    cursor: pointer;\n    right: 0;\n    stroke-width: var(--docsearch-icon-stroke-width);\n  }\n}\n.DocSearch-Reset {\n  animation: fade-in 0.1s ease-in forwards;\n  appearance: none;\n  background: none;\n  border: 0;\n  border-radius: 50%;\n  color: var(--docsearch-icon-color);\n  cursor: pointer;\n  padding: 2px;\n  right: 0;\n  stroke-width: var(--docsearch-icon-stroke-width);\n}\n.DocSearch-Reset[hidden] {\n  display: none;\n}\n.DocSearch-Reset:hover {\n  color: var(--docsearch-highlight-color);\n}\n.DocSearch-LoadingIndicator svg,\n.DocSearch-MagnifierLabel svg {\n  height: 24px;\n  width: 24px;\n}\n.DocSearch-Cancel {\n  display: none;\n}\n.DocSearch-Dropdown {\n  max-height: calc(\n    var(--docsearch-modal-height) - var(--docsearch-searchbox-height) -\n      var(--docsearch-spacing) - var(--docsearch-footer-height)\n  );\n  min-height: var(--docsearch-spacing);\n  overflow-y: auto;\n  overflow-y: overlay;\n  padding: 0 var(--docsearch-spacing);\n  scrollbar-color: var(--docsearch-muted-color)\n    var(--docsearch-modal-background);\n  scrollbar-width: thin;\n}\n.DocSearch-Dropdown::-webkit-scrollbar {\n  width: 12px;\n}\n.DocSearch-Dropdown::-webkit-scrollbar-track {\n  background: transparent;\n}\n.DocSearch-Dropdown::-webkit-scrollbar-thumb {\n  background-color: var(--docsearch-muted-color);\n  border: 3px solid var(--docsearch-modal-background);\n  border-radius: 20px;\n}\n.DocSearch-Dropdown ul {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n.DocSearch-Label {\n  font-size: 0.75em;\n  line-height: 1.6em;\n}\n.DocSearch-Help,\n.DocSearch-Label {\n  color: var(--docsearch-muted-color);\n}\n.DocSearch-Help {\n  font-size: 0.9em;\n  margin: 0;\n  user-select: none;\n}\n.DocSearch-Title {\n  font-size: 1.2em;\n}\n.DocSearch-Logo a {\n  display: flex;\n}\n.DocSearch-Logo svg {\n  color: var(--docsearch-logo-color);\n  margin-left: 8px;\n}\n.DocSearch-Hits:last-of-type {\n  margin-bottom: 24px;\n}\n.DocSearch-Hits mark {\n  background: none;\n  color: var(--docsearch-highlight-color);\n}\n.DocSearch-HitsFooter {\n  color: var(--docsearch-muted-color);\n  display: flex;\n  font-size: 0.85em;\n  justify-content: center;\n  margin-bottom: var(--docsearch-spacing);\n  padding: var(--docsearch-spacing);\n}\n.DocSearch-HitsFooter a {\n  border-bottom: 1px solid;\n  color: inherit;\n}\n.DocSearch-Hit {\n  border-radius: 4px;\n  display: flex;\n  padding-bottom: 4px;\n  position: relative;\n}\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit--deleting {\n    transition: none;\n  }\n}\n.DocSearch-Hit--deleting {\n  opacity: 0;\n  transition: all 0.25s linear;\n}\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit--favoriting {\n    transition: none;\n  }\n}\n.DocSearch-Hit--favoriting {\n  transform: scale(0);\n  transform-origin: top center;\n  transition: all 0.25s linear;\n  transition-delay: 0.25s;\n}\n.DocSearch-Hit a {\n  background: var(--docsearch-hit-background);\n  border-radius: 4px;\n  box-shadow: var(--docsearch-hit-shadow);\n  display: block;\n  padding-left: var(--docsearch-spacing);\n  width: 100%;\n}\n.DocSearch-Hit-source {\n  background: var(--docsearch-modal-background);\n  color: var(--docsearch-highlight-color);\n  font-size: 0.85em;\n  font-weight: 600;\n  line-height: 32px;\n  margin: 0 -4px;\n  padding: 8px 4px 0;\n  position: sticky;\n  top: 0;\n  z-index: 10;\n}\n.DocSearch-Hit-Tree {\n  color: var(--docsearch-muted-color);\n  height: var(--docsearch-hit-height);\n  opacity: 0.5;\n  stroke-width: var(--docsearch-icon-stroke-width);\n  width: 24px;\n}\n.DocSearch-Hit[aria-selected=\"true\"] a {\n  background-color: var(--docsearch-highlight-color);\n}\n.DocSearch-Hit[aria-selected=\"true\"] mark {\n  text-decoration: underline;\n}\n.DocSearch-Hit-Container {\n  align-items: center;\n  color: var(--docsearch-hit-color);\n  display: flex;\n  flex-direction: row;\n  height: var(--docsearch-hit-height);\n  padding: 0 var(--docsearch-spacing) 0 0;\n}\n.DocSearch-Hit-icon {\n  height: 20px;\n  width: 20px;\n}\n.DocSearch-Hit-action,\n.DocSearch-Hit-icon {\n  color: var(--docsearch-muted-color);\n  stroke-width: var(--docsearch-icon-stroke-width);\n}\n.DocSearch-Hit-action {\n  align-items: center;\n  display: flex;\n  height: 22px;\n  width: 22px;\n}\n.DocSearch-Hit-action svg {\n  display: block;\n  height: 18px;\n  width: 18px;\n}\n.DocSearch-Hit-action + .DocSearch-Hit-action {\n  margin-left: 6px;\n}\n.DocSearch-Hit-action-button {\n  appearance: none;\n  background: none;\n  border: 0;\n  border-radius: 50%;\n  color: inherit;\n  cursor: pointer;\n  padding: 2px;\n}\nsvg.DocSearch-Hit-Select-Icon {\n  display: none;\n}\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-Select-Icon {\n  display: block;\n}\n.DocSearch-Hit-action-button:focus,\n.DocSearch-Hit-action-button:hover {\n  background: rgba(0, 0, 0, 0.2);\n  transition: background-color 0.1s ease-in;\n}\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit-action-button:focus,\n  .DocSearch-Hit-action-button:hover {\n    transition: none;\n  }\n}\n.DocSearch-Hit-action-button:focus path,\n.DocSearch-Hit-action-button:hover path {\n  fill: #fff;\n}\n.DocSearch-Hit-content-wrapper {\n  display: flex;\n  flex: 1 1 auto;\n  flex-direction: column;\n  font-weight: 500;\n  justify-content: center;\n  line-height: 1.2em;\n  margin: 0 8px;\n  overflow-x: hidden;\n  position: relative;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  width: 80%;\n}\n.DocSearch-Hit-title {\n  font-size: 0.9em;\n}\n.DocSearch-Hit-path {\n  color: var(--docsearch-muted-color);\n  font-size: 0.75em;\n}\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-action,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-icon,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-path,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-text,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-title,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-Tree,\n.DocSearch-Hit[aria-selected=\"true\"] mark {\n  color: var(--docsearch-hit-active-color) !important;\n}\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit-action-button:focus,\n  .DocSearch-Hit-action-button:hover {\n    background: rgba(0, 0, 0, 0.2);\n    transition: none;\n  }\n}\n.DocSearch-ErrorScreen,\n.DocSearch-NoResults,\n.DocSearch-StartScreen {\n  font-size: 0.9em;\n  margin: 0 auto;\n  padding: 36px 0;\n  text-align: center;\n  width: 80%;\n}\n.DocSearch-Screen-Icon {\n  color: var(--docsearch-muted-color);\n  padding-bottom: 12px;\n}\n.DocSearch-NoResults-Prefill-List {\n  display: inline-block;\n  padding-bottom: 24px;\n  text-align: left;\n}\n.DocSearch-NoResults-Prefill-List ul {\n  display: inline-block;\n  padding: 8px 0 0;\n}\n.DocSearch-NoResults-Prefill-List li {\n  list-style-position: inside;\n  list-style-type: \"» \";\n}\n.DocSearch-Prefill {\n  appearance: none;\n  background: none;\n  border: 0;\n  border-radius: 1em;\n  color: var(--docsearch-highlight-color);\n  cursor: pointer;\n  display: inline-block;\n  font-size: 1em;\n  font-weight: 700;\n  padding: 0;\n}\n.DocSearch-Prefill:focus,\n.DocSearch-Prefill:hover {\n  outline: none;\n  text-decoration: underline;\n}\n.DocSearch-Footer {\n  align-items: center;\n  background: var(--docsearch-footer-background);\n  border-radius: 0 0 8px 8px;\n  box-shadow: var(--docsearch-footer-shadow);\n  display: flex;\n  flex-direction: row-reverse;\n  flex-shrink: 0;\n  height: var(--docsearch-footer-height);\n  justify-content: space-between;\n  padding: 0 var(--docsearch-spacing);\n  position: relative;\n  user-select: none;\n  width: 100%;\n  z-index: 300;\n}\n.DocSearch-Commands {\n  color: var(--docsearch-muted-color);\n  display: flex;\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n.DocSearch-Commands li {\n  align-items: center;\n  display: flex;\n}\n.DocSearch-Commands li:not(:last-of-type) {\n  margin-right: 0.8em;\n}\n.DocSearch-Commands-Key {\n  align-items: center;\n  background: var(--docsearch-key-gradient);\n  border-radius: 2px;\n  box-shadow: var(--docsearch-key-shadow);\n  display: flex;\n  height: 18px;\n  justify-content: center;\n  margin-right: 0.4em;\n  padding: 0 0 1px;\n  color: var(--docsearch-muted-color);\n  border: 0;\n  width: 20px;\n}\n.DocSearch-VisuallyHiddenForAccessibility {\n  clip: rect(0 0 0 0);\n  clip-path: inset(50%);\n  height: 1px;\n  overflow: hidden;\n  position: absolute;\n  white-space: nowrap;\n  width: 1px;\n}\n@media (max-width: 768px) {\n  :root {\n    --docsearch-spacing: 10px;\n    --docsearch-footer-height: 40px;\n  }\n  .DocSearch-Dropdown {\n    height: 100%;\n  }\n  .DocSearch-Container {\n    height: 100vh;\n    height: -webkit-fill-available;\n    height: calc(var(--docsearch-vh, 1vh) * 100);\n    position: absolute;\n  }\n  .DocSearch-Footer {\n    border-radius: 0;\n    bottom: 0;\n    position: absolute;\n  }\n  .DocSearch-Hit-content-wrapper {\n    display: flex;\n    position: relative;\n    width: 80%;\n  }\n  .DocSearch-Modal {\n    border-radius: 0;\n    box-shadow: none;\n    height: 100vh;\n    height: -webkit-fill-available;\n    height: calc(var(--docsearch-vh, 1vh) * 100);\n    margin: 0;\n    max-width: 100%;\n    width: 100%;\n  }\n  .DocSearch-Dropdown {\n    max-height: calc(\n      var(--docsearch-vh, 1vh) * 100 - var(--docsearch-searchbox-height) -\n        var(--docsearch-spacing) - var(--docsearch-footer-height)\n    );\n  }\n  .DocSearch-Cancel {\n    appearance: none;\n    background: none;\n    border: 0;\n    color: var(--docsearch-highlight-color);\n    cursor: pointer;\n    display: inline-block;\n    flex: none;\n    font: inherit;\n    font-size: 1em;\n    font-weight: 500;\n    margin-left: var(--docsearch-spacing);\n    outline: none;\n    overflow: hidden;\n    padding: 0;\n    user-select: none;\n    white-space: nowrap;\n  }\n  .DocSearch-Commands,\n  .DocSearch-Hit-Tree {\n    display: none;\n  }\n}\n@keyframes fade-in {\n  0% {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/_static/css/custom.css",
    "content": "#my-component-root *,\n#headlessui-portal-root * {\n  z-index: 1000000000000;\n  font-size: 100%;\n}\n\ntextarea {\n  border: 0;\n  padding: 0;\n}\n\narticle p {\n  margin-bottom: 10px !important;\n}\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/_static/js/algolia.js",
    "content": "/**\n * Skipped minification because the original files appears to be already minified.\n * Original file: /npm/@docsearch/js@3.6.1/dist/umd/index.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n/*! @docsearch/js 3.6.1 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */\n!(function (e, t) {\n  \"object\" == typeof exports && \"undefined\" != typeof module\n    ? (module.exports = t())\n    : \"function\" == typeof define && define.amd\n    ? define(t)\n    : ((e = e || self).docsearch = t());\n})(this, function () {\n  \"use strict\";\n  function e(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function t(t) {\n    for (var n = 1; n < arguments.length; n++) {\n      var o = null != arguments[n] ? arguments[n] : {};\n      n % 2\n        ? e(Object(o), !0).forEach(function (e) {\n            r(t, e, o[e]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(t, Object.getOwnPropertyDescriptors(o))\n        : e(Object(o)).forEach(function (e) {\n            Object.defineProperty(t, e, Object.getOwnPropertyDescriptor(o, e));\n          });\n    }\n    return t;\n  }\n  function n(e) {\n    return (\n      (n =\n        \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator\n          ? function (e) {\n              return typeof e;\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : typeof e;\n            }),\n      n(e)\n    );\n  }\n  function r(e, t, n) {\n    return (\n      t in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function o() {\n    return (\n      (o =\n        Object.assign ||\n        function (e) {\n          for (var t = 1; t < arguments.length; t++) {\n            var n = arguments[t];\n            for (var r in n)\n              Object.prototype.hasOwnProperty.call(n, r) && (e[r] = n[r]);\n          }\n          return e;\n        }),\n      o.apply(this, arguments)\n    );\n  }\n  function i(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function c(e, t) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return e;\n      })(e) ||\n      (function (e, t) {\n        var n =\n          null == e\n            ? null\n            : (\"undefined\" != typeof Symbol && e[Symbol.iterator]) ||\n              e[\"@@iterator\"];\n        if (null == n) return;\n        var r,\n          o,\n          i = [],\n          c = !0,\n          a = !1;\n        try {\n          for (\n            n = n.call(e);\n            !(c = (r = n.next()).done) &&\n            (i.push(r.value), !t || i.length !== t);\n            c = !0\n          );\n        } catch (e) {\n          (a = !0), (o = e);\n        } finally {\n          try {\n            c || null == n.return || n.return();\n          } finally {\n            if (a) throw o;\n          }\n        }\n        return i;\n      })(e, t) ||\n      u(e, t) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function a(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return l(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      u(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function u(e, t) {\n    if (e) {\n      if (\"string\" == typeof e) return l(e, t);\n      var n = Object.prototype.toString.call(e).slice(8, -1);\n      return (\n        \"Object\" === n && e.constructor && (n = e.constructor.name),\n        \"Map\" === n || \"Set\" === n\n          ? Array.from(e)\n          : \"Arguments\" === n ||\n            /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n          ? l(e, t)\n          : void 0\n      );\n    }\n  }\n  function l(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  var s,\n    f,\n    p,\n    m,\n    d,\n    v = {},\n    h = [],\n    y = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;\n  function _(e, t) {\n    for (var n in t) e[n] = t[n];\n    return e;\n  }\n  function b(e) {\n    var t = e.parentNode;\n    t && t.removeChild(e);\n  }\n  function g(e, t, n) {\n    var r,\n      o,\n      i,\n      c = arguments,\n      a = {};\n    for (i in t)\n      \"key\" == i ? (r = t[i]) : \"ref\" == i ? (o = t[i]) : (a[i] = t[i]);\n    if (arguments.length > 3)\n      for (n = [n], i = 3; i < arguments.length; i++) n.push(c[i]);\n    if (\n      (null != n && (a.children = n),\n      \"function\" == typeof e && null != e.defaultProps)\n    )\n      for (i in e.defaultProps) void 0 === a[i] && (a[i] = e.defaultProps[i]);\n    return S(e, a, r, o, null);\n  }\n  function S(e, t, n, r, o) {\n    var i = {\n      type: e,\n      props: t,\n      key: n,\n      ref: r,\n      __k: null,\n      __: null,\n      __b: 0,\n      __e: null,\n      __d: void 0,\n      __c: null,\n      __h: null,\n      constructor: void 0,\n      __v: null == o ? ++s.__v : o,\n    };\n    return null != s.vnode && s.vnode(i), i;\n  }\n  function O(e) {\n    return e.children;\n  }\n  function w(e, t) {\n    (this.props = e), (this.context = t);\n  }\n  function E(e, t) {\n    if (null == t) return e.__ ? E(e.__, e.__.__k.indexOf(e) + 1) : null;\n    for (var n; t < e.__k.length; t++)\n      if (null != (n = e.__k[t]) && null != n.__e) return n.__e;\n    return \"function\" == typeof e.type ? E(e) : null;\n  }\n  function j(e) {\n    var t, n;\n    if (null != (e = e.__) && null != e.__c) {\n      for (e.__e = e.__c.base = null, t = 0; t < e.__k.length; t++)\n        if (null != (n = e.__k[t]) && null != n.__e) {\n          e.__e = e.__c.base = n.__e;\n          break;\n        }\n      return j(e);\n    }\n  }\n  function P(e) {\n    ((!e.__d && (e.__d = !0) && f.push(e) && !I.__r++) ||\n      m !== s.debounceRendering) &&\n      ((m = s.debounceRendering) || p)(I);\n  }\n  function I() {\n    for (var e; (I.__r = f.length); )\n      (e = f.sort(function (e, t) {\n        return e.__v.__b - t.__v.__b;\n      })),\n        (f = []),\n        e.some(function (e) {\n          var t, n, r, o, i, c;\n          e.__d &&\n            ((i = (o = (t = e).__v).__e),\n            (c = t.__P) &&\n              ((n = []),\n              ((r = _({}, o)).__v = o.__v + 1),\n              q(\n                c,\n                o,\n                r,\n                t.__n,\n                void 0 !== c.ownerSVGElement,\n                null != o.__h ? [i] : null,\n                n,\n                null == i ? E(o) : i,\n                o.__h,\n              ),\n              L(n, o),\n              o.__e != i && j(o)));\n        });\n  }\n  function D(e, t, n, r, o, i, c, a, u, l) {\n    var s,\n      f,\n      p,\n      m,\n      d,\n      y,\n      _,\n      b = (r && r.__k) || h,\n      g = b.length;\n    for (n.__k = [], s = 0; s < t.length; s++)\n      if (\n        null !=\n        (m = n.__k[s] =\n          null == (m = t[s]) || \"boolean\" == typeof m\n            ? null\n            : \"string\" == typeof m || \"number\" == typeof m\n            ? S(null, m, null, null, m)\n            : Array.isArray(m)\n            ? S(O, { children: m }, null, null, null)\n            : m.__b > 0\n            ? S(m.type, m.props, m.key, null, m.__v)\n            : m)\n      ) {\n        if (\n          ((m.__ = n),\n          (m.__b = n.__b + 1),\n          null === (p = b[s]) || (p && m.key == p.key && m.type === p.type))\n        )\n          b[s] = void 0;\n        else\n          for (f = 0; f < g; f++) {\n            if ((p = b[f]) && m.key == p.key && m.type === p.type) {\n              b[f] = void 0;\n              break;\n            }\n            p = null;\n          }\n        q(e, m, (p = p || v), o, i, c, a, u, l),\n          (d = m.__e),\n          (f = m.ref) &&\n            p.ref != f &&\n            (_ || (_ = []),\n            p.ref && _.push(p.ref, null, m),\n            _.push(f, m.__c || d, m)),\n          null != d\n            ? (null == y && (y = d),\n              \"function\" == typeof m.type && null != m.__k && m.__k === p.__k\n                ? (m.__d = u = k(m, u, e))\n                : (u = A(e, m, p, b, d, u)),\n              l || \"option\" !== n.type\n                ? \"function\" == typeof n.type && (n.__d = u)\n                : (e.value = \"\"))\n            : u && p.__e == u && u.parentNode != e && (u = E(p));\n      }\n    for (n.__e = y, s = g; s--; )\n      null != b[s] &&\n        (\"function\" == typeof n.type &&\n          null != b[s].__e &&\n          b[s].__e == n.__d &&\n          (n.__d = E(r, s + 1)),\n        U(b[s], b[s]));\n    if (_) for (s = 0; s < _.length; s++) H(_[s], _[++s], _[++s]);\n  }\n  function k(e, t, n) {\n    var r, o;\n    for (r = 0; r < e.__k.length; r++)\n      (o = e.__k[r]) &&\n        ((o.__ = e),\n        (t =\n          \"function\" == typeof o.type\n            ? k(o, t, n)\n            : A(n, o, o, e.__k, o.__e, t)));\n    return t;\n  }\n  function C(e, t) {\n    return (\n      (t = t || []),\n      null == e ||\n        \"boolean\" == typeof e ||\n        (Array.isArray(e)\n          ? e.some(function (e) {\n              C(e, t);\n            })\n          : t.push(e)),\n      t\n    );\n  }\n  function A(e, t, n, r, o, i) {\n    var c, a, u;\n    if (void 0 !== t.__d) (c = t.__d), (t.__d = void 0);\n    else if (null == n || o != i || null == o.parentNode)\n      e: if (null == i || i.parentNode !== e) e.appendChild(o), (c = null);\n      else {\n        for (a = i, u = 0; (a = a.nextSibling) && u < r.length; u += 2)\n          if (a == o) break e;\n        e.insertBefore(o, i), (c = i);\n      }\n    return void 0 !== c ? c : o.nextSibling;\n  }\n  function x(e, t, n) {\n    \"-\" === t[0]\n      ? e.setProperty(t, n)\n      : (e[t] =\n          null == n ? \"\" : \"number\" != typeof n || y.test(t) ? n : n + \"px\");\n  }\n  function N(e, t, n, r, o) {\n    var i;\n    e: if (\"style\" === t)\n      if (\"string\" == typeof n) e.style.cssText = n;\n      else {\n        if ((\"string\" == typeof r && (e.style.cssText = r = \"\"), r))\n          for (t in r) (n && t in n) || x(e.style, t, \"\");\n        if (n) for (t in n) (r && n[t] === r[t]) || x(e.style, t, n[t]);\n      }\n    else if (\"o\" === t[0] && \"n\" === t[1])\n      (i = t !== (t = t.replace(/Capture$/, \"\"))),\n        (t = t.toLowerCase() in e ? t.toLowerCase().slice(2) : t.slice(2)),\n        e.l || (e.l = {}),\n        (e.l[t + i] = n),\n        n\n          ? r || e.addEventListener(t, i ? R : T, i)\n          : e.removeEventListener(t, i ? R : T, i);\n    else if (\"dangerouslySetInnerHTML\" !== t) {\n      if (o) t = t.replace(/xlink[H:h]/, \"h\").replace(/sName$/, \"s\");\n      else if (\n        \"href\" !== t &&\n        \"list\" !== t &&\n        \"form\" !== t &&\n        \"download\" !== t &&\n        t in e\n      )\n        try {\n          e[t] = null == n ? \"\" : n;\n          break e;\n        } catch (e) {}\n      \"function\" == typeof n ||\n        (null != n && (!1 !== n || (\"a\" === t[0] && \"r\" === t[1]))\n          ? e.setAttribute(t, n)\n          : e.removeAttribute(t));\n    }\n  }\n  function T(e) {\n    this.l[e.type + !1](s.event ? s.event(e) : e);\n  }\n  function R(e) {\n    this.l[e.type + !0](s.event ? s.event(e) : e);\n  }\n  function q(e, t, n, r, o, i, c, a, u) {\n    var l,\n      f,\n      p,\n      m,\n      d,\n      v,\n      h,\n      y,\n      b,\n      g,\n      S,\n      E = t.type;\n    if (void 0 !== t.constructor) return null;\n    null != n.__h &&\n      ((u = n.__h), (a = t.__e = n.__e), (t.__h = null), (i = [a])),\n      (l = s.__b) && l(t);\n    try {\n      e: if (\"function\" == typeof E) {\n        if (\n          ((y = t.props),\n          (b = (l = E.contextType) && r[l.__c]),\n          (g = l ? (b ? b.props.value : l.__) : r),\n          n.__c\n            ? (h = (f = t.__c = n.__c).__ = f.__E)\n            : (\"prototype\" in E && E.prototype.render\n                ? (t.__c = f = new E(y, g))\n                : ((t.__c = f = new w(y, g)),\n                  (f.constructor = E),\n                  (f.render = F)),\n              b && b.sub(f),\n              (f.props = y),\n              f.state || (f.state = {}),\n              (f.context = g),\n              (f.__n = r),\n              (p = f.__d = !0),\n              (f.__h = [])),\n          null == f.__s && (f.__s = f.state),\n          null != E.getDerivedStateFromProps &&\n            (f.__s == f.state && (f.__s = _({}, f.__s)),\n            _(f.__s, E.getDerivedStateFromProps(y, f.__s))),\n          (m = f.props),\n          (d = f.state),\n          p)\n        )\n          null == E.getDerivedStateFromProps &&\n            null != f.componentWillMount &&\n            f.componentWillMount(),\n            null != f.componentDidMount && f.__h.push(f.componentDidMount);\n        else {\n          if (\n            (null == E.getDerivedStateFromProps &&\n              y !== m &&\n              null != f.componentWillReceiveProps &&\n              f.componentWillReceiveProps(y, g),\n            (!f.__e &&\n              null != f.shouldComponentUpdate &&\n              !1 === f.shouldComponentUpdate(y, f.__s, g)) ||\n              t.__v === n.__v)\n          ) {\n            (f.props = y),\n              (f.state = f.__s),\n              t.__v !== n.__v && (f.__d = !1),\n              (f.__v = t),\n              (t.__e = n.__e),\n              (t.__k = n.__k),\n              f.__h.length && c.push(f);\n            break e;\n          }\n          null != f.componentWillUpdate && f.componentWillUpdate(y, f.__s, g),\n            null != f.componentDidUpdate &&\n              f.__h.push(function () {\n                f.componentDidUpdate(m, d, v);\n              });\n        }\n        (f.context = g),\n          (f.props = y),\n          (f.state = f.__s),\n          (l = s.__r) && l(t),\n          (f.__d = !1),\n          (f.__v = t),\n          (f.__P = e),\n          (l = f.render(f.props, f.state, f.context)),\n          (f.state = f.__s),\n          null != f.getChildContext && (r = _(_({}, r), f.getChildContext())),\n          p ||\n            null == f.getSnapshotBeforeUpdate ||\n            (v = f.getSnapshotBeforeUpdate(m, d)),\n          (S =\n            null != l && l.type === O && null == l.key ? l.props.children : l),\n          D(e, Array.isArray(S) ? S : [S], t, n, r, o, i, c, a, u),\n          (f.base = t.__e),\n          (t.__h = null),\n          f.__h.length && c.push(f),\n          h && (f.__E = f.__ = null),\n          (f.__e = !1);\n      } else\n        null == i && t.__v === n.__v\n          ? ((t.__k = n.__k), (t.__e = n.__e))\n          : (t.__e = M(n.__e, t, n, r, o, i, c, u));\n      (l = s.diffed) && l(t);\n    } catch (e) {\n      (t.__v = null),\n        (u || null != i) &&\n          ((t.__e = a), (t.__h = !!u), (i[i.indexOf(a)] = null)),\n        s.__e(e, t, n);\n    }\n  }\n  function L(e, t) {\n    s.__c && s.__c(t, e),\n      e.some(function (t) {\n        try {\n          (e = t.__h),\n            (t.__h = []),\n            e.some(function (e) {\n              e.call(t);\n            });\n        } catch (e) {\n          s.__e(e, t.__v);\n        }\n      });\n  }\n  function M(e, t, n, r, o, i, c, a) {\n    var u,\n      l,\n      s,\n      f,\n      p = n.props,\n      m = t.props,\n      d = t.type,\n      y = 0;\n    if ((\"svg\" === d && (o = !0), null != i))\n      for (; y < i.length; y++)\n        if (\n          (u = i[y]) &&\n          (u === e || (d ? u.localName == d : 3 == u.nodeType))\n        ) {\n          (e = u), (i[y] = null);\n          break;\n        }\n    if (null == e) {\n      if (null === d) return document.createTextNode(m);\n      (e = o\n        ? document.createElementNS(\"http://www.w3.org/2000/svg\", d)\n        : document.createElement(d, m.is && m)),\n        (i = null),\n        (a = !1);\n    }\n    if (null === d) p === m || (a && e.data === m) || (e.data = m);\n    else {\n      if (\n        ((i = i && h.slice.call(e.childNodes)),\n        (l = (p = n.props || v).dangerouslySetInnerHTML),\n        (s = m.dangerouslySetInnerHTML),\n        !a)\n      ) {\n        if (null != i)\n          for (p = {}, f = 0; f < e.attributes.length; f++)\n            p[e.attributes[f].name] = e.attributes[f].value;\n        (s || l) &&\n          ((s && ((l && s.__html == l.__html) || s.__html === e.innerHTML)) ||\n            (e.innerHTML = (s && s.__html) || \"\"));\n      }\n      if (\n        ((function (e, t, n, r, o) {\n          var i;\n          for (i in n)\n            \"children\" === i || \"key\" === i || i in t || N(e, i, null, n[i], r);\n          for (i in t)\n            (o && \"function\" != typeof t[i]) ||\n              \"children\" === i ||\n              \"key\" === i ||\n              \"value\" === i ||\n              \"checked\" === i ||\n              n[i] === t[i] ||\n              N(e, i, t[i], n[i], r);\n        })(e, m, p, o, a),\n        s)\n      )\n        t.__k = [];\n      else if (\n        ((y = t.props.children),\n        D(\n          e,\n          Array.isArray(y) ? y : [y],\n          t,\n          n,\n          r,\n          o && \"foreignObject\" !== d,\n          i,\n          c,\n          e.firstChild,\n          a,\n        ),\n        null != i)\n      )\n        for (y = i.length; y--; ) null != i[y] && b(i[y]);\n      a ||\n        (\"value\" in m &&\n          void 0 !== (y = m.value) &&\n          (y !== e.value || (\"progress\" === d && !y)) &&\n          N(e, \"value\", y, p.value, !1),\n        \"checked\" in m &&\n          void 0 !== (y = m.checked) &&\n          y !== e.checked &&\n          N(e, \"checked\", y, p.checked, !1));\n    }\n    return e;\n  }\n  function H(e, t, n) {\n    try {\n      \"function\" == typeof e ? e(t) : (e.current = t);\n    } catch (e) {\n      s.__e(e, n);\n    }\n  }\n  function U(e, t, n) {\n    var r, o, i;\n    if (\n      (s.unmount && s.unmount(e),\n      (r = e.ref) && ((r.current && r.current !== e.__e) || H(r, null, t)),\n      n || \"function\" == typeof e.type || (n = null != (o = e.__e)),\n      (e.__e = e.__d = void 0),\n      null != (r = e.__c))\n    ) {\n      if (r.componentWillUnmount)\n        try {\n          r.componentWillUnmount();\n        } catch (e) {\n          s.__e(e, t);\n        }\n      r.base = r.__P = null;\n    }\n    if ((r = e.__k)) for (i = 0; i < r.length; i++) r[i] && U(r[i], t, n);\n    null != o && b(o);\n  }\n  function F(e, t, n) {\n    return this.constructor(e, n);\n  }\n  function B(e, t, n) {\n    var r, o, i;\n    s.__ && s.__(e, t),\n      (o = (r = \"function\" == typeof n) ? null : (n && n.__k) || t.__k),\n      (i = []),\n      q(\n        t,\n        (e = ((!r && n) || t).__k = g(O, null, [e])),\n        o || v,\n        v,\n        void 0 !== t.ownerSVGElement,\n        !r && n\n          ? [n]\n          : o\n          ? null\n          : t.firstChild\n          ? h.slice.call(t.childNodes)\n          : null,\n        i,\n        !r && n ? n : o ? o.__e : t.firstChild,\n        r,\n      ),\n      L(i, e);\n  }\n  function V(e, t) {\n    B(e, t, V);\n  }\n  function K(e, t, n) {\n    var r,\n      o,\n      i,\n      c = arguments,\n      a = _({}, e.props);\n    for (i in t)\n      \"key\" == i ? (r = t[i]) : \"ref\" == i ? (o = t[i]) : (a[i] = t[i]);\n    if (arguments.length > 3)\n      for (n = [n], i = 3; i < arguments.length; i++) n.push(c[i]);\n    return (\n      null != n && (a.children = n), S(e.type, a, r || e.key, o || e.ref, null)\n    );\n  }\n  (s = {\n    __e: function (e, t) {\n      for (var n, r, o; (t = t.__); )\n        if ((n = t.__c) && !n.__)\n          try {\n            if (\n              ((r = n.constructor) &&\n                null != r.getDerivedStateFromError &&\n                (n.setState(r.getDerivedStateFromError(e)), (o = n.__d)),\n              null != n.componentDidCatch &&\n                (n.componentDidCatch(e), (o = n.__d)),\n              o)\n            )\n              return (n.__E = n);\n          } catch (t) {\n            e = t;\n          }\n      throw e;\n    },\n    __v: 0,\n  }),\n    (w.prototype.setState = function (e, t) {\n      var n;\n      (n =\n        null != this.__s && this.__s !== this.state\n          ? this.__s\n          : (this.__s = _({}, this.state))),\n        \"function\" == typeof e && (e = e(_({}, n), this.props)),\n        e && _(n, e),\n        null != e && this.__v && (t && this.__h.push(t), P(this));\n    }),\n    (w.prototype.forceUpdate = function (e) {\n      this.__v && ((this.__e = !0), e && this.__h.push(e), P(this));\n    }),\n    (w.prototype.render = O),\n    (f = []),\n    (p =\n      \"function\" == typeof Promise\n        ? Promise.prototype.then.bind(Promise.resolve())\n        : setTimeout),\n    (I.__r = 0),\n    (d = 0);\n  var W,\n    z,\n    J,\n    $ = 0,\n    Z = [],\n    Q = s.__b,\n    Y = s.__r,\n    G = s.diffed,\n    X = s.__c,\n    ee = s.unmount;\n  function te(e, t) {\n    s.__h && s.__h(z, e, $ || t), ($ = 0);\n    var n = z.__H || (z.__H = { __: [], __h: [] });\n    return e >= n.__.length && n.__.push({}), n.__[e];\n  }\n  function ne(e) {\n    return ($ = 1), re(pe, e);\n  }\n  function re(e, t, n) {\n    var r = te(W++, 2);\n    return (\n      (r.t = e),\n      r.__c ||\n        ((r.__ = [\n          n ? n(t) : pe(void 0, t),\n          function (e) {\n            var t = r.t(r.__[0], e);\n            r.__[0] !== t && ((r.__ = [t, r.__[1]]), r.__c.setState({}));\n          },\n        ]),\n        (r.__c = z)),\n      r.__\n    );\n  }\n  function oe(e, t) {\n    var n = te(W++, 3);\n    !s.__s && fe(n.__H, t) && ((n.__ = e), (n.__H = t), z.__H.__h.push(n));\n  }\n  function ie(e, t) {\n    var n = te(W++, 4);\n    !s.__s && fe(n.__H, t) && ((n.__ = e), (n.__H = t), z.__h.push(n));\n  }\n  function ce(e, t) {\n    var n = te(W++, 7);\n    return fe(n.__H, t) && ((n.__ = e()), (n.__H = t), (n.__h = e)), n.__;\n  }\n  function ae() {\n    Z.forEach(function (e) {\n      if (e.__P)\n        try {\n          e.__H.__h.forEach(le), e.__H.__h.forEach(se), (e.__H.__h = []);\n        } catch (t) {\n          (e.__H.__h = []), s.__e(t, e.__v);\n        }\n    }),\n      (Z = []);\n  }\n  (s.__b = function (e) {\n    (z = null), Q && Q(e);\n  }),\n    (s.__r = function (e) {\n      Y && Y(e), (W = 0);\n      var t = (z = e.__c).__H;\n      t && (t.__h.forEach(le), t.__h.forEach(se), (t.__h = []));\n    }),\n    (s.diffed = function (e) {\n      G && G(e);\n      var t = e.__c;\n      t &&\n        t.__H &&\n        t.__H.__h.length &&\n        ((1 !== Z.push(t) && J === s.requestAnimationFrame) ||\n          (\n            (J = s.requestAnimationFrame) ||\n            function (e) {\n              var t,\n                n = function () {\n                  clearTimeout(r), ue && cancelAnimationFrame(t), setTimeout(e);\n                },\n                r = setTimeout(n, 100);\n              ue && (t = requestAnimationFrame(n));\n            }\n          )(ae)),\n        (z = void 0);\n    }),\n    (s.__c = function (e, t) {\n      t.some(function (e) {\n        try {\n          e.__h.forEach(le),\n            (e.__h = e.__h.filter(function (e) {\n              return !e.__ || se(e);\n            }));\n        } catch (n) {\n          t.some(function (e) {\n            e.__h && (e.__h = []);\n          }),\n            (t = []),\n            s.__e(n, e.__v);\n        }\n      }),\n        X && X(e, t);\n    }),\n    (s.unmount = function (e) {\n      ee && ee(e);\n      var t = e.__c;\n      if (t && t.__H)\n        try {\n          t.__H.__.forEach(le);\n        } catch (e) {\n          s.__e(e, t.__v);\n        }\n    });\n  var ue = \"function\" == typeof requestAnimationFrame;\n  function le(e) {\n    var t = z;\n    \"function\" == typeof e.__c && e.__c(), (z = t);\n  }\n  function se(e) {\n    var t = z;\n    (e.__c = e.__()), (z = t);\n  }\n  function fe(e, t) {\n    return (\n      !e ||\n      e.length !== t.length ||\n      t.some(function (t, n) {\n        return t !== e[n];\n      })\n    );\n  }\n  function pe(e, t) {\n    return \"function\" == typeof t ? t(e) : t;\n  }\n  function me(e, t) {\n    for (var n in t) e[n] = t[n];\n    return e;\n  }\n  function de(e, t) {\n    for (var n in e) if (\"__source\" !== n && !(n in t)) return !0;\n    for (var r in t) if (\"__source\" !== r && e[r] !== t[r]) return !0;\n    return !1;\n  }\n  function ve(e) {\n    this.props = e;\n  }\n  ((ve.prototype = new w()).isPureReactComponent = !0),\n    (ve.prototype.shouldComponentUpdate = function (e, t) {\n      return de(this.props, e) || de(this.state, t);\n    });\n  var he = s.__b;\n  s.__b = function (e) {\n    e.type && e.type.__f && e.ref && ((e.props.ref = e.ref), (e.ref = null)),\n      he && he(e);\n  };\n  var ye =\n    (\"undefined\" != typeof Symbol &&\n      Symbol.for &&\n      Symbol.for(\"react.forward_ref\")) ||\n    3911;\n  var _e = function (e, t) {\n      return null == e ? null : C(C(e).map(t));\n    },\n    be = {\n      map: _e,\n      forEach: _e,\n      count: function (e) {\n        return e ? C(e).length : 0;\n      },\n      only: function (e) {\n        var t = C(e);\n        if (1 !== t.length) throw \"Children.only\";\n        return t[0];\n      },\n      toArray: C,\n    },\n    ge = s.__e;\n  function Se() {\n    (this.__u = 0), (this.t = null), (this.__b = null);\n  }\n  function Oe(e) {\n    var t = e.__.__c;\n    return t && t.__e && t.__e(e);\n  }\n  function we() {\n    (this.u = null), (this.o = null);\n  }\n  (s.__e = function (e, t, n) {\n    if (e.then)\n      for (var r, o = t; (o = o.__); )\n        if ((r = o.__c) && r.__c)\n          return (\n            null == t.__e && ((t.__e = n.__e), (t.__k = n.__k)), r.__c(e, t)\n          );\n    ge(e, t, n);\n  }),\n    ((Se.prototype = new w()).__c = function (e, t) {\n      var n = t.__c,\n        r = this;\n      null == r.t && (r.t = []), r.t.push(n);\n      var o = Oe(r.__v),\n        i = !1,\n        c = function () {\n          i || ((i = !0), (n.componentWillUnmount = n.__c), o ? o(a) : a());\n        };\n      (n.__c = n.componentWillUnmount),\n        (n.componentWillUnmount = function () {\n          c(), n.__c && n.__c();\n        });\n      var a = function () {\n          if (!--r.__u) {\n            if (r.state.__e) {\n              var e = r.state.__e;\n              r.__v.__k[0] = (function e(t, n, r) {\n                return (\n                  t &&\n                    ((t.__v = null),\n                    (t.__k =\n                      t.__k &&\n                      t.__k.map(function (t) {\n                        return e(t, n, r);\n                      })),\n                    t.__c &&\n                      t.__c.__P === n &&\n                      (t.__e && r.insertBefore(t.__e, t.__d),\n                      (t.__c.__e = !0),\n                      (t.__c.__P = r))),\n                  t\n                );\n              })(e, e.__c.__P, e.__c.__O);\n            }\n            var t;\n            for (r.setState({ __e: (r.__b = null) }); (t = r.t.pop()); )\n              t.forceUpdate();\n          }\n        },\n        u = !0 === t.__h;\n      r.__u++ || u || r.setState({ __e: (r.__b = r.__v.__k[0]) }), e.then(c, c);\n    }),\n    (Se.prototype.componentWillUnmount = function () {\n      this.t = [];\n    }),\n    (Se.prototype.render = function (e, t) {\n      if (this.__b) {\n        if (this.__v.__k) {\n          var n = document.createElement(\"div\"),\n            r = this.__v.__k[0].__c;\n          this.__v.__k[0] = (function e(t, n, r) {\n            return (\n              t &&\n                (t.__c &&\n                  t.__c.__H &&\n                  (t.__c.__H.__.forEach(function (e) {\n                    \"function\" == typeof e.__c && e.__c();\n                  }),\n                  (t.__c.__H = null)),\n                null != (t = me({}, t)).__c &&\n                  (t.__c.__P === r && (t.__c.__P = n), (t.__c = null)),\n                (t.__k =\n                  t.__k &&\n                  t.__k.map(function (t) {\n                    return e(t, n, r);\n                  }))),\n              t\n            );\n          })(this.__b, n, (r.__O = r.__P));\n        }\n        this.__b = null;\n      }\n      var o = t.__e && g(O, null, e.fallback);\n      return o && (o.__h = null), [g(O, null, t.__e ? null : e.children), o];\n    });\n  var Ee = function (e, t, n) {\n    if (\n      (++n[1] === n[0] && e.o.delete(t),\n      e.props.revealOrder && (\"t\" !== e.props.revealOrder[0] || !e.o.size))\n    )\n      for (n = e.u; n; ) {\n        for (; n.length > 3; ) n.pop()();\n        if (n[1] < n[0]) break;\n        e.u = n = n[2];\n      }\n  };\n  function je(e) {\n    return (\n      (this.getChildContext = function () {\n        return e.context;\n      }),\n      e.children\n    );\n  }\n  function Pe(e) {\n    var t = this,\n      n = e.i;\n    (t.componentWillUnmount = function () {\n      B(null, t.l), (t.l = null), (t.i = null);\n    }),\n      t.i && t.i !== n && t.componentWillUnmount(),\n      e.__v\n        ? (t.l ||\n            ((t.i = n),\n            (t.l = {\n              nodeType: 1,\n              parentNode: n,\n              childNodes: [],\n              appendChild: function (e) {\n                this.childNodes.push(e), t.i.appendChild(e);\n              },\n              insertBefore: function (e, n) {\n                this.childNodes.push(e), t.i.appendChild(e);\n              },\n              removeChild: function (e) {\n                this.childNodes.splice(this.childNodes.indexOf(e) >>> 1, 1),\n                  t.i.removeChild(e);\n              },\n            })),\n          B(g(je, { context: t.context }, e.__v), t.l))\n        : t.l && t.componentWillUnmount();\n  }\n  function Ie(e, t) {\n    return g(Pe, { __v: e, i: t });\n  }\n  ((we.prototype = new w()).__e = function (e) {\n    var t = this,\n      n = Oe(t.__v),\n      r = t.o.get(e);\n    return (\n      r[0]++,\n      function (o) {\n        var i = function () {\n          t.props.revealOrder ? (r.push(o), Ee(t, e, r)) : o();\n        };\n        n ? n(i) : i();\n      }\n    );\n  }),\n    (we.prototype.render = function (e) {\n      (this.u = null), (this.o = new Map());\n      var t = C(e.children);\n      e.revealOrder && \"b\" === e.revealOrder[0] && t.reverse();\n      for (var n = t.length; n--; ) this.o.set(t[n], (this.u = [1, 0, this.u]));\n      return e.children;\n    }),\n    (we.prototype.componentDidUpdate = we.prototype.componentDidMount =\n      function () {\n        var e = this;\n        this.o.forEach(function (t, n) {\n          Ee(e, n, t);\n        });\n      });\n  var De =\n      (\"undefined\" != typeof Symbol &&\n        Symbol.for &&\n        Symbol.for(\"react.element\")) ||\n      60103,\n    ke =\n      /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,\n    Ce = function (e) {\n      return (\n        \"undefined\" != typeof Symbol && \"symbol\" == n(Symbol())\n          ? /fil|che|rad/i\n          : /fil|che|ra/i\n      ).test(e);\n    };\n  function Ae(e, t, n) {\n    return (\n      null == t.__k && (t.textContent = \"\"),\n      B(e, t),\n      \"function\" == typeof n && n(),\n      e ? e.__c : null\n    );\n  }\n  (w.prototype.isReactComponent = {}),\n    [\n      \"componentWillMount\",\n      \"componentWillReceiveProps\",\n      \"componentWillUpdate\",\n    ].forEach(function (e) {\n      Object.defineProperty(w.prototype, e, {\n        configurable: !0,\n        get: function () {\n          return this[\"UNSAFE_\" + e];\n        },\n        set: function (t) {\n          Object.defineProperty(this, e, {\n            configurable: !0,\n            writable: !0,\n            value: t,\n          });\n        },\n      });\n    });\n  var xe = s.event;\n  function Ne() {}\n  function Te() {\n    return this.cancelBubble;\n  }\n  function Re() {\n    return this.defaultPrevented;\n  }\n  s.event = function (e) {\n    return (\n      xe && (e = xe(e)),\n      (e.persist = Ne),\n      (e.isPropagationStopped = Te),\n      (e.isDefaultPrevented = Re),\n      (e.nativeEvent = e)\n    );\n  };\n  var qe,\n    Le = {\n      configurable: !0,\n      get: function () {\n        return this.class;\n      },\n    },\n    Me = s.vnode;\n  s.vnode = function (e) {\n    var t = e.type,\n      n = e.props,\n      r = n;\n    if (\"string\" == typeof t) {\n      for (var o in ((r = {}), n)) {\n        var i = n[o];\n        (\"value\" === o && \"defaultValue\" in n && null == i) ||\n          (\"defaultValue\" === o && \"value\" in n && null == n.value\n            ? (o = \"value\")\n            : \"download\" === o && !0 === i\n            ? (i = \"\")\n            : /ondoubleclick/i.test(o)\n            ? (o = \"ondblclick\")\n            : /^onchange(textarea|input)/i.test(o + t) && !Ce(n.type)\n            ? (o = \"oninput\")\n            : /^on(Ani|Tra|Tou|BeforeInp)/.test(o)\n            ? (o = o.toLowerCase())\n            : ke.test(o)\n            ? (o = o.replace(/[A-Z0-9]/, \"-$&\").toLowerCase())\n            : null === i && (i = void 0),\n          (r[o] = i));\n      }\n      \"select\" == t &&\n        r.multiple &&\n        Array.isArray(r.value) &&\n        (r.value = C(n.children).forEach(function (e) {\n          e.props.selected = -1 != r.value.indexOf(e.props.value);\n        })),\n        \"select\" == t &&\n          null != r.defaultValue &&\n          (r.value = C(n.children).forEach(function (e) {\n            e.props.selected = r.multiple\n              ? -1 != r.defaultValue.indexOf(e.props.value)\n              : r.defaultValue == e.props.value;\n          })),\n        (e.props = r);\n    }\n    t &&\n      n.class != n.className &&\n      ((Le.enumerable = \"className\" in n),\n      null != n.className && (r.class = n.className),\n      Object.defineProperty(r, \"className\", Le)),\n      (e.$$typeof = De),\n      Me && Me(e);\n  };\n  var He = s.__r;\n  s.__r = function (e) {\n    He && He(e), (qe = e.__c);\n  };\n  var Ue = {\n    ReactCurrentDispatcher: {\n      current: {\n        readContext: function (e) {\n          return qe.__n[e.__c].props.value;\n        },\n      },\n    },\n  };\n  \"object\" ==\n    (\"undefined\" == typeof performance ? \"undefined\" : n(performance)) &&\n    \"function\" == typeof performance.now &&\n    performance.now.bind(performance);\n  function Fe(e) {\n    return !!e && e.$$typeof === De;\n  }\n  var Be = {\n      useState: ne,\n      useReducer: re,\n      useEffect: oe,\n      useLayoutEffect: ie,\n      useRef: function (e) {\n        return (\n          ($ = 5),\n          ce(function () {\n            return { current: e };\n          }, [])\n        );\n      },\n      useImperativeHandle: function (e, t, n) {\n        ($ = 6),\n          ie(\n            function () {\n              \"function\" == typeof e ? e(t()) : e && (e.current = t());\n            },\n            null == n ? n : n.concat(e),\n          );\n      },\n      useMemo: ce,\n      useCallback: function (e, t) {\n        return (\n          ($ = 8),\n          ce(function () {\n            return e;\n          }, t)\n        );\n      },\n      useContext: function (e) {\n        var t = z.context[e.__c],\n          n = te(W++, 9);\n        return (\n          (n.__c = e),\n          t ? (null == n.__ && ((n.__ = !0), t.sub(z)), t.props.value) : e.__\n        );\n      },\n      useDebugValue: function (e, t) {\n        s.useDebugValue && s.useDebugValue(t ? t(e) : e);\n      },\n      version: \"16.8.0\",\n      Children: be,\n      render: Ae,\n      hydrate: function (e, t, n) {\n        return V(e, t), \"function\" == typeof n && n(), e ? e.__c : null;\n      },\n      unmountComponentAtNode: function (e) {\n        return !!e.__k && (B(null, e), !0);\n      },\n      createPortal: Ie,\n      createElement: g,\n      createContext: function (e, t) {\n        var n = {\n          __c: (t = \"__cC\" + d++),\n          __: e,\n          Consumer: function (e, t) {\n            return e.children(t);\n          },\n          Provider: function (e) {\n            var n, r;\n            return (\n              this.getChildContext ||\n                ((n = []),\n                ((r = {})[t] = this),\n                (this.getChildContext = function () {\n                  return r;\n                }),\n                (this.shouldComponentUpdate = function (e) {\n                  this.props.value !== e.value && n.some(P);\n                }),\n                (this.sub = function (e) {\n                  n.push(e);\n                  var t = e.componentWillUnmount;\n                  e.componentWillUnmount = function () {\n                    n.splice(n.indexOf(e), 1), t && t.call(e);\n                  };\n                })),\n              e.children\n            );\n          },\n        };\n        return (n.Provider.__ = n.Consumer.contextType = n);\n      },\n      createFactory: function (e) {\n        return g.bind(null, e);\n      },\n      cloneElement: function (e) {\n        return Fe(e) ? K.apply(null, arguments) : e;\n      },\n      createRef: function () {\n        return { current: null };\n      },\n      Fragment: O,\n      isValidElement: Fe,\n      findDOMNode: function (e) {\n        return (e && (e.base || (1 === e.nodeType && e))) || null;\n      },\n      Component: w,\n      PureComponent: ve,\n      memo: function (e, t) {\n        function n(e) {\n          var n = this.props.ref,\n            r = n == e.ref;\n          return (\n            !r && n && (n.call ? n(null) : (n.current = null)),\n            t ? !t(this.props, e) || !r : de(this.props, e)\n          );\n        }\n        function r(t) {\n          return (this.shouldComponentUpdate = n), g(e, t);\n        }\n        return (\n          (r.displayName = \"Memo(\" + (e.displayName || e.name) + \")\"),\n          (r.prototype.isReactComponent = !0),\n          (r.__f = !0),\n          r\n        );\n      },\n      forwardRef: function (e) {\n        function t(t, r) {\n          var o = me({}, t);\n          return (\n            delete o.ref,\n            e(\n              o,\n              (r = t.ref || r) && (\"object\" != n(r) || \"current\" in r)\n                ? r\n                : null,\n            )\n          );\n        }\n        return (\n          (t.$$typeof = ye),\n          (t.render = t),\n          (t.prototype.isReactComponent = t.__f = !0),\n          (t.displayName = \"ForwardRef(\" + (e.displayName || e.name) + \")\"),\n          t\n        );\n      },\n      unstable_batchedUpdates: function (e, t) {\n        return e(t);\n      },\n      StrictMode: O,\n      Suspense: Se,\n      SuspenseList: we,\n      lazy: function (e) {\n        var t, n, r;\n        function o(o) {\n          if (\n            (t ||\n              (t = e()).then(\n                function (e) {\n                  n = e.default || e;\n                },\n                function (e) {\n                  r = e;\n                },\n              ),\n            r)\n          )\n            throw r;\n          if (!n) throw t;\n          return g(n, o);\n        }\n        return (o.displayName = \"Lazy\"), (o.__f = !0), o;\n      },\n      __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: Ue,\n    },\n    Ve = [\"facetName\", \"facetQuery\"];\n  function Ke(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function We(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Ke(Object(n), !0).forEach(function (t) {\n            ze(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Ke(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function ze(e, t, n) {\n    return (\n      t in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Je() {\n    return (\n      (Je =\n        Object.assign ||\n        function (e) {\n          for (var t = 1; t < arguments.length; t++) {\n            var n = arguments[t];\n            for (var r in n)\n              Object.prototype.hasOwnProperty.call(n, r) && (e[r] = n[r]);\n          }\n          return e;\n        }),\n      Je.apply(this, arguments)\n    );\n  }\n  function $e(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function Ze(e, t) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return e;\n      })(e) ||\n      (function (e, t) {\n        var n =\n          null == e\n            ? null\n            : (\"undefined\" != typeof Symbol && e[Symbol.iterator]) ||\n              e[\"@@iterator\"];\n        if (null != n) {\n          var r,\n            o,\n            i = [],\n            c = !0,\n            a = !1;\n          try {\n            for (\n              n = n.call(e);\n              !(c = (r = n.next()).done) &&\n              (i.push(r.value), !t || i.length !== t);\n              c = !0\n            );\n          } catch (e) {\n            (a = !0), (o = e);\n          } finally {\n            try {\n              c || null == n.return || n.return();\n            } finally {\n              if (a) throw o;\n            }\n          }\n          return i;\n        }\n      })(e, t) ||\n      Qe(e, t) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function Qe(e, t) {\n    if (e) {\n      if (\"string\" == typeof e) return Ye(e, t);\n      var n = Object.prototype.toString.call(e).slice(8, -1);\n      return (\n        \"Object\" === n && e.constructor && (n = e.constructor.name),\n        \"Map\" === n || \"Set\" === n\n          ? Array.from(e)\n          : \"Arguments\" === n ||\n            /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n          ? Ye(e, t)\n          : void 0\n      );\n    }\n  }\n  function Ye(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function Ge() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"15\", height: \"15\", className: \"DocSearch-Control-Key-Icon\" },\n      Be.createElement(\"path\", {\n        d: \"M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953\",\n        strokeWidth: \"1.2\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        strokeLinecap: \"square\",\n      }),\n    );\n  }\n  function Xe() {\n    return Be.createElement(\n      \"svg\",\n      {\n        width: \"20\",\n        height: \"20\",\n        className: \"DocSearch-Search-Icon\",\n        viewBox: \"0 0 20 20\",\n        \"aria-hidden\": \"true\",\n      },\n      Be.createElement(\"path\", {\n        d: \"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  var et = [\"translations\"],\n    tt = Be.forwardRef(function (e, t) {\n      var n = e.translations,\n        r = void 0 === n ? {} : n,\n        o = $e(e, et),\n        i = r.buttonText,\n        c = void 0 === i ? \"Search\" : i,\n        a = r.buttonAriaLabel,\n        u = void 0 === a ? \"Search\" : a,\n        l = Ze(ne(null), 2),\n        s = l[0],\n        f = l[1];\n      return (\n        oe(function () {\n          \"undefined\" != typeof navigator &&\n            (/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)\n              ? f(\"⌘\")\n              : f(\"Ctrl\"));\n        }, []),\n        Be.createElement(\n          \"button\",\n          Je(\n            {\n              type: \"button\",\n              className: \"DocSearch DocSearch-Button\",\n              \"aria-label\": u,\n            },\n            o,\n            { ref: t },\n          ),\n          Be.createElement(\n            \"span\",\n            { className: \"DocSearch-Button-Container\" },\n            Be.createElement(Xe, null),\n            Be.createElement(\n              \"span\",\n              { className: \"DocSearch-Button-Placeholder\" },\n              c,\n            ),\n          ),\n          Be.createElement(\n            \"span\",\n            { className: \"DocSearch-Button-Keys\" },\n            null !== s &&\n              Be.createElement(\n                Be.Fragment,\n                null,\n                Be.createElement(\n                  nt,\n                  { reactsToKey: \"Ctrl\" === s ? \"Ctrl\" : \"Meta\" },\n                  \"Ctrl\" === s ? Be.createElement(Ge, null) : s,\n                ),\n                Be.createElement(nt, { reactsToKey: \"k\" }, \"K\"),\n              ),\n          ),\n        )\n      );\n    });\n  function nt(e) {\n    var t = e.reactsToKey,\n      n = e.children,\n      r = Ze(ne(!1), 2),\n      o = r[0],\n      i = r[1];\n    return (\n      oe(\n        function () {\n          if (t)\n            return (\n              window.addEventListener(\"keydown\", e),\n              window.addEventListener(\"keyup\", n),\n              function () {\n                window.removeEventListener(\"keydown\", e),\n                  window.removeEventListener(\"keyup\", n);\n              }\n            );\n          function e(e) {\n            e.key === t && i(!0);\n          }\n          function n(e) {\n            (e.key !== t && \"Meta\" !== e.key) || i(!1);\n          }\n        },\n        [t],\n      ),\n      Be.createElement(\n        \"kbd\",\n        {\n          className: o\n            ? \"DocSearch-Button-Key DocSearch-Button-Key--pressed\"\n            : \"DocSearch-Button-Key\",\n        },\n        n,\n      )\n    );\n  }\n  function rt(e, t) {\n    var n = void 0;\n    return function () {\n      for (var r = arguments.length, o = new Array(r), i = 0; i < r; i++)\n        o[i] = arguments[i];\n      n && clearTimeout(n),\n        (n = setTimeout(function () {\n          return e.apply(void 0, o);\n        }, t));\n    };\n  }\n  function ot(e) {\n    return e.reduce(function (e, t) {\n      return e.concat(t);\n    }, []);\n  }\n  var it = 0;\n  function ct(e) {\n    return 0 === e.collections.length\n      ? 0\n      : e.collections.reduce(function (e, t) {\n          return e + t.items.length;\n        }, 0);\n  }\n  function at(e) {\n    return e !== Object(e);\n  }\n  function ut(e, t) {\n    if (e === t) return !0;\n    if (at(e) || at(t) || \"function\" == typeof e || \"function\" == typeof t)\n      return e === t;\n    if (Object.keys(e).length !== Object.keys(t).length) return !1;\n    for (var n = 0, r = Object.keys(e); n < r.length; n++) {\n      var o = r[n];\n      if (!(o in t)) return !1;\n      if (!ut(e[o], t[o])) return !1;\n    }\n    return !0;\n  }\n  var lt = function () {},\n    st = [{ segment: \"autocomplete-core\", version: \"1.9.3\" }];\n  function ft(e) {\n    var t = e.item,\n      n = e.items;\n    return {\n      index: t.__autocomplete_indexName,\n      items: [t],\n      positions: [\n        1 +\n          n.findIndex(function (e) {\n            return e.objectID === t.objectID;\n          }),\n      ],\n      queryID: t.__autocomplete_queryID,\n      algoliaSource: [\"autocomplete\"],\n    };\n  }\n  function pt(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  var mt = [\"items\"],\n    dt = [\"items\"];\n  function vt(e) {\n    return (\n      (vt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      vt(e)\n    );\n  }\n  function ht(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return yt(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      (function (e, t) {\n        if (e) {\n          if (\"string\" == typeof e) return yt(e, t);\n          var n = Object.prototype.toString.call(e).slice(8, -1);\n          return (\n            \"Object\" === n && e.constructor && (n = e.constructor.name),\n            \"Map\" === n || \"Set\" === n\n              ? Array.from(e)\n              : \"Arguments\" === n ||\n                /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n              ? yt(e, t)\n              : void 0\n          );\n        }\n      })(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function yt(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function _t(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function bt(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function gt(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? bt(Object(n), !0).forEach(function (t) {\n            St(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : bt(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function St(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== vt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== vt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === vt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Ot(e) {\n    for (\n      var t =\n          arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 20,\n        n = [],\n        r = 0;\n      r < e.objectIDs.length;\n      r += t\n    )\n      n.push(gt(gt({}, e), {}, { objectIDs: e.objectIDs.slice(r, r + t) }));\n    return n;\n  }\n  function wt(e) {\n    return e.map(function (e) {\n      var t = e.items,\n        n = _t(e, mt);\n      return gt(\n        gt({}, n),\n        {},\n        {\n          objectIDs:\n            (null == t\n              ? void 0\n              : t.map(function (e) {\n                  return e.objectID;\n                })) || n.objectIDs,\n        },\n      );\n    });\n  }\n  function Et(e) {\n    var t,\n      n,\n      r,\n      o =\n        ((t = (function (e, t) {\n          return (\n            (function (e) {\n              if (Array.isArray(e)) return e;\n            })(e) ||\n            (function (e, t) {\n              var n =\n                null == e\n                  ? null\n                  : (\"undefined\" != typeof Symbol && e[Symbol.iterator]) ||\n                    e[\"@@iterator\"];\n              if (null != n) {\n                var r,\n                  o,\n                  i,\n                  c,\n                  a = [],\n                  u = !0,\n                  l = !1;\n                try {\n                  if (((i = (n = n.call(e)).next), 0 === t)) {\n                    if (Object(n) !== n) return;\n                    u = !1;\n                  } else\n                    for (\n                      ;\n                      !(u = (r = i.call(n)).done) &&\n                      (a.push(r.value), a.length !== t);\n                      u = !0\n                    );\n                } catch (e) {\n                  (l = !0), (o = e);\n                } finally {\n                  try {\n                    if (\n                      !u &&\n                      null != n.return &&\n                      ((c = n.return()), Object(c) !== c)\n                    )\n                      return;\n                  } finally {\n                    if (l) throw o;\n                  }\n                }\n                return a;\n              }\n            })(e, t) ||\n            (function (e, t) {\n              if (e) {\n                if (\"string\" == typeof e) return pt(e, t);\n                var n = Object.prototype.toString.call(e).slice(8, -1);\n                return (\n                  \"Object\" === n && e.constructor && (n = e.constructor.name),\n                  \"Map\" === n || \"Set\" === n\n                    ? Array.from(e)\n                    : \"Arguments\" === n ||\n                      /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n                    ? pt(e, t)\n                    : void 0\n                );\n              }\n            })(e, t) ||\n            (function () {\n              throw new TypeError(\n                \"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n              );\n            })()\n          );\n        })((e.version || \"\").split(\".\").map(Number), 2)),\n        (n = t[0]),\n        (r = t[1]),\n        n >= 3 || (2 === n && r >= 4) || (1 === n && r >= 10));\n    function i(t, n, r) {\n      if (o && void 0 !== r) {\n        var i = r[0].__autocomplete_algoliaCredentials,\n          c = {\n            \"X-Algolia-Application-Id\": i.appId,\n            \"X-Algolia-API-Key\": i.apiKey,\n          };\n        e.apply(void 0, [t].concat(ht(n), [{ headers: c }]));\n      } else e.apply(void 0, [t].concat(ht(n)));\n    }\n    return {\n      init: function (t, n) {\n        e(\"init\", { appId: t, apiKey: n });\n      },\n      setUserToken: function (t) {\n        e(\"setUserToken\", t);\n      },\n      clickedObjectIDsAfterSearch: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"clickedObjectIDsAfterSearch\", wt(t), t[0].items);\n      },\n      clickedObjectIDs: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"clickedObjectIDs\", wt(t), t[0].items);\n      },\n      clickedFilters: function () {\n        for (var t = arguments.length, n = new Array(t), r = 0; r < t; r++)\n          n[r] = arguments[r];\n        n.length > 0 && e.apply(void 0, [\"clickedFilters\"].concat(n));\n      },\n      convertedObjectIDsAfterSearch: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"convertedObjectIDsAfterSearch\", wt(t), t[0].items);\n      },\n      convertedObjectIDs: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"convertedObjectIDs\", wt(t), t[0].items);\n      },\n      convertedFilters: function () {\n        for (var t = arguments.length, n = new Array(t), r = 0; r < t; r++)\n          n[r] = arguments[r];\n        n.length > 0 && e.apply(void 0, [\"convertedFilters\"].concat(n));\n      },\n      viewedObjectIDs: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 &&\n          t\n            .reduce(function (e, t) {\n              var n = t.items,\n                r = _t(t, dt);\n              return [].concat(\n                ht(e),\n                ht(\n                  Ot(\n                    gt(\n                      gt({}, r),\n                      {},\n                      {\n                        objectIDs:\n                          (null == n\n                            ? void 0\n                            : n.map(function (e) {\n                                return e.objectID;\n                              })) || r.objectIDs,\n                      },\n                    ),\n                  ).map(function (e) {\n                    return { items: n, payload: e };\n                  }),\n                ),\n              );\n            }, [])\n            .forEach(function (e) {\n              var t = e.items;\n              return i(\"viewedObjectIDs\", [e.payload], t);\n            });\n      },\n      viewedFilters: function () {\n        for (var t = arguments.length, n = new Array(t), r = 0; r < t; r++)\n          n[r] = arguments[r];\n        n.length > 0 && e.apply(void 0, [\"viewedFilters\"].concat(n));\n      },\n    };\n  }\n  function jt(e) {\n    var t = e.items.reduce(function (e, t) {\n      var n;\n      return (\n        (e[t.__autocomplete_indexName] = (\n          null !== (n = e[t.__autocomplete_indexName]) && void 0 !== n ? n : []\n        ).concat(t)),\n        e\n      );\n    }, {});\n    return Object.keys(t).map(function (e) {\n      return { index: e, items: t[e], algoliaSource: [\"autocomplete\"] };\n    });\n  }\n  function Pt(e) {\n    return e.objectID && e.__autocomplete_indexName && e.__autocomplete_queryID;\n  }\n  function It(e) {\n    return (\n      (It =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      It(e)\n    );\n  }\n  function Dt(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return kt(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      (function (e, t) {\n        if (e) {\n          if (\"string\" == typeof e) return kt(e, t);\n          var n = Object.prototype.toString.call(e).slice(8, -1);\n          return (\n            \"Object\" === n && e.constructor && (n = e.constructor.name),\n            \"Map\" === n || \"Set\" === n\n              ? Array.from(e)\n              : \"Arguments\" === n ||\n                /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n              ? kt(e, t)\n              : void 0\n          );\n        }\n      })(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function kt(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function Ct(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function At(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Ct(Object(n), !0).forEach(function (t) {\n            xt(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Ct(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function xt(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== It(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== It(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === It(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  var Nt = \"https://cdn.jsdelivr.net/npm/search-insights@\".concat(\n      \"2.6.0\",\n      \"/dist/search-insights.min.js\",\n    ),\n    Tt = rt(function (e) {\n      var t = e.onItemsChange,\n        n = e.items,\n        r = e.insights,\n        o = e.state;\n      t({\n        insights: r,\n        insightsEvents: jt({ items: n }).map(function (e) {\n          return At({ eventName: \"Items Viewed\" }, e);\n        }),\n        state: o,\n      });\n    }, 400);\n  function Rt(e) {\n    var t = (function (e) {\n        return At(\n          {\n            onItemsChange: function (e) {\n              var t = e.insights,\n                n = e.insightsEvents;\n              t.viewedObjectIDs.apply(\n                t,\n                Dt(\n                  n.map(function (e) {\n                    return At(\n                      At({}, e),\n                      {},\n                      {\n                        algoliaSource: [].concat(Dt(e.algoliaSource || []), [\n                          \"autocomplete-internal\",\n                        ]),\n                      },\n                    );\n                  }),\n                ),\n              );\n            },\n            onSelect: function (e) {\n              var t = e.insights,\n                n = e.insightsEvents;\n              t.clickedObjectIDsAfterSearch.apply(\n                t,\n                Dt(\n                  n.map(function (e) {\n                    return At(\n                      At({}, e),\n                      {},\n                      {\n                        algoliaSource: [].concat(Dt(e.algoliaSource || []), [\n                          \"autocomplete-internal\",\n                        ]),\n                      },\n                    );\n                  }),\n                ),\n              );\n            },\n            onActive: lt,\n          },\n          e,\n        );\n      })(e),\n      n = t.insightsClient,\n      r = t.onItemsChange,\n      o = t.onSelect,\n      i = t.onActive,\n      c = n;\n    n ||\n      (\"undefined\" != typeof window &&\n        (function (e) {\n          var t = e.window,\n            n = t.AlgoliaAnalyticsObject || \"aa\";\n          \"string\" == typeof n && (c = t[n]),\n            c ||\n              ((t.AlgoliaAnalyticsObject = n),\n              t[n] ||\n                (t[n] = function () {\n                  t[n].queue || (t[n].queue = []);\n                  for (\n                    var e = arguments.length, r = new Array(e), o = 0;\n                    o < e;\n                    o++\n                  )\n                    r[o] = arguments[o];\n                  t[n].queue.push(r);\n                }),\n              (t[n].version = \"2.6.0\"),\n              (c = t[n]),\n              (function (e) {\n                var t =\n                  \"[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete\";\n                try {\n                  var n = e.document.createElement(\"script\");\n                  (n.async = !0),\n                    (n.src = Nt),\n                    (n.onerror = function () {\n                      console.error(t);\n                    }),\n                    document.body.appendChild(n);\n                } catch (e) {\n                  console.error(t);\n                }\n              })(t));\n        })({ window: window }));\n    var a = Et(c),\n      u = { current: [] },\n      l = rt(function (e) {\n        var t = e.state;\n        if (t.isOpen) {\n          var n = t.collections\n            .reduce(function (e, t) {\n              return [].concat(Dt(e), Dt(t.items));\n            }, [])\n            .filter(Pt);\n          ut(\n            u.current.map(function (e) {\n              return e.objectID;\n            }),\n            n.map(function (e) {\n              return e.objectID;\n            }),\n          ) ||\n            ((u.current = n),\n            n.length > 0 &&\n              Tt({ onItemsChange: r, items: n, insights: a, state: t }));\n        }\n      }, 0);\n    return {\n      name: \"aa.algoliaInsightsPlugin\",\n      subscribe: function (e) {\n        var t = e.setContext,\n          n = e.onSelect,\n          r = e.onActive;\n        c(\"addAlgoliaAgent\", \"insights-plugin\"),\n          t({\n            algoliaInsightsPlugin: {\n              __algoliaSearchParameters: { clickAnalytics: !0 },\n              insights: a,\n            },\n          }),\n          n(function (e) {\n            var t = e.item,\n              n = e.state,\n              r = e.event;\n            Pt(t) &&\n              o({\n                state: n,\n                event: r,\n                insights: a,\n                item: t,\n                insightsEvents: [\n                  At(\n                    { eventName: \"Item Selected\" },\n                    ft({ item: t, items: u.current }),\n                  ),\n                ],\n              });\n          }),\n          r(function (e) {\n            var t = e.item,\n              n = e.state,\n              r = e.event;\n            Pt(t) &&\n              i({\n                state: n,\n                event: r,\n                insights: a,\n                item: t,\n                insightsEvents: [\n                  At(\n                    { eventName: \"Item Active\" },\n                    ft({ item: t, items: u.current }),\n                  ),\n                ],\n              });\n          });\n      },\n      onStateChange: function (e) {\n        var t = e.state;\n        l({ state: t });\n      },\n      __autocomplete_pluginOptions: e,\n    };\n  }\n  function qt(e, t) {\n    var n = t;\n    return {\n      then: function (t, r) {\n        return qt(e.then(Mt(t, n, e), Mt(r, n, e)), n);\n      },\n      catch: function (t) {\n        return qt(e.catch(Mt(t, n, e)), n);\n      },\n      finally: function (t) {\n        return (\n          t && n.onCancelList.push(t),\n          qt(\n            e.finally(\n              Mt(\n                t &&\n                  function () {\n                    return (n.onCancelList = []), t();\n                  },\n                n,\n                e,\n              ),\n            ),\n            n,\n          )\n        );\n      },\n      cancel: function () {\n        n.isCanceled = !0;\n        var e = n.onCancelList;\n        (n.onCancelList = []),\n          e.forEach(function (e) {\n            e();\n          });\n      },\n      isCanceled: function () {\n        return !0 === n.isCanceled;\n      },\n    };\n  }\n  function Lt(e) {\n    return qt(e, { isCanceled: !1, onCancelList: [] });\n  }\n  function Mt(e, t, n) {\n    return e\n      ? function (n) {\n          return t.isCanceled ? n : e(n);\n        }\n      : n;\n  }\n  function Ht(e, t, n, r) {\n    if (!n) return null;\n    if (e < 0 && (null === t || (null !== r && 0 === t))) return n + e;\n    var o = (null === t ? -1 : t) + e;\n    return o <= -1 || o >= n ? (null === r ? null : 0) : o;\n  }\n  function Ut(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Ft(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Ut(Object(n), !0).forEach(function (t) {\n            Bt(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Ut(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Bt(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Vt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Vt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Vt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Vt(e) {\n    return (\n      (Vt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Vt(e)\n    );\n  }\n  function Kt(e) {\n    var t = (function (e) {\n      var t = e.collections\n        .map(function (e) {\n          return e.items.length;\n        })\n        .reduce(function (e, t, n) {\n          var r = (e[n - 1] || 0) + t;\n          return e.push(r), e;\n        }, [])\n        .reduce(function (t, n) {\n          return n <= e.activeItemId ? t + 1 : t;\n        }, 0);\n      return e.collections[t];\n    })(e);\n    if (!t) return null;\n    var n =\n        t.items[\n          (function (e) {\n            for (\n              var t = e.state, n = e.collection, r = !1, o = 0, i = 0;\n              !1 === r;\n\n            ) {\n              var c = t.collections[o];\n              if (c === n) {\n                r = !0;\n                break;\n              }\n              (i += c.items.length), o++;\n            }\n            return t.activeItemId - i;\n          })({ state: e, collection: t })\n        ],\n      r = t.source;\n    return {\n      item: n,\n      itemInputValue: r.getItemInputValue({ item: n, state: e }),\n      itemUrl: r.getItemUrl({ item: n, state: e }),\n      source: r,\n    };\n  }\n  var Wt = /((gt|sm)-|galaxy nexus)|samsung[- ]|samsungbrowser/i;\n  function zt(e) {\n    return (\n      (zt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      zt(e)\n    );\n  }\n  function Jt(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function $t(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== zt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== zt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === zt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Zt(e) {\n    return (\n      (Zt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Zt(e)\n    );\n  }\n  function Qt(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Yt(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Qt(Object(n), !0).forEach(function (t) {\n            Gt(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Qt(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Gt(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Zt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Zt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Zt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Xt(e) {\n    return (\n      (Xt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Xt(e)\n    );\n  }\n  function en(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function tn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function nn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? tn(Object(n), !0).forEach(function (t) {\n            rn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : tn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function rn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Xt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Xt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Xt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function on(e, t) {\n    var n,\n      r = \"undefined\" != typeof window ? window : {},\n      o = e.plugins || [];\n    return nn(\n      nn(\n        {\n          debug: !1,\n          openOnFocus: !1,\n          placeholder: \"\",\n          autoFocus: !1,\n          defaultActiveItemId: null,\n          stallThreshold: 300,\n          insights: !1,\n          environment: r,\n          shouldPanelOpen: function (e) {\n            return ct(e.state) > 0;\n          },\n          reshape: function (e) {\n            return e.sources;\n          },\n        },\n        e,\n      ),\n      {},\n      {\n        id:\n          null !== (n = e.id) && void 0 !== n\n            ? n\n            : \"autocomplete-\".concat(it++),\n        plugins: o,\n        initialState: nn(\n          {\n            activeItemId: null,\n            query: \"\",\n            completion: null,\n            collections: [],\n            isOpen: !1,\n            status: \"idle\",\n            context: {},\n          },\n          e.initialState,\n        ),\n        onStateChange: function (t) {\n          var n;\n          null === (n = e.onStateChange) || void 0 === n || n.call(e, t),\n            o.forEach(function (e) {\n              var n;\n              return null === (n = e.onStateChange) || void 0 === n\n                ? void 0\n                : n.call(e, t);\n            });\n        },\n        onSubmit: function (t) {\n          var n;\n          null === (n = e.onSubmit) || void 0 === n || n.call(e, t),\n            o.forEach(function (e) {\n              var n;\n              return null === (n = e.onSubmit) || void 0 === n\n                ? void 0\n                : n.call(e, t);\n            });\n        },\n        onReset: function (t) {\n          var n;\n          null === (n = e.onReset) || void 0 === n || n.call(e, t),\n            o.forEach(function (e) {\n              var n;\n              return null === (n = e.onReset) || void 0 === n\n                ? void 0\n                : n.call(e, t);\n            });\n        },\n        getSources: function (n) {\n          return Promise.all(\n            []\n              .concat(\n                (function (e) {\n                  return (\n                    (function (e) {\n                      if (Array.isArray(e)) return en(e);\n                    })(e) ||\n                    (function (e) {\n                      if (\n                        (\"undefined\" != typeof Symbol &&\n                          null != e[Symbol.iterator]) ||\n                        null != e[\"@@iterator\"]\n                      )\n                        return Array.from(e);\n                    })(e) ||\n                    (function (e, t) {\n                      if (e) {\n                        if (\"string\" == typeof e) return en(e, t);\n                        var n = Object.prototype.toString.call(e).slice(8, -1);\n                        return (\n                          \"Object\" === n &&\n                            e.constructor &&\n                            (n = e.constructor.name),\n                          \"Map\" === n || \"Set\" === n\n                            ? Array.from(e)\n                            : \"Arguments\" === n ||\n                              /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n                            ? en(e, t)\n                            : void 0\n                        );\n                      }\n                    })(e) ||\n                    (function () {\n                      throw new TypeError(\n                        \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n                      );\n                    })()\n                  );\n                })(\n                  o.map(function (e) {\n                    return e.getSources;\n                  }),\n                ),\n                [e.getSources],\n              )\n              .filter(Boolean)\n              .map(function (e) {\n                return (function (e, t) {\n                  var n = [];\n                  return Promise.resolve(e(t)).then(function (e) {\n                    return Promise.all(\n                      e\n                        .filter(function (e) {\n                          return Boolean(e);\n                        })\n                        .map(function (e) {\n                          if ((e.sourceId, n.includes(e.sourceId)))\n                            throw new Error(\n                              \"[Autocomplete] The `sourceId` \".concat(\n                                JSON.stringify(e.sourceId),\n                                \" is not unique.\",\n                              ),\n                            );\n                          n.push(e.sourceId);\n                          var t = {\n                            getItemInputValue: function (e) {\n                              return e.state.query;\n                            },\n                            getItemUrl: function () {},\n                            onSelect: function (e) {\n                              (0, e.setIsOpen)(!1);\n                            },\n                            onActive: lt,\n                            onResolve: lt,\n                          };\n                          Object.keys(t).forEach(function (e) {\n                            t[e].__default = !0;\n                          });\n                          var r = Ft(Ft({}, t), e);\n                          return Promise.resolve(r);\n                        }),\n                    );\n                  });\n                })(e, n);\n              }),\n          )\n            .then(function (e) {\n              return ot(e);\n            })\n            .then(function (e) {\n              return e.map(function (e) {\n                return nn(\n                  nn({}, e),\n                  {},\n                  {\n                    onSelect: function (n) {\n                      e.onSelect(n),\n                        t.forEach(function (e) {\n                          var t;\n                          return null === (t = e.onSelect) || void 0 === t\n                            ? void 0\n                            : t.call(e, n);\n                        });\n                    },\n                    onActive: function (n) {\n                      e.onActive(n),\n                        t.forEach(function (e) {\n                          var t;\n                          return null === (t = e.onActive) || void 0 === t\n                            ? void 0\n                            : t.call(e, n);\n                        });\n                    },\n                    onResolve: function (n) {\n                      e.onResolve(n),\n                        t.forEach(function (e) {\n                          var t;\n                          return null === (t = e.onResolve) || void 0 === t\n                            ? void 0\n                            : t.call(e, n);\n                        });\n                    },\n                  },\n                );\n              });\n            });\n        },\n        navigator: nn(\n          {\n            navigate: function (e) {\n              var t = e.itemUrl;\n              r.location.assign(t);\n            },\n            navigateNewTab: function (e) {\n              var t = e.itemUrl,\n                n = r.open(t, \"_blank\", \"noopener\");\n              null == n || n.focus();\n            },\n            navigateNewWindow: function (e) {\n              var t = e.itemUrl;\n              r.open(t, \"_blank\", \"noopener\");\n            },\n          },\n          e.navigator,\n        ),\n      },\n    );\n  }\n  function cn(e) {\n    return (\n      (cn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      cn(e)\n    );\n  }\n  function an(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function un(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? an(Object(n), !0).forEach(function (t) {\n            ln(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : an(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function ln(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== cn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== cn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === cn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function sn(e) {\n    return (\n      (sn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      sn(e)\n    );\n  }\n  function fn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function pn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? fn(Object(n), !0).forEach(function (t) {\n            mn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : fn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function mn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== sn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== sn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === sn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function dn(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return vn(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      (function (e, t) {\n        if (e) {\n          if (\"string\" == typeof e) return vn(e, t);\n          var n = Object.prototype.toString.call(e).slice(8, -1);\n          return (\n            \"Object\" === n && e.constructor && (n = e.constructor.name),\n            \"Map\" === n || \"Set\" === n\n              ? Array.from(e)\n              : \"Arguments\" === n ||\n                /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n              ? vn(e, t)\n              : void 0\n          );\n        }\n      })(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function vn(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function hn(e) {\n    return Boolean(e.execute);\n  }\n  function yn(e) {\n    var t = e\n      .reduce(function (e, t) {\n        if (!hn(t)) return e.push(t), e;\n        var n = t.searchClient,\n          r = t.execute,\n          o = t.requesterId,\n          i = t.requests,\n          c = e.find(function (e) {\n            return (\n              hn(t) &&\n              hn(e) &&\n              e.searchClient === n &&\n              Boolean(o) &&\n              e.requesterId === o\n            );\n          });\n        if (c) {\n          var a;\n          (a = c.items).push.apply(a, dn(i));\n        } else {\n          var u = { execute: r, requesterId: o, items: i, searchClient: n };\n          e.push(u);\n        }\n        return e;\n      }, [])\n      .map(function (e) {\n        if (!hn(e)) return Promise.resolve(e);\n        var t = e,\n          n = t.execute,\n          r = t.items;\n        return n({ searchClient: t.searchClient, requests: r });\n      });\n    return Promise.all(t).then(function (e) {\n      return ot(e);\n    });\n  }\n  function _n(e) {\n    return (\n      (_n =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      _n(e)\n    );\n  }\n  var bn = [\"event\", \"nextState\", \"props\", \"query\", \"refresh\", \"store\"];\n  function gn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Sn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? gn(Object(n), !0).forEach(function (t) {\n            On(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : gn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function On(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== _n(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== _n(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === _n(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  var wn,\n    En,\n    jn,\n    Pn = null,\n    In =\n      ((wn = -1),\n      (En = -1),\n      (jn = void 0),\n      function (e) {\n        var t = ++wn;\n        return Promise.resolve(e).then(function (e) {\n          return jn && t < En ? jn : ((En = t), (jn = e), e);\n        });\n      });\n  function Dn(e) {\n    var t = e.event,\n      n = e.nextState,\n      r = void 0 === n ? {} : n,\n      o = e.props,\n      i = e.query,\n      c = e.refresh,\n      a = e.store,\n      u = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = (function (e, t) {\n            if (null == e) return {};\n            var n,\n              r,\n              o = {},\n              i = Object.keys(e);\n            for (r = 0; r < i.length; r++)\n              (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n            return o;\n          })(e, t);\n        if (Object.getOwnPropertySymbols) {\n          var i = Object.getOwnPropertySymbols(e);\n          for (r = 0; r < i.length; r++)\n            (n = i[r]),\n              t.indexOf(n) >= 0 ||\n                (Object.prototype.propertyIsEnumerable.call(e, n) &&\n                  (o[n] = e[n]));\n        }\n        return o;\n      })(e, bn);\n    Pn && o.environment.clearTimeout(Pn);\n    var l = u.setCollections,\n      s = u.setIsOpen,\n      f = u.setQuery,\n      p = u.setActiveItemId,\n      m = u.setStatus;\n    if ((f(i), p(o.defaultActiveItemId), !i && !1 === o.openOnFocus)) {\n      var d,\n        v = a.getState().collections.map(function (e) {\n          return Sn(Sn({}, e), {}, { items: [] });\n        });\n      m(\"idle\"),\n        l(v),\n        s(\n          null !== (d = r.isOpen) && void 0 !== d\n            ? d\n            : o.shouldPanelOpen({ state: a.getState() }),\n        );\n      var h = Lt(\n        In(v).then(function () {\n          return Promise.resolve();\n        }),\n      );\n      return a.pendingRequests.add(h);\n    }\n    m(\"loading\"),\n      (Pn = o.environment.setTimeout(function () {\n        m(\"stalled\");\n      }, o.stallThreshold));\n    var y = Lt(\n      In(\n        o\n          .getSources(Sn({ query: i, refresh: c, state: a.getState() }, u))\n          .then(function (e) {\n            return Promise.all(\n              e.map(function (e) {\n                return Promise.resolve(\n                  e.getItems(\n                    Sn({ query: i, refresh: c, state: a.getState() }, u),\n                  ),\n                ).then(function (t) {\n                  return (function (e, t, n) {\n                    if (((o = e), Boolean(null == o ? void 0 : o.execute))) {\n                      var r =\n                        \"algolia\" === e.requesterId\n                          ? Object.assign.apply(\n                              Object,\n                              [{}].concat(\n                                dn(\n                                  Object.keys(n.context).map(function (e) {\n                                    var t;\n                                    return null === (t = n.context[e]) ||\n                                      void 0 === t\n                                      ? void 0\n                                      : t.__algoliaSearchParameters;\n                                  }),\n                                ),\n                              ),\n                            )\n                          : {};\n                      return pn(\n                        pn({}, e),\n                        {},\n                        {\n                          requests: e.queries.map(function (n) {\n                            return {\n                              query:\n                                \"algolia\" === e.requesterId\n                                  ? pn(\n                                      pn({}, n),\n                                      {},\n                                      { params: pn(pn({}, r), n.params) },\n                                    )\n                                  : n,\n                              sourceId: t,\n                              transformResponse: e.transformResponse,\n                            };\n                          }),\n                        },\n                      );\n                    }\n                    var o;\n                    return { items: e, sourceId: t };\n                  })(t, e.sourceId, a.getState());\n                });\n              }),\n            )\n              .then(yn)\n              .then(function (t) {\n                return (function (e, t, n) {\n                  return t.map(function (t) {\n                    var r,\n                      o = e.filter(function (e) {\n                        return e.sourceId === t.sourceId;\n                      }),\n                      i = o.map(function (e) {\n                        return e.items;\n                      }),\n                      c = o[0].transformResponse,\n                      a = c\n                        ? c({\n                            results: (r = i),\n                            hits: r\n                              .map(function (e) {\n                                return e.hits;\n                              })\n                              .filter(Boolean),\n                            facetHits: r\n                              .map(function (e) {\n                                var t;\n                                return null === (t = e.facetHits) ||\n                                  void 0 === t\n                                  ? void 0\n                                  : t.map(function (e) {\n                                      return {\n                                        label: e.value,\n                                        count: e.count,\n                                        _highlightResult: {\n                                          label: { value: e.highlighted },\n                                        },\n                                      };\n                                    });\n                              })\n                              .filter(Boolean),\n                          })\n                        : i;\n                    return (\n                      t.onResolve({\n                        source: t,\n                        results: i,\n                        items: a,\n                        state: n.getState(),\n                      }),\n                      a.every(Boolean),\n                      'The `getItems` function from source \"'\n                        .concat(\n                          t.sourceId,\n                          '\" must return an array of items but returned ',\n                        )\n                        .concat(\n                          JSON.stringify(void 0),\n                          \".\\n\\nDid you forget to return items?\\n\\nSee: https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/sources/#param-getitems\",\n                        ),\n                      { source: t, items: a }\n                    );\n                  });\n                })(t, e, a);\n              })\n              .then(function (e) {\n                return (function (e) {\n                  var t = e.props,\n                    n = e.state,\n                    r = e.collections.reduce(function (e, t) {\n                      return un(\n                        un({}, e),\n                        {},\n                        ln(\n                          {},\n                          t.source.sourceId,\n                          un(\n                            un({}, t.source),\n                            {},\n                            {\n                              getItems: function () {\n                                return ot(t.items);\n                              },\n                            },\n                          ),\n                        ),\n                      );\n                    }, {}),\n                    o = t.plugins.reduce(\n                      function (e, t) {\n                        return t.reshape ? t.reshape(e) : e;\n                      },\n                      { sourcesBySourceId: r, state: n },\n                    ).sourcesBySourceId;\n                  return ot(\n                    t.reshape({\n                      sourcesBySourceId: o,\n                      sources: Object.values(o),\n                      state: n,\n                    }),\n                  )\n                    .filter(Boolean)\n                    .map(function (e) {\n                      return { source: e, items: e.getItems() };\n                    });\n                })({ collections: e, props: o, state: a.getState() });\n              });\n          }),\n      ),\n    )\n      .then(function (e) {\n        var n;\n        m(\"idle\"), l(e);\n        var f = o.shouldPanelOpen({ state: a.getState() });\n        s(\n          null !== (n = r.isOpen) && void 0 !== n\n            ? n\n            : (o.openOnFocus && !i && f) || f,\n        );\n        var p = Kt(a.getState());\n        if (null !== a.getState().activeItemId && p) {\n          var d = p.item,\n            v = p.itemInputValue,\n            h = p.itemUrl,\n            y = p.source;\n          y.onActive(\n            Sn(\n              {\n                event: t,\n                item: d,\n                itemInputValue: v,\n                itemUrl: h,\n                refresh: c,\n                source: y,\n                state: a.getState(),\n              },\n              u,\n            ),\n          );\n        }\n      })\n      .finally(function () {\n        m(\"idle\"), Pn && o.environment.clearTimeout(Pn);\n      });\n    return a.pendingRequests.add(y);\n  }\n  function kn(e) {\n    return (\n      (kn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      kn(e)\n    );\n  }\n  var Cn = [\"event\", \"props\", \"refresh\", \"store\"];\n  function An(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function xn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? An(Object(n), !0).forEach(function (t) {\n            Nn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : An(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Nn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== kn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== kn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === kn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Tn(e) {\n    return (\n      (Tn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Tn(e)\n    );\n  }\n  var Rn = [\"props\", \"refresh\", \"store\"],\n    qn = [\"inputElement\", \"formElement\", \"panelElement\"],\n    Ln = [\"inputElement\"],\n    Mn = [\"inputElement\", \"maxLength\"],\n    Hn = [\"sourceIndex\"],\n    Un = [\"sourceIndex\"],\n    Fn = [\"item\", \"source\", \"sourceIndex\"];\n  function Bn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Vn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Bn(Object(n), !0).forEach(function (t) {\n            Kn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Bn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Kn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Tn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Tn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Tn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Wn(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function zn(e) {\n    var t = e.props,\n      n = e.refresh,\n      r = e.store,\n      o = Wn(e, Rn),\n      i = function (e, t) {\n        return void 0 !== t ? \"\".concat(e, \"-\").concat(t) : e;\n      };\n    return {\n      getEnvironmentProps: function (e) {\n        var n = e.inputElement,\n          o = e.formElement,\n          i = e.panelElement;\n        function c(e) {\n          (!r.getState().isOpen && r.pendingRequests.isEmpty()) ||\n            e.target === n ||\n            (!1 ===\n              [o, i].some(function (t) {\n                return (n = t) === (r = e.target) || n.contains(r);\n                var n, r;\n              }) &&\n              (r.dispatch(\"blur\", null),\n              t.debug || r.pendingRequests.cancelAll()));\n        }\n        return Vn(\n          {\n            onTouchStart: c,\n            onMouseDown: c,\n            onTouchMove: function (e) {\n              !1 !== r.getState().isOpen &&\n                n === t.environment.document.activeElement &&\n                e.target !== n &&\n                n.blur();\n            },\n          },\n          Wn(e, qn),\n        );\n      },\n      getRootProps: function (e) {\n        return Vn(\n          {\n            role: \"combobox\",\n            \"aria-expanded\": r.getState().isOpen,\n            \"aria-haspopup\": \"listbox\",\n            \"aria-owns\": r.getState().isOpen\n              ? \"\".concat(t.id, \"-list\")\n              : void 0,\n            \"aria-labelledby\": \"\".concat(t.id, \"-label\"),\n          },\n          e,\n        );\n      },\n      getFormProps: function (e) {\n        return (\n          e.inputElement,\n          Vn(\n            {\n              action: \"\",\n              noValidate: !0,\n              role: \"search\",\n              onSubmit: function (i) {\n                var c;\n                i.preventDefault(),\n                  t.onSubmit(\n                    Vn({ event: i, refresh: n, state: r.getState() }, o),\n                  ),\n                  r.dispatch(\"submit\", null),\n                  null === (c = e.inputElement) || void 0 === c || c.blur();\n              },\n              onReset: function (i) {\n                var c;\n                i.preventDefault(),\n                  t.onReset(\n                    Vn({ event: i, refresh: n, state: r.getState() }, o),\n                  ),\n                  r.dispatch(\"reset\", null),\n                  null === (c = e.inputElement) || void 0 === c || c.focus();\n              },\n            },\n            Wn(e, Ln),\n          )\n        );\n      },\n      getLabelProps: function (e) {\n        var n = e || {},\n          r = n.sourceIndex,\n          o = Wn(n, Hn);\n        return Vn(\n          {\n            htmlFor: \"\".concat(i(t.id, r), \"-input\"),\n            id: \"\".concat(i(t.id, r), \"-label\"),\n          },\n          o,\n        );\n      },\n      getInputProps: function (e) {\n        var i;\n        function c(e) {\n          (t.openOnFocus || Boolean(r.getState().query)) &&\n            Dn(\n              Vn(\n                {\n                  event: e,\n                  props: t,\n                  query: r.getState().completion || r.getState().query,\n                  refresh: n,\n                  store: r,\n                },\n                o,\n              ),\n            ),\n            r.dispatch(\"focus\", null);\n        }\n        var a = e || {},\n          u = (a.inputElement, a.maxLength),\n          l = void 0 === u ? 512 : u,\n          s = Wn(a, Mn),\n          f = Kt(r.getState()),\n          p = (function (e) {\n            return Boolean(e && e.match(Wt));\n          })(\n            (null === (i = t.environment.navigator) || void 0 === i\n              ? void 0\n              : i.userAgent) || \"\",\n          ),\n          m = null != f && f.itemUrl && !p ? \"go\" : \"search\";\n        return Vn(\n          {\n            \"aria-autocomplete\": \"both\",\n            \"aria-activedescendant\":\n              r.getState().isOpen && null !== r.getState().activeItemId\n                ? \"\".concat(t.id, \"-item-\").concat(r.getState().activeItemId)\n                : void 0,\n            \"aria-controls\": r.getState().isOpen\n              ? \"\".concat(t.id, \"-list\")\n              : void 0,\n            \"aria-labelledby\": \"\".concat(t.id, \"-label\"),\n            value: r.getState().completion || r.getState().query,\n            id: \"\".concat(t.id, \"-input\"),\n            autoComplete: \"off\",\n            autoCorrect: \"off\",\n            autoCapitalize: \"off\",\n            enterKeyHint: m,\n            spellCheck: \"false\",\n            autoFocus: t.autoFocus,\n            placeholder: t.placeholder,\n            maxLength: l,\n            type: \"search\",\n            onChange: function (e) {\n              Dn(\n                Vn(\n                  {\n                    event: e,\n                    props: t,\n                    query: e.currentTarget.value.slice(0, l),\n                    refresh: n,\n                    store: r,\n                  },\n                  o,\n                ),\n              );\n            },\n            onKeyDown: function (e) {\n              !(function (e) {\n                var t = e.event,\n                  n = e.props,\n                  r = e.refresh,\n                  o = e.store,\n                  i = (function (e, t) {\n                    if (null == e) return {};\n                    var n,\n                      r,\n                      o = (function (e, t) {\n                        if (null == e) return {};\n                        var n,\n                          r,\n                          o = {},\n                          i = Object.keys(e);\n                        for (r = 0; r < i.length; r++)\n                          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n                        return o;\n                      })(e, t);\n                    if (Object.getOwnPropertySymbols) {\n                      var i = Object.getOwnPropertySymbols(e);\n                      for (r = 0; r < i.length; r++)\n                        (n = i[r]),\n                          t.indexOf(n) >= 0 ||\n                            (Object.prototype.propertyIsEnumerable.call(e, n) &&\n                              (o[n] = e[n]));\n                    }\n                    return o;\n                  })(e, Cn);\n                if (\"ArrowUp\" === t.key || \"ArrowDown\" === t.key) {\n                  var c = function () {\n                      var e = n.environment.document.getElementById(\n                        \"\"\n                          .concat(n.id, \"-item-\")\n                          .concat(o.getState().activeItemId),\n                      );\n                      e &&\n                        (e.scrollIntoViewIfNeeded\n                          ? e.scrollIntoViewIfNeeded(!1)\n                          : e.scrollIntoView(!1));\n                    },\n                    a = function () {\n                      var e = Kt(o.getState());\n                      if (null !== o.getState().activeItemId && e) {\n                        var n = e.item,\n                          c = e.itemInputValue,\n                          a = e.itemUrl,\n                          u = e.source;\n                        u.onActive(\n                          xn(\n                            {\n                              event: t,\n                              item: n,\n                              itemInputValue: c,\n                              itemUrl: a,\n                              refresh: r,\n                              source: u,\n                              state: o.getState(),\n                            },\n                            i,\n                          ),\n                        );\n                      }\n                    };\n                  t.preventDefault(),\n                    !1 === o.getState().isOpen &&\n                    (n.openOnFocus || Boolean(o.getState().query))\n                      ? Dn(\n                          xn(\n                            {\n                              event: t,\n                              props: n,\n                              query: o.getState().query,\n                              refresh: r,\n                              store: o,\n                            },\n                            i,\n                          ),\n                        ).then(function () {\n                          o.dispatch(t.key, {\n                            nextActiveItemId: n.defaultActiveItemId,\n                          }),\n                            a(),\n                            setTimeout(c, 0);\n                        })\n                      : (o.dispatch(t.key, {}), a(), c());\n                } else if (\"Escape\" === t.key)\n                  t.preventDefault(),\n                    o.dispatch(t.key, null),\n                    o.pendingRequests.cancelAll();\n                else if (\"Tab\" === t.key)\n                  o.dispatch(\"blur\", null), o.pendingRequests.cancelAll();\n                else if (\"Enter\" === t.key) {\n                  if (\n                    null === o.getState().activeItemId ||\n                    o.getState().collections.every(function (e) {\n                      return 0 === e.items.length;\n                    })\n                  )\n                    return void (n.debug || o.pendingRequests.cancelAll());\n                  t.preventDefault();\n                  var u = Kt(o.getState()),\n                    l = u.item,\n                    s = u.itemInputValue,\n                    f = u.itemUrl,\n                    p = u.source;\n                  if (t.metaKey || t.ctrlKey)\n                    void 0 !== f &&\n                      (p.onSelect(\n                        xn(\n                          {\n                            event: t,\n                            item: l,\n                            itemInputValue: s,\n                            itemUrl: f,\n                            refresh: r,\n                            source: p,\n                            state: o.getState(),\n                          },\n                          i,\n                        ),\n                      ),\n                      n.navigator.navigateNewTab({\n                        itemUrl: f,\n                        item: l,\n                        state: o.getState(),\n                      }));\n                  else if (t.shiftKey)\n                    void 0 !== f &&\n                      (p.onSelect(\n                        xn(\n                          {\n                            event: t,\n                            item: l,\n                            itemInputValue: s,\n                            itemUrl: f,\n                            refresh: r,\n                            source: p,\n                            state: o.getState(),\n                          },\n                          i,\n                        ),\n                      ),\n                      n.navigator.navigateNewWindow({\n                        itemUrl: f,\n                        item: l,\n                        state: o.getState(),\n                      }));\n                  else if (t.altKey);\n                  else {\n                    if (void 0 !== f)\n                      return (\n                        p.onSelect(\n                          xn(\n                            {\n                              event: t,\n                              item: l,\n                              itemInputValue: s,\n                              itemUrl: f,\n                              refresh: r,\n                              source: p,\n                              state: o.getState(),\n                            },\n                            i,\n                          ),\n                        ),\n                        void n.navigator.navigate({\n                          itemUrl: f,\n                          item: l,\n                          state: o.getState(),\n                        })\n                      );\n                    Dn(\n                      xn(\n                        {\n                          event: t,\n                          nextState: { isOpen: !1 },\n                          props: n,\n                          query: s,\n                          refresh: r,\n                          store: o,\n                        },\n                        i,\n                      ),\n                    ).then(function () {\n                      p.onSelect(\n                        xn(\n                          {\n                            event: t,\n                            item: l,\n                            itemInputValue: s,\n                            itemUrl: f,\n                            refresh: r,\n                            source: p,\n                            state: o.getState(),\n                          },\n                          i,\n                        ),\n                      );\n                    });\n                  }\n                }\n              })(Vn({ event: e, props: t, refresh: n, store: r }, o));\n            },\n            onFocus: c,\n            onBlur: lt,\n            onClick: function (n) {\n              e.inputElement !== t.environment.document.activeElement ||\n                r.getState().isOpen ||\n                c(n);\n            },\n          },\n          s,\n        );\n      },\n      getPanelProps: function (e) {\n        return Vn(\n          {\n            onMouseDown: function (e) {\n              e.preventDefault();\n            },\n            onMouseLeave: function () {\n              r.dispatch(\"mouseleave\", null);\n            },\n          },\n          e,\n        );\n      },\n      getListProps: function (e) {\n        var n = e || {},\n          r = n.sourceIndex,\n          o = Wn(n, Un);\n        return Vn(\n          {\n            role: \"listbox\",\n            \"aria-labelledby\": \"\".concat(i(t.id, r), \"-label\"),\n            id: \"\".concat(i(t.id, r), \"-list\"),\n          },\n          o,\n        );\n      },\n      getItemProps: function (e) {\n        var c = e.item,\n          a = e.source,\n          u = e.sourceIndex,\n          l = Wn(e, Fn);\n        return Vn(\n          {\n            id: \"\".concat(i(t.id, u), \"-item-\").concat(c.__autocomplete_id),\n            role: \"option\",\n            \"aria-selected\": r.getState().activeItemId === c.__autocomplete_id,\n            onMouseMove: function (e) {\n              if (c.__autocomplete_id !== r.getState().activeItemId) {\n                r.dispatch(\"mousemove\", c.__autocomplete_id);\n                var t = Kt(r.getState());\n                if (null !== r.getState().activeItemId && t) {\n                  var i = t.item,\n                    a = t.itemInputValue,\n                    u = t.itemUrl,\n                    l = t.source;\n                  l.onActive(\n                    Vn(\n                      {\n                        event: e,\n                        item: i,\n                        itemInputValue: a,\n                        itemUrl: u,\n                        refresh: n,\n                        source: l,\n                        state: r.getState(),\n                      },\n                      o,\n                    ),\n                  );\n                }\n              }\n            },\n            onMouseDown: function (e) {\n              e.preventDefault();\n            },\n            onClick: function (e) {\n              var i = a.getItemInputValue({ item: c, state: r.getState() }),\n                u = a.getItemUrl({ item: c, state: r.getState() });\n              (u\n                ? Promise.resolve()\n                : Dn(\n                    Vn(\n                      {\n                        event: e,\n                        nextState: { isOpen: !1 },\n                        props: t,\n                        query: i,\n                        refresh: n,\n                        store: r,\n                      },\n                      o,\n                    ),\n                  )\n              ).then(function () {\n                a.onSelect(\n                  Vn(\n                    {\n                      event: e,\n                      item: c,\n                      itemInputValue: i,\n                      itemUrl: u,\n                      refresh: n,\n                      source: a,\n                      state: r.getState(),\n                    },\n                    o,\n                  ),\n                );\n              });\n            },\n          },\n          l,\n        );\n      },\n    };\n  }\n  function Jn(e) {\n    return (\n      (Jn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Jn(e)\n    );\n  }\n  function $n(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Zn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? $n(Object(n), !0).forEach(function (t) {\n            Qn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : $n(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Qn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Jn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Jn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Jn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Yn(e) {\n    var t,\n      n,\n      r,\n      o,\n      i = e.plugins,\n      c = e.options,\n      a =\n        null ===\n          (t = ((null === (n = c.__autocomplete_metadata) || void 0 === n\n            ? void 0\n            : n.userAgents) || [])[0]) || void 0 === t\n          ? void 0\n          : t.segment,\n      u = a\n        ? Qn(\n            {},\n            a,\n            Object.keys(\n              (null === (r = c.__autocomplete_metadata) || void 0 === r\n                ? void 0\n                : r.options) || {},\n            ),\n          )\n        : {};\n    return {\n      plugins: i.map(function (e) {\n        return {\n          name: e.name,\n          options: Object.keys(e.__autocomplete_pluginOptions || []),\n        };\n      }),\n      options: Zn({ \"autocomplete-core\": Object.keys(c) }, u),\n      ua: st.concat(\n        (null === (o = c.__autocomplete_metadata) || void 0 === o\n          ? void 0\n          : o.userAgents) || [],\n      ),\n    };\n  }\n  function Gn(e) {\n    var t,\n      n = e.state;\n    return !1 === n.isOpen || null === n.activeItemId\n      ? null\n      : (null === (t = Kt(n)) || void 0 === t ? void 0 : t.itemInputValue) ||\n          null;\n  }\n  function Xn(e) {\n    return (\n      (Xn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Xn(e)\n    );\n  }\n  function er(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function tr(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? er(Object(n), !0).forEach(function (t) {\n            nr(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : er(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function nr(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Xn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Xn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Xn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  var rr = function (e, t) {\n    switch (t.type) {\n      case \"setActiveItemId\":\n      case \"mousemove\":\n        return tr(tr({}, e), {}, { activeItemId: t.payload });\n      case \"setQuery\":\n        return tr(tr({}, e), {}, { query: t.payload, completion: null });\n      case \"setCollections\":\n        return tr(tr({}, e), {}, { collections: t.payload });\n      case \"setIsOpen\":\n        return tr(tr({}, e), {}, { isOpen: t.payload });\n      case \"setStatus\":\n        return tr(tr({}, e), {}, { status: t.payload });\n      case \"setContext\":\n        return tr(tr({}, e), {}, { context: tr(tr({}, e.context), t.payload) });\n      case \"ArrowDown\":\n        var n = tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId: t.payload.hasOwnProperty(\"nextActiveItemId\")\n              ? t.payload.nextActiveItemId\n              : Ht(1, e.activeItemId, ct(e), t.props.defaultActiveItemId),\n          },\n        );\n        return tr(tr({}, n), {}, { completion: Gn({ state: n }) });\n      case \"ArrowUp\":\n        var r = tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId: Ht(\n              -1,\n              e.activeItemId,\n              ct(e),\n              t.props.defaultActiveItemId,\n            ),\n          },\n        );\n        return tr(tr({}, r), {}, { completion: Gn({ state: r }) });\n      case \"Escape\":\n        return e.isOpen\n          ? tr(\n              tr({}, e),\n              {},\n              { activeItemId: null, isOpen: !1, completion: null },\n            )\n          : tr(\n              tr({}, e),\n              {},\n              {\n                activeItemId: null,\n                query: \"\",\n                status: \"idle\",\n                collections: [],\n              },\n            );\n      case \"submit\":\n        return tr(\n          tr({}, e),\n          {},\n          { activeItemId: null, isOpen: !1, status: \"idle\" },\n        );\n      case \"reset\":\n        return tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId:\n              !0 === t.props.openOnFocus ? t.props.defaultActiveItemId : null,\n            status: \"idle\",\n            query: \"\",\n          },\n        );\n      case \"focus\":\n        return tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId: t.props.defaultActiveItemId,\n            isOpen:\n              (t.props.openOnFocus || Boolean(e.query)) &&\n              t.props.shouldPanelOpen({ state: e }),\n          },\n        );\n      case \"blur\":\n        return t.props.debug\n          ? e\n          : tr(tr({}, e), {}, { isOpen: !1, activeItemId: null });\n      case \"mouseleave\":\n        return tr(tr({}, e), {}, { activeItemId: t.props.defaultActiveItemId });\n      default:\n        return (\n          \"The reducer action \".concat(\n            JSON.stringify(t.type),\n            \" is not supported.\",\n          ),\n          e\n        );\n    }\n  };\n  function or(e) {\n    return (\n      (or =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      or(e)\n    );\n  }\n  function ir(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function cr(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? ir(Object(n), !0).forEach(function (t) {\n            ar(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : ir(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function ar(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== or(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== or(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === or(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function ur(e) {\n    var t = [],\n      n = on(e, t),\n      r = (function (e, t, n) {\n        var r,\n          o = t.initialState;\n        return {\n          getState: function () {\n            return o;\n          },\n          dispatch: function (r, i) {\n            var c = (function (e) {\n              for (var t = 1; t < arguments.length; t++) {\n                var n = null != arguments[t] ? arguments[t] : {};\n                t % 2\n                  ? Jt(Object(n), !0).forEach(function (t) {\n                      $t(e, t, n[t]);\n                    })\n                  : Object.getOwnPropertyDescriptors\n                  ? Object.defineProperties(\n                      e,\n                      Object.getOwnPropertyDescriptors(n),\n                    )\n                  : Jt(Object(n)).forEach(function (t) {\n                      Object.defineProperty(\n                        e,\n                        t,\n                        Object.getOwnPropertyDescriptor(n, t),\n                      );\n                    });\n              }\n              return e;\n            })({}, o);\n            (o = e(o, { type: r, props: t, payload: i })),\n              n({ state: o, prevState: c });\n          },\n          pendingRequests:\n            ((r = []),\n            {\n              add: function (e) {\n                return (\n                  r.push(e),\n                  e.finally(function () {\n                    r = r.filter(function (t) {\n                      return t !== e;\n                    });\n                  })\n                );\n              },\n              cancelAll: function () {\n                r.forEach(function (e) {\n                  return e.cancel();\n                });\n              },\n              isEmpty: function () {\n                return 0 === r.length;\n              },\n            }),\n        };\n      })(rr, n, function (e) {\n        var t = e.prevState,\n          r = e.state;\n        n.onStateChange(\n          cr({ prevState: t, state: r, refresh: c, navigator: n.navigator }, o),\n        );\n      }),\n      o = (function (e) {\n        var t = e.store;\n        return {\n          setActiveItemId: function (e) {\n            t.dispatch(\"setActiveItemId\", e);\n          },\n          setQuery: function (e) {\n            t.dispatch(\"setQuery\", e);\n          },\n          setCollections: function (e) {\n            var n = 0,\n              r = e.map(function (e) {\n                return Yt(\n                  Yt({}, e),\n                  {},\n                  {\n                    items: ot(e.items).map(function (e) {\n                      return Yt(Yt({}, e), {}, { __autocomplete_id: n++ });\n                    }),\n                  },\n                );\n              });\n            t.dispatch(\"setCollections\", r);\n          },\n          setIsOpen: function (e) {\n            t.dispatch(\"setIsOpen\", e);\n          },\n          setStatus: function (e) {\n            t.dispatch(\"setStatus\", e);\n          },\n          setContext: function (e) {\n            t.dispatch(\"setContext\", e);\n          },\n        };\n      })({ store: r }),\n      i = zn(cr({ props: n, refresh: c, store: r, navigator: n.navigator }, o));\n    function c() {\n      return Dn(\n        cr(\n          {\n            event: new Event(\"input\"),\n            nextState: { isOpen: r.getState().isOpen },\n            props: n,\n            navigator: n.navigator,\n            query: r.getState().query,\n            refresh: c,\n            store: r,\n          },\n          o,\n        ),\n      );\n    }\n    if (\n      e.insights &&\n      !n.plugins.some(function (e) {\n        return \"aa.algoliaInsightsPlugin\" === e.name;\n      })\n    ) {\n      var a = \"boolean\" == typeof e.insights ? {} : e.insights;\n      n.plugins.push(Rt(a));\n    }\n    return (\n      n.plugins.forEach(function (e) {\n        var r;\n        return null === (r = e.subscribe) || void 0 === r\n          ? void 0\n          : r.call(\n              e,\n              cr(\n                cr({}, o),\n                {},\n                {\n                  navigator: n.navigator,\n                  refresh: c,\n                  onSelect: function (e) {\n                    t.push({ onSelect: e });\n                  },\n                  onActive: function (e) {\n                    t.push({ onActive: e });\n                  },\n                  onResolve: function (e) {\n                    t.push({ onResolve: e });\n                  },\n                },\n              ),\n            );\n      }),\n      (function (e) {\n        var t,\n          n,\n          r = e.metadata,\n          o = e.environment;\n        if (\n          null === (t = o.navigator) ||\n          void 0 === t ||\n          null === (n = t.userAgent) ||\n          void 0 === n\n            ? void 0\n            : n.includes(\"Algolia Crawler\")\n        ) {\n          var i = o.document.createElement(\"meta\"),\n            c = o.document.querySelector(\"head\");\n          (i.name = \"algolia:metadata\"),\n            setTimeout(function () {\n              (i.content = JSON.stringify(r)), c.appendChild(i);\n            }, 0);\n        }\n      })({\n        metadata: Yn({ plugins: n.plugins, options: e }),\n        environment: n.environment,\n      }),\n      cr(cr({ refresh: c, navigator: n.navigator }, i), o)\n    );\n  }\n  function lr(e) {\n    var t = e.translations,\n      n = (void 0 === t ? {} : t).searchByText,\n      r = void 0 === n ? \"Search by\" : n;\n    return Be.createElement(\n      \"a\",\n      {\n        href: \"https://www.algolia.com/ref/docsearch/?utm_source=\".concat(\n          window.location.hostname,\n          \"&utm_medium=referral&utm_content=powered_by&utm_campaign=docsearch\",\n        ),\n        target: \"_blank\",\n        rel: \"noopener noreferrer\",\n      },\n      Be.createElement(\"span\", { className: \"DocSearch-Label\" }, r),\n      Be.createElement(\n        \"svg\",\n        {\n          width: \"77\",\n          height: \"19\",\n          \"aria-label\": \"Algolia\",\n          role: \"img\",\n          id: \"Layer_1\",\n          xmlns: \"http://www.w3.org/2000/svg\",\n          viewBox: \"0 0 2196.2 500\",\n        },\n        Be.createElement(\n          \"defs\",\n          null,\n          Be.createElement(\n            \"style\",\n            null,\n            \".cls-1,.cls-2{fill:#003dff;}.cls-2{fill-rule:evenodd;}\",\n          ),\n        ),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z\",\n        }),\n        Be.createElement(\"rect\", {\n          className: \"cls-1\",\n          x: \"1845.88\",\n          y: \"104.73\",\n          width: \"62.58\",\n          height: \"277.9\",\n          rx: \"5.9\",\n          ry: \"5.9\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-1\",\n          d: \"M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z\",\n        }),\n      ),\n    );\n  }\n  function sr(e) {\n    return Be.createElement(\n      \"svg\",\n      { width: \"15\", height: \"15\", \"aria-label\": e.ariaLabel, role: \"img\" },\n      Be.createElement(\n        \"g\",\n        {\n          fill: \"none\",\n          stroke: \"currentColor\",\n          strokeLinecap: \"round\",\n          strokeLinejoin: \"round\",\n          strokeWidth: \"1.2\",\n        },\n        e.children,\n      ),\n    );\n  }\n  function fr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = n.selectText,\n      o = void 0 === r ? \"to select\" : r,\n      i = n.selectKeyAriaLabel,\n      c = void 0 === i ? \"Enter key\" : i,\n      a = n.navigateText,\n      u = void 0 === a ? \"to navigate\" : a,\n      l = n.navigateUpKeyAriaLabel,\n      s = void 0 === l ? \"Arrow up\" : l,\n      f = n.navigateDownKeyAriaLabel,\n      p = void 0 === f ? \"Arrow down\" : f,\n      m = n.closeText,\n      d = void 0 === m ? \"to close\" : m,\n      v = n.closeKeyAriaLabel,\n      h = void 0 === v ? \"Escape key\" : v,\n      y = n.searchByText,\n      _ = void 0 === y ? \"Search by\" : y;\n    return Be.createElement(\n      Be.Fragment,\n      null,\n      Be.createElement(\n        \"div\",\n        { className: \"DocSearch-Logo\" },\n        Be.createElement(lr, { translations: { searchByText: _ } }),\n      ),\n      Be.createElement(\n        \"ul\",\n        { className: \"DocSearch-Commands\" },\n        Be.createElement(\n          \"li\",\n          null,\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: c },\n              Be.createElement(\"path\", {\n                d: \"M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3\",\n              }),\n            ),\n          ),\n          Be.createElement(\"span\", { className: \"DocSearch-Label\" }, o),\n        ),\n        Be.createElement(\n          \"li\",\n          null,\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: p },\n              Be.createElement(\"path\", { d: \"M7.5 3.5v8M10.5 8.5l-3 3-3-3\" }),\n            ),\n          ),\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: s },\n              Be.createElement(\"path\", { d: \"M7.5 11.5v-8M10.5 6.5l-3-3-3 3\" }),\n            ),\n          ),\n          Be.createElement(\"span\", { className: \"DocSearch-Label\" }, u),\n        ),\n        Be.createElement(\n          \"li\",\n          null,\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: h },\n              Be.createElement(\"path\", {\n                d: \"M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956\",\n              }),\n            ),\n          ),\n          Be.createElement(\"span\", { className: \"DocSearch-Label\" }, d),\n        ),\n      ),\n    );\n  }\n  function pr(e) {\n    var t = e.hit,\n      n = e.children;\n    return Be.createElement(\"a\", { href: t.url }, n);\n  }\n  function mr() {\n    return Be.createElement(\n      \"svg\",\n      { viewBox: \"0 0 38 38\", stroke: \"currentColor\", strokeOpacity: \".5\" },\n      Be.createElement(\n        \"g\",\n        { fill: \"none\", fillRule: \"evenodd\" },\n        Be.createElement(\n          \"g\",\n          { transform: \"translate(1 1)\", strokeWidth: \"2\" },\n          Be.createElement(\"circle\", {\n            strokeOpacity: \".3\",\n            cx: \"18\",\n            cy: \"18\",\n            r: \"18\",\n          }),\n          Be.createElement(\n            \"path\",\n            { d: \"M36 18c0-9.94-8.06-18-18-18\" },\n            Be.createElement(\"animateTransform\", {\n              attributeName: \"transform\",\n              type: \"rotate\",\n              from: \"0 18 18\",\n              to: \"360 18 18\",\n              dur: \"1s\",\n              repeatCount: \"indefinite\",\n            }),\n          ),\n        ),\n      ),\n    );\n  }\n  function dr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\n        \"g\",\n        {\n          stroke: \"currentColor\",\n          fill: \"none\",\n          fillRule: \"evenodd\",\n          strokeLinecap: \"round\",\n          strokeLinejoin: \"round\",\n        },\n        Be.createElement(\"path\", {\n          d: \"M3.18 6.6a8.23 8.23 0 1112.93 9.94h0a8.23 8.23 0 01-11.63 0\",\n        }),\n        Be.createElement(\"path\", {\n          d: \"M6.44 7.25H2.55V3.36M10.45 6v5.6M10.45 11.6L13 13\",\n        }),\n      ),\n    );\n  }\n  function vr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function hr() {\n    return Be.createElement(\n      \"svg\",\n      {\n        className: \"DocSearch-Hit-Select-Icon\",\n        width: \"20\",\n        height: \"20\",\n        viewBox: \"0 0 20 20\",\n      },\n      Be.createElement(\n        \"g\",\n        {\n          stroke: \"currentColor\",\n          fill: \"none\",\n          fillRule: \"evenodd\",\n          strokeLinecap: \"round\",\n          strokeLinejoin: \"round\",\n        },\n        Be.createElement(\"path\", { d: \"M18 3v4c0 2-2 4-4 4H2\" }),\n        Be.createElement(\"path\", { d: \"M8 17l-6-6 6-6\" }),\n      ),\n    );\n  }\n  var yr = function () {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M17 6v12c0 .52-.2 1-1 1H4c-.7 0-1-.33-1-1V2c0-.55.42-1 1-1h8l5 5zM14 8h-3.13c-.51 0-.87-.34-.87-.87V4\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  };\n  function _r(e) {\n    switch (e.type) {\n      case \"lvl1\":\n        return Be.createElement(yr, null);\n      case \"content\":\n        return Be.createElement(gr, null);\n      default:\n        return Be.createElement(br, null);\n    }\n  }\n  function br() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M13 13h4-4V8H7v5h6v4-4H7V8H3h4V3v5h6V3v5h4-4v5zm-6 0v4-4H3h4z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function gr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M17 5H3h14zm0 5H3h14zm0 5H3h14z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function Sr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M10 14.2L5 17l1-5.6-4-4 5.5-.7 2.5-5 2.5 5 5.6.8-4 4 .9 5.5z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function Or() {\n    return Be.createElement(\n      \"svg\",\n      {\n        width: \"40\",\n        height: \"40\",\n        viewBox: \"0 0 20 20\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        stroke: \"currentColor\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      },\n      Be.createElement(\"path\", {\n        d: \"M19 4.8a16 16 0 00-2-1.2m-3.3-1.2A16 16 0 001.1 4.7M16.7 8a12 12 0 00-2.8-1.4M10 6a12 12 0 00-6.7 2M12.3 14.7a4 4 0 00-4.5 0M14.5 11.4A8 8 0 0010 10M3 16L18 2M10 18h0\",\n      }),\n    );\n  }\n  function wr() {\n    return Be.createElement(\n      \"svg\",\n      {\n        width: \"40\",\n        height: \"40\",\n        viewBox: \"0 0 20 20\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        stroke: \"currentColor\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      },\n      Be.createElement(\"path\", {\n        d: \"M15.5 4.8c2 3 1.7 7-1 9.7h0l4.3 4.3-4.3-4.3a7.8 7.8 0 01-9.8 1m-2.2-2.2A7.8 7.8 0 0113.2 2.4M2 18L18 2\",\n      }),\n    );\n  }\n  function Er(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = n.titleText,\n      o = void 0 === r ? \"Unable to fetch results\" : r,\n      i = n.helpText,\n      c = void 0 === i ? \"You might want to check your network connection.\" : i;\n    return Be.createElement(\n      \"div\",\n      { className: \"DocSearch-ErrorScreen\" },\n      Be.createElement(\n        \"div\",\n        { className: \"DocSearch-Screen-Icon\" },\n        Be.createElement(Or, null),\n      ),\n      Be.createElement(\"p\", { className: \"DocSearch-Title\" }, o),\n      Be.createElement(\"p\", { className: \"DocSearch-Help\" }, c),\n    );\n  }\n  var jr = [\"translations\"];\n  function Pr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = $e(e, jr),\n      o = n.noResultsText,\n      i = void 0 === o ? \"No results for\" : o,\n      c = n.suggestedQueryText,\n      a = void 0 === c ? \"Try searching for\" : c,\n      u = n.reportMissingResultsText,\n      l = void 0 === u ? \"Believe this query should return results?\" : u,\n      s = n.reportMissingResultsLinkText,\n      f = void 0 === s ? \"Let us know.\" : s,\n      p = r.state.context.searchSuggestions;\n    return Be.createElement(\n      \"div\",\n      { className: \"DocSearch-NoResults\" },\n      Be.createElement(\n        \"div\",\n        { className: \"DocSearch-Screen-Icon\" },\n        Be.createElement(wr, null),\n      ),\n      Be.createElement(\n        \"p\",\n        { className: \"DocSearch-Title\" },\n        i,\n        ' \"',\n        Be.createElement(\"strong\", null, r.state.query),\n        '\"',\n      ),\n      p &&\n        p.length > 0 &&\n        Be.createElement(\n          \"div\",\n          { className: \"DocSearch-NoResults-Prefill-List\" },\n          Be.createElement(\"p\", { className: \"DocSearch-Help\" }, a, \":\"),\n          Be.createElement(\n            \"ul\",\n            null,\n            p.slice(0, 3).reduce(function (e, t) {\n              return [].concat(\n                (function (e) {\n                  return (\n                    (function (e) {\n                      if (Array.isArray(e)) return Ye(e);\n                    })(e) ||\n                    (function (e) {\n                      if (\n                        (\"undefined\" != typeof Symbol &&\n                          null != e[Symbol.iterator]) ||\n                        null != e[\"@@iterator\"]\n                      )\n                        return Array.from(e);\n                    })(e) ||\n                    Qe(e) ||\n                    (function () {\n                      throw new TypeError(\n                        \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n                      );\n                    })()\n                  );\n                })(e),\n                [\n                  Be.createElement(\n                    \"li\",\n                    { key: t },\n                    Be.createElement(\n                      \"button\",\n                      {\n                        className: \"DocSearch-Prefill\",\n                        key: t,\n                        type: \"button\",\n                        onClick: function () {\n                          r.setQuery(t.toLowerCase() + \" \"),\n                            r.refresh(),\n                            r.inputRef.current.focus();\n                        },\n                      },\n                      t,\n                    ),\n                  ),\n                ],\n              );\n            }, []),\n          ),\n        ),\n      r.getMissingResultsUrl &&\n        Be.createElement(\n          \"p\",\n          { className: \"DocSearch-Help\" },\n          \"\".concat(l, \" \"),\n          Be.createElement(\n            \"a\",\n            {\n              href: r.getMissingResultsUrl({ query: r.state.query }),\n              target: \"_blank\",\n              rel: \"noopener noreferrer\",\n            },\n            f,\n          ),\n        ),\n    );\n  }\n  var Ir = [\"hit\", \"attribute\", \"tagName\"];\n  function Dr(e, t) {\n    return t.split(\".\").reduce(function (e, t) {\n      return null != e && e[t] ? e[t] : null;\n    }, e);\n  }\n  function kr(e) {\n    var t = e.hit,\n      n = e.attribute,\n      r = e.tagName;\n    return g(\n      void 0 === r ? \"span\" : r,\n      We(\n        We({}, $e(e, Ir)),\n        {},\n        {\n          dangerouslySetInnerHTML: {\n            __html: Dr(t, \"_snippetResult.\".concat(n, \".value\")) || Dr(t, n),\n          },\n        },\n      ),\n    );\n  }\n  function Cr(e) {\n    return e.collection && 0 !== e.collection.items.length\n      ? Be.createElement(\n          \"section\",\n          { className: \"DocSearch-Hits\" },\n          Be.createElement(\n            \"div\",\n            { className: \"DocSearch-Hit-source\" },\n            e.title,\n          ),\n          Be.createElement(\n            \"ul\",\n            e.getListProps(),\n            e.collection.items.map(function (t, n) {\n              return Be.createElement(\n                Ar,\n                Je(\n                  { key: [e.title, t.objectID].join(\":\"), item: t, index: n },\n                  e,\n                ),\n              );\n            }),\n          ),\n        )\n      : null;\n  }\n  function Ar(e) {\n    var t = e.item,\n      n = e.index,\n      r = e.renderIcon,\n      o = e.renderAction,\n      i = e.getItemProps,\n      c = e.onItemClick,\n      a = e.collection,\n      u = e.hitComponent,\n      l = Ze(Be.useState(!1), 2),\n      s = l[0],\n      f = l[1],\n      p = Ze(Be.useState(!1), 2),\n      m = p[0],\n      d = p[1],\n      v = Be.useRef(null),\n      h = u;\n    return Be.createElement(\n      \"li\",\n      Je(\n        {\n          className: [\n            \"DocSearch-Hit\",\n            t.__docsearch_parent && \"DocSearch-Hit--Child\",\n            s && \"DocSearch-Hit--deleting\",\n            m && \"DocSearch-Hit--favoriting\",\n          ]\n            .filter(Boolean)\n            .join(\" \"),\n          onTransitionEnd: function () {\n            v.current && v.current();\n          },\n        },\n        i({\n          item: t,\n          source: a.source,\n          onClick: function (e) {\n            c(t, e);\n          },\n        }),\n      ),\n      Be.createElement(\n        h,\n        { hit: t },\n        Be.createElement(\n          \"div\",\n          { className: \"DocSearch-Hit-Container\" },\n          r({ item: t, index: n }),\n          t.hierarchy[t.type] &&\n            \"lvl1\" === t.type &&\n            Be.createElement(\n              \"div\",\n              { className: \"DocSearch-Hit-content-wrapper\" },\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-title\",\n                hit: t,\n                attribute: \"hierarchy.lvl1\",\n              }),\n              t.content &&\n                Be.createElement(kr, {\n                  className: \"DocSearch-Hit-path\",\n                  hit: t,\n                  attribute: \"content\",\n                }),\n            ),\n          t.hierarchy[t.type] &&\n            (\"lvl2\" === t.type ||\n              \"lvl3\" === t.type ||\n              \"lvl4\" === t.type ||\n              \"lvl5\" === t.type ||\n              \"lvl6\" === t.type) &&\n            Be.createElement(\n              \"div\",\n              { className: \"DocSearch-Hit-content-wrapper\" },\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-title\",\n                hit: t,\n                attribute: \"hierarchy.\".concat(t.type),\n              }),\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-path\",\n                hit: t,\n                attribute: \"hierarchy.lvl1\",\n              }),\n            ),\n          \"content\" === t.type &&\n            Be.createElement(\n              \"div\",\n              { className: \"DocSearch-Hit-content-wrapper\" },\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-title\",\n                hit: t,\n                attribute: \"content\",\n              }),\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-path\",\n                hit: t,\n                attribute: \"hierarchy.lvl1\",\n              }),\n            ),\n          o({\n            item: t,\n            runDeleteTransition: function (e) {\n              f(!0), (v.current = e);\n            },\n            runFavoriteTransition: function (e) {\n              d(!0), (v.current = e);\n            },\n          }),\n        ),\n      ),\n    );\n  }\n  function xr(e, t, n) {\n    return e.reduce(function (e, r) {\n      var o = t(r);\n      return (\n        e.hasOwnProperty(o) || (e[o] = []),\n        e[o].length < (n || 5) && e[o].push(r),\n        e\n      );\n    }, {});\n  }\n  function Nr(e) {\n    return e;\n  }\n  function Tr(e) {\n    return 1 === e.button || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;\n  }\n  function Rr() {}\n  var qr = /(<mark>|<\\/mark>)/g,\n    Lr = RegExp(qr.source);\n  function Mr(e) {\n    var t,\n      n,\n      r = e;\n    if (!r.__docsearch_parent && !e._highlightResult) return e.hierarchy.lvl0;\n    var o = (\n      (r.__docsearch_parent\n        ? null === (t = r.__docsearch_parent) ||\n          void 0 === t ||\n          null === (t = t._highlightResult) ||\n          void 0 === t ||\n          null === (t = t.hierarchy) ||\n          void 0 === t\n          ? void 0\n          : t.lvl0\n        : null === (n = e._highlightResult) ||\n          void 0 === n ||\n          null === (n = n.hierarchy) ||\n          void 0 === n\n        ? void 0\n        : n.lvl0) || {}\n    ).value;\n    return o && Lr.test(o) ? o.replace(qr, \"\") : o;\n  }\n  function Hr(e) {\n    return Be.createElement(\n      \"div\",\n      { className: \"DocSearch-Dropdown-Container\" },\n      e.state.collections.map(function (t) {\n        if (0 === t.items.length) return null;\n        var n = Mr(t.items[0]);\n        return Be.createElement(\n          Cr,\n          Je({}, e, {\n            key: t.source.sourceId,\n            title: n,\n            collection: t,\n            renderIcon: function (e) {\n              var n,\n                r = e.item,\n                o = e.index;\n              return Be.createElement(\n                Be.Fragment,\n                null,\n                r.__docsearch_parent &&\n                  Be.createElement(\n                    \"svg\",\n                    { className: \"DocSearch-Hit-Tree\", viewBox: \"0 0 24 54\" },\n                    Be.createElement(\n                      \"g\",\n                      {\n                        stroke: \"currentColor\",\n                        fill: \"none\",\n                        fillRule: \"evenodd\",\n                        strokeLinecap: \"round\",\n                        strokeLinejoin: \"round\",\n                      },\n                      r.__docsearch_parent !==\n                        (null === (n = t.items[o + 1]) || void 0 === n\n                          ? void 0\n                          : n.__docsearch_parent)\n                        ? Be.createElement(\"path\", { d: \"M8 6v21M20 27H8.3\" })\n                        : Be.createElement(\"path\", { d: \"M8 6v42M20 27H8.3\" }),\n                    ),\n                  ),\n                Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-icon\" },\n                  Be.createElement(_r, { type: r.type }),\n                ),\n              );\n            },\n            renderAction: function () {\n              return Be.createElement(\n                \"div\",\n                { className: \"DocSearch-Hit-action\" },\n                Be.createElement(hr, null),\n              );\n            },\n          }),\n        );\n      }),\n      e.resultsFooterComponent &&\n        Be.createElement(\n          \"section\",\n          { className: \"DocSearch-HitsFooter\" },\n          Be.createElement(e.resultsFooterComponent, { state: e.state }),\n        ),\n    );\n  }\n  var Ur = [\"translations\"];\n  function Fr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = $e(e, Ur),\n      o = n.recentSearchesTitle,\n      i = void 0 === o ? \"Recent\" : o,\n      c = n.noRecentSearchesText,\n      a = void 0 === c ? \"No recent searches\" : c,\n      u = n.saveRecentSearchButtonTitle,\n      l = void 0 === u ? \"Save this search\" : u,\n      s = n.removeRecentSearchButtonTitle,\n      f = void 0 === s ? \"Remove this search from history\" : s,\n      p = n.favoriteSearchesTitle,\n      m = void 0 === p ? \"Favorite\" : p,\n      d = n.removeFavoriteSearchButtonTitle,\n      v = void 0 === d ? \"Remove this search from favorites\" : d;\n    return \"idle\" === r.state.status && !1 === r.hasCollections\n      ? r.disableUserPersonalization\n        ? null\n        : Be.createElement(\n            \"div\",\n            { className: \"DocSearch-StartScreen\" },\n            Be.createElement(\"p\", { className: \"DocSearch-Help\" }, a),\n          )\n      : !1 === r.hasCollections\n      ? null\n      : Be.createElement(\n          \"div\",\n          { className: \"DocSearch-Dropdown-Container\" },\n          Be.createElement(\n            Cr,\n            Je({}, r, {\n              title: i,\n              collection: r.state.collections[0],\n              renderIcon: function () {\n                return Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-icon\" },\n                  Be.createElement(dr, null),\n                );\n              },\n              renderAction: function (e) {\n                var t = e.item,\n                  n = e.runFavoriteTransition,\n                  o = e.runDeleteTransition;\n                return Be.createElement(\n                  Be.Fragment,\n                  null,\n                  Be.createElement(\n                    \"div\",\n                    { className: \"DocSearch-Hit-action\" },\n                    Be.createElement(\n                      \"button\",\n                      {\n                        className: \"DocSearch-Hit-action-button\",\n                        title: l,\n                        type: \"submit\",\n                        onClick: function (e) {\n                          e.preventDefault(),\n                            e.stopPropagation(),\n                            n(function () {\n                              r.favoriteSearches.add(t),\n                                r.recentSearches.remove(t),\n                                r.refresh();\n                            });\n                        },\n                      },\n                      Be.createElement(Sr, null),\n                    ),\n                  ),\n                  Be.createElement(\n                    \"div\",\n                    { className: \"DocSearch-Hit-action\" },\n                    Be.createElement(\n                      \"button\",\n                      {\n                        className: \"DocSearch-Hit-action-button\",\n                        title: f,\n                        type: \"submit\",\n                        onClick: function (e) {\n                          e.preventDefault(),\n                            e.stopPropagation(),\n                            o(function () {\n                              r.recentSearches.remove(t), r.refresh();\n                            });\n                        },\n                      },\n                      Be.createElement(vr, null),\n                    ),\n                  ),\n                );\n              },\n            }),\n          ),\n          Be.createElement(\n            Cr,\n            Je({}, r, {\n              title: m,\n              collection: r.state.collections[1],\n              renderIcon: function () {\n                return Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-icon\" },\n                  Be.createElement(Sr, null),\n                );\n              },\n              renderAction: function (e) {\n                var t = e.item,\n                  n = e.runDeleteTransition;\n                return Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-action\" },\n                  Be.createElement(\n                    \"button\",\n                    {\n                      className: \"DocSearch-Hit-action-button\",\n                      title: v,\n                      type: \"submit\",\n                      onClick: function (e) {\n                        e.preventDefault(),\n                          e.stopPropagation(),\n                          n(function () {\n                            r.favoriteSearches.remove(t), r.refresh();\n                          });\n                      },\n                    },\n                    Be.createElement(vr, null),\n                  ),\n                );\n              },\n            }),\n          ),\n        );\n  }\n  var Br = [\"translations\"],\n    Vr = Be.memo(\n      function (e) {\n        var t = e.translations,\n          n = void 0 === t ? {} : t,\n          r = $e(e, Br);\n        if (\"error\" === r.state.status)\n          return Be.createElement(Er, {\n            translations: null == n ? void 0 : n.errorScreen,\n          });\n        var o = r.state.collections.some(function (e) {\n          return e.items.length > 0;\n        });\n        return r.state.query\n          ? !1 === o\n            ? Be.createElement(\n                Pr,\n                Je({}, r, {\n                  translations: null == n ? void 0 : n.noResultsScreen,\n                }),\n              )\n            : Be.createElement(Hr, r)\n          : Be.createElement(\n              Fr,\n              Je({}, r, {\n                hasCollections: o,\n                translations: null == n ? void 0 : n.startScreen,\n              }),\n            );\n      },\n      function (e, t) {\n        return \"loading\" === t.state.status || \"stalled\" === t.state.status;\n      },\n    ),\n    Kr = [\"translations\"];\n  function Wr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = $e(e, Kr),\n      o = n.resetButtonTitle,\n      i = void 0 === o ? \"Clear the query\" : o,\n      c = n.resetButtonAriaLabel,\n      a = void 0 === c ? \"Clear the query\" : c,\n      u = n.cancelButtonText,\n      l = void 0 === u ? \"Cancel\" : u,\n      s = n.cancelButtonAriaLabel,\n      f = void 0 === s ? \"Cancel\" : s,\n      p = n.searchInputLabel,\n      m = void 0 === p ? \"Search\" : p,\n      d = r.getFormProps({ inputElement: r.inputRef.current }).onReset;\n    return (\n      Be.useEffect(\n        function () {\n          r.autoFocus && r.inputRef.current && r.inputRef.current.focus();\n        },\n        [r.autoFocus, r.inputRef],\n      ),\n      Be.useEffect(\n        function () {\n          r.isFromSelection &&\n            r.inputRef.current &&\n            r.inputRef.current.select();\n        },\n        [r.isFromSelection, r.inputRef],\n      ),\n      Be.createElement(\n        Be.Fragment,\n        null,\n        Be.createElement(\n          \"form\",\n          {\n            className: \"DocSearch-Form\",\n            onSubmit: function (e) {\n              e.preventDefault();\n            },\n            onReset: d,\n          },\n          Be.createElement(\n            \"label\",\n            Je({ className: \"DocSearch-MagnifierLabel\" }, r.getLabelProps()),\n            Be.createElement(Xe, null),\n            Be.createElement(\n              \"span\",\n              { className: \"DocSearch-VisuallyHiddenForAccessibility\" },\n              m,\n            ),\n          ),\n          Be.createElement(\n            \"div\",\n            { className: \"DocSearch-LoadingIndicator\" },\n            Be.createElement(mr, null),\n          ),\n          Be.createElement(\n            \"input\",\n            Je(\n              { className: \"DocSearch-Input\", ref: r.inputRef },\n              r.getInputProps({\n                inputElement: r.inputRef.current,\n                autoFocus: r.autoFocus,\n                maxLength: 64,\n              }),\n            ),\n          ),\n          Be.createElement(\n            \"button\",\n            {\n              type: \"reset\",\n              title: i,\n              className: \"DocSearch-Reset\",\n              \"aria-label\": a,\n              hidden: !r.state.query,\n            },\n            Be.createElement(vr, null),\n          ),\n        ),\n        Be.createElement(\n          \"button\",\n          {\n            className: \"DocSearch-Cancel\",\n            type: \"reset\",\n            \"aria-label\": f,\n            onClick: r.onClose,\n          },\n          l,\n        ),\n      )\n    );\n  }\n  var zr = [\"_highlightResult\", \"_snippetResult\"];\n  function Jr(e) {\n    var t = e.key,\n      n = e.limit,\n      r = void 0 === n ? 5 : n,\n      o = (function (e) {\n        return !1 ===\n          (function () {\n            var e = \"__TEST_KEY__\";\n            try {\n              return (\n                localStorage.setItem(e, \"\"), localStorage.removeItem(e), !0\n              );\n            } catch (e) {\n              return !1;\n            }\n          })()\n          ? {\n              setItem: function () {},\n              getItem: function () {\n                return [];\n              },\n            }\n          : {\n              setItem: function (t) {\n                return window.localStorage.setItem(e, JSON.stringify(t));\n              },\n              getItem: function () {\n                var t = window.localStorage.getItem(e);\n                return t ? JSON.parse(t) : [];\n              },\n            };\n      })(t),\n      i = o.getItem().slice(0, r);\n    return {\n      add: function (e) {\n        var t = e,\n          n = (t._highlightResult, t._snippetResult, $e(t, zr)),\n          c = i.findIndex(function (e) {\n            return e.objectID === n.objectID;\n          });\n        c > -1 && i.splice(c, 1),\n          i.unshift(n),\n          (i = i.slice(0, r)),\n          o.setItem(i);\n      },\n      remove: function (e) {\n        (i = i.filter(function (t) {\n          return t.objectID !== e.objectID;\n        })),\n          o.setItem(i);\n      },\n      getAll: function () {\n        return i;\n      },\n    };\n  }\n  function $r(e) {\n    var t,\n      n = \"algoliasearch-client-js-\".concat(e.key),\n      r = function () {\n        return void 0 === t && (t = e.localStorage || window.localStorage), t;\n      },\n      o = function () {\n        return JSON.parse(r().getItem(n) || \"{}\");\n      },\n      i = function (e) {\n        r().setItem(n, JSON.stringify(e));\n      };\n    return {\n      get: function (t, n) {\n        var r =\n          arguments.length > 2 && void 0 !== arguments[2]\n            ? arguments[2]\n            : {\n                miss: function () {\n                  return Promise.resolve();\n                },\n              };\n        return Promise.resolve()\n          .then(function () {\n            !(function () {\n              var t = e.timeToLive ? 1e3 * e.timeToLive : null,\n                n = o(),\n                r = Object.fromEntries(\n                  Object.entries(n).filter(function (e) {\n                    return void 0 !== c(e, 2)[1].timestamp;\n                  }),\n                );\n              if ((i(r), t)) {\n                var a = Object.fromEntries(\n                  Object.entries(r).filter(function (e) {\n                    var n = c(e, 2)[1],\n                      r = new Date().getTime();\n                    return !(n.timestamp + t < r);\n                  }),\n                );\n                i(a);\n              }\n            })();\n            var n = JSON.stringify(t);\n            return o()[n];\n          })\n          .then(function (e) {\n            return Promise.all([e ? e.value : n(), void 0 !== e]);\n          })\n          .then(function (e) {\n            var t = c(e, 2),\n              n = t[0],\n              o = t[1];\n            return Promise.all([n, o || r.miss(n)]);\n          })\n          .then(function (e) {\n            return c(e, 1)[0];\n          });\n      },\n      set: function (e, t) {\n        return Promise.resolve().then(function () {\n          var i = o();\n          return (\n            (i[JSON.stringify(e)] = {\n              timestamp: new Date().getTime(),\n              value: t,\n            }),\n            r().setItem(n, JSON.stringify(i)),\n            t\n          );\n        });\n      },\n      delete: function (e) {\n        return Promise.resolve().then(function () {\n          var t = o();\n          delete t[JSON.stringify(e)], r().setItem(n, JSON.stringify(t));\n        });\n      },\n      clear: function () {\n        return Promise.resolve().then(function () {\n          r().removeItem(n);\n        });\n      },\n    };\n  }\n  function Zr(e) {\n    var t = a(e.caches),\n      n = t.shift();\n    return void 0 === n\n      ? {\n          get: function (e, t) {\n            var n =\n              arguments.length > 2 && void 0 !== arguments[2]\n                ? arguments[2]\n                : {\n                    miss: function () {\n                      return Promise.resolve();\n                    },\n                  };\n            return t()\n              .then(function (e) {\n                return Promise.all([e, n.miss(e)]);\n              })\n              .then(function (e) {\n                return c(e, 1)[0];\n              });\n          },\n          set: function (e, t) {\n            return Promise.resolve(t);\n          },\n          delete: function (e) {\n            return Promise.resolve();\n          },\n          clear: function () {\n            return Promise.resolve();\n          },\n        }\n      : {\n          get: function (e, r) {\n            var o =\n              arguments.length > 2 && void 0 !== arguments[2]\n                ? arguments[2]\n                : {\n                    miss: function () {\n                      return Promise.resolve();\n                    },\n                  };\n            return n.get(e, r, o).catch(function () {\n              return Zr({ caches: t }).get(e, r, o);\n            });\n          },\n          set: function (e, r) {\n            return n.set(e, r).catch(function () {\n              return Zr({ caches: t }).set(e, r);\n            });\n          },\n          delete: function (e) {\n            return n.delete(e).catch(function () {\n              return Zr({ caches: t }).delete(e);\n            });\n          },\n          clear: function () {\n            return n.clear().catch(function () {\n              return Zr({ caches: t }).clear();\n            });\n          },\n        };\n  }\n  function Qr() {\n    var e =\n        arguments.length > 0 && void 0 !== arguments[0]\n          ? arguments[0]\n          : { serializable: !0 },\n      t = {};\n    return {\n      get: function (n, r) {\n        var o =\n            arguments.length > 2 && void 0 !== arguments[2]\n              ? arguments[2]\n              : {\n                  miss: function () {\n                    return Promise.resolve();\n                  },\n                },\n          i = JSON.stringify(n);\n        if (i in t)\n          return Promise.resolve(e.serializable ? JSON.parse(t[i]) : t[i]);\n        var c = r(),\n          a =\n            (o && o.miss) ||\n            function () {\n              return Promise.resolve();\n            };\n        return c\n          .then(function (e) {\n            return a(e);\n          })\n          .then(function () {\n            return c;\n          });\n      },\n      set: function (n, r) {\n        return (\n          (t[JSON.stringify(n)] = e.serializable ? JSON.stringify(r) : r),\n          Promise.resolve(r)\n        );\n      },\n      delete: function (e) {\n        return delete t[JSON.stringify(e)], Promise.resolve();\n      },\n      clear: function () {\n        return (t = {}), Promise.resolve();\n      },\n    };\n  }\n  function Yr(e) {\n    for (var t = e.length - 1; t > 0; t--) {\n      var n = Math.floor(Math.random() * (t + 1)),\n        r = e[t];\n      (e[t] = e[n]), (e[n] = r);\n    }\n    return e;\n  }\n  function Gr(e, t) {\n    return t\n      ? (Object.keys(t).forEach(function (n) {\n          e[n] = t[n](e);\n        }),\n        e)\n      : e;\n  }\n  function Xr(e) {\n    for (\n      var t = arguments.length, n = new Array(t > 1 ? t - 1 : 0), r = 1;\n      r < t;\n      r++\n    )\n      n[r - 1] = arguments[r];\n    var o = 0;\n    return e.replace(/%s/g, function () {\n      return encodeURIComponent(n[o++]);\n    });\n  }\n  var eo = 0,\n    to = 1;\n  function no(e, t) {\n    var n = e || {},\n      r = n.data || {};\n    return (\n      Object.keys(n).forEach(function (e) {\n        -1 ===\n          [\n            \"timeout\",\n            \"headers\",\n            \"queryParameters\",\n            \"data\",\n            \"cacheable\",\n          ].indexOf(e) && (r[e] = n[e]);\n      }),\n      {\n        data: Object.entries(r).length > 0 ? r : void 0,\n        timeout: n.timeout || t,\n        headers: n.headers || {},\n        queryParameters: n.queryParameters || {},\n        cacheable: n.cacheable,\n      }\n    );\n  }\n  var ro = { Read: 1, Write: 2, Any: 3 };\n  function oo(e) {\n    var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 1;\n    return t(t({}, e), {}, { status: n, lastUpdate: Date.now() });\n  }\n  function io(e) {\n    return \"string\" == typeof e\n      ? { protocol: \"https\", url: e, accept: ro.Any }\n      : {\n          protocol: e.protocol || \"https\",\n          url: e.url,\n          accept: e.accept || ro.Any,\n        };\n  }\n  var co = \"GET\",\n    ao = \"POST\";\n  function uo(e, n, r, o) {\n    var i = [],\n      c = (function (e, n) {\n        if (e.method !== co && (void 0 !== e.data || void 0 !== n.data)) {\n          var r = Array.isArray(e.data) ? e.data : t(t({}, e.data), n.data);\n          return JSON.stringify(r);\n        }\n      })(r, o),\n      u = (function (e, n) {\n        var r = t(t({}, e.headers), n.headers),\n          o = {};\n        return (\n          Object.keys(r).forEach(function (e) {\n            var t = r[e];\n            o[e.toLowerCase()] = t;\n          }),\n          o\n        );\n      })(e, o),\n      l = r.method,\n      s = r.method !== co ? {} : t(t({}, r.data), o.data),\n      f = t(\n        t(t({ \"x-algolia-agent\": e.userAgent.value }, e.queryParameters), s),\n        o.queryParameters,\n      ),\n      p = 0,\n      m = function t(n, a) {\n        var s = n.pop();\n        if (void 0 === s)\n          throw {\n            name: \"RetryError\",\n            message:\n              \"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.\",\n            transporterStackTrace: po(i),\n          };\n        var m = {\n            data: c,\n            headers: u,\n            method: l,\n            url: so(s, r.path, f),\n            connectTimeout: a(p, e.timeouts.connect),\n            responseTimeout: a(p, o.timeout),\n          },\n          d = function (e) {\n            var t = { request: m, response: e, host: s, triesLeft: n.length };\n            return i.push(t), t;\n          },\n          v = {\n            onSuccess: function (e) {\n              return (function (e) {\n                try {\n                  return JSON.parse(e.content);\n                } catch (t) {\n                  throw (function (e, t) {\n                    return {\n                      name: \"DeserializationError\",\n                      message: e,\n                      response: t,\n                    };\n                  })(t.message, e);\n                }\n              })(e);\n            },\n            onRetry: function (r) {\n              var o = d(r);\n              return (\n                r.isTimedOut && p++,\n                Promise.all([\n                  e.logger.info(\"Retryable failure\", mo(o)),\n                  e.hostsCache.set(s, oo(s, r.isTimedOut ? 3 : 2)),\n                ]).then(function () {\n                  return t(n, a);\n                })\n              );\n            },\n            onFail: function (e) {\n              throw (\n                (d(e),\n                (function (e, t) {\n                  var n = e.content,\n                    r = e.status,\n                    o = n;\n                  try {\n                    o = JSON.parse(n).message;\n                  } catch (n) {}\n                  return (function (e, t, n) {\n                    return {\n                      name: \"ApiError\",\n                      message: e,\n                      status: t,\n                      transporterStackTrace: n,\n                    };\n                  })(o, r, t);\n                })(e, po(i)))\n              );\n            },\n          };\n        return e.requester.send(m).then(function (e) {\n          return (function (e, t) {\n            return (function (e) {\n              var t = e.status;\n              return (\n                e.isTimedOut ||\n                (function (e) {\n                  var t = e.isTimedOut,\n                    n = e.status;\n                  return !t && 0 == ~~n;\n                })(e) ||\n                (2 != ~~(t / 100) && 4 != ~~(t / 100))\n              );\n            })(e)\n              ? t.onRetry(e)\n              : ((n = e),\n                2 == ~~(n.status / 100) ? t.onSuccess(e) : t.onFail(e));\n            var n;\n          })(e, v);\n        });\n      };\n    return (function (e, t) {\n      return Promise.all(\n        t.map(function (t) {\n          return e.get(t, function () {\n            return Promise.resolve(oo(t));\n          });\n        }),\n      ).then(function (e) {\n        var n = e.filter(function (e) {\n            return (function (e) {\n              return 1 === e.status || Date.now() - e.lastUpdate > 12e4;\n            })(e);\n          }),\n          r = e.filter(function (e) {\n            return (function (e) {\n              return 3 === e.status && Date.now() - e.lastUpdate <= 12e4;\n            })(e);\n          }),\n          o = [].concat(a(n), a(r));\n        return {\n          getTimeout: function (e, t) {\n            return (0 === r.length && 0 === e ? 1 : r.length + 3 + e) * t;\n          },\n          statelessHosts:\n            o.length > 0\n              ? o.map(function (e) {\n                  return io(e);\n                })\n              : t,\n        };\n      });\n    })(e.hostsCache, n).then(function (e) {\n      return m(a(e.statelessHosts).reverse(), e.getTimeout);\n    });\n  }\n  function lo(e) {\n    var t = {\n      value: \"Algolia for JavaScript (\".concat(e, \")\"),\n      add: function (e) {\n        var n = \"; \"\n          .concat(e.segment)\n          .concat(void 0 !== e.version ? \" (\".concat(e.version, \")\") : \"\");\n        return (\n          -1 === t.value.indexOf(n) && (t.value = \"\".concat(t.value).concat(n)),\n          t\n        );\n      },\n    };\n    return t;\n  }\n  function so(e, t, n) {\n    var r = fo(n),\n      o = \"\"\n        .concat(e.protocol, \"://\")\n        .concat(e.url, \"/\")\n        .concat(\"/\" === t.charAt(0) ? t.substr(1) : t);\n    return r.length && (o += \"?\".concat(r)), o;\n  }\n  function fo(e) {\n    return Object.keys(e)\n      .map(function (t) {\n        return Xr(\n          \"%s=%s\",\n          t,\n          ((n = e[t]),\n          \"[object Object]\" === Object.prototype.toString.call(n) ||\n          \"[object Array]\" === Object.prototype.toString.call(n)\n            ? JSON.stringify(e[t])\n            : e[t]),\n        );\n        var n;\n      })\n      .join(\"&\");\n  }\n  function po(e) {\n    return e.map(function (e) {\n      return mo(e);\n    });\n  }\n  function mo(e) {\n    var n = e.request.headers[\"x-algolia-api-key\"]\n      ? { \"x-algolia-api-key\": \"*****\" }\n      : {};\n    return t(\n      t({}, e),\n      {},\n      {\n        request: t(\n          t({}, e.request),\n          {},\n          { headers: t(t({}, e.request.headers), n) },\n        ),\n      },\n    );\n  }\n  var vo = function (e) {\n      return function (t, n) {\n        return t.method === co\n          ? e.transporter.read(t, n)\n          : e.transporter.write(t, n);\n      };\n    },\n    ho = function (e) {\n      return function (t) {\n        var n =\n          arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {};\n        return Gr(\n          { transporter: e.transporter, appId: e.appId, indexName: t },\n          n.methods,\n        );\n      };\n    },\n    yo = function (e) {\n      return function (n, r) {\n        var o = n.map(function (e) {\n          return t(t({}, e), {}, { params: fo(e.params || {}) });\n        });\n        return e.transporter.read(\n          {\n            method: ao,\n            path: \"1/indexes/*/queries\",\n            data: { requests: o },\n            cacheable: !0,\n          },\n          r,\n        );\n      };\n    },\n    _o = function (e) {\n      return function (n, r) {\n        return Promise.all(\n          n.map(function (n) {\n            var o = n.params,\n              c = o.facetName,\n              a = o.facetQuery,\n              u = i(o, Ve);\n            return ho(e)(n.indexName, {\n              methods: { searchForFacetValues: So },\n            }).searchForFacetValues(c, a, t(t({}, r), u));\n          }),\n        );\n      };\n    },\n    bo = function (e) {\n      return function (t, n, r) {\n        return e.transporter.read(\n          {\n            method: ao,\n            path: Xr(\"1/answers/%s/prediction\", e.indexName),\n            data: { query: t, queryLanguages: n },\n            cacheable: !0,\n          },\n          r,\n        );\n      };\n    },\n    go = function (e) {\n      return function (t, n) {\n        return e.transporter.read(\n          {\n            method: ao,\n            path: Xr(\"1/indexes/%s/query\", e.indexName),\n            data: { query: t },\n            cacheable: !0,\n          },\n          n,\n        );\n      };\n    },\n    So = function (e) {\n      return function (t, n, r) {\n        return e.transporter.read(\n          {\n            method: ao,\n            path: Xr(\"1/indexes/%s/facets/%s/query\", e.indexName, t),\n            data: { facetQuery: n },\n            cacheable: !0,\n          },\n          r,\n        );\n      };\n    };\n  function Oo(e, n, r) {\n    var o = {\n      appId: e,\n      apiKey: n,\n      timeouts: { connect: 1, read: 2, write: 30 },\n      requester: {\n        send: function (e) {\n          return new Promise(function (t) {\n            var n = new XMLHttpRequest();\n            n.open(e.method, e.url, !0),\n              Object.keys(e.headers).forEach(function (t) {\n                return n.setRequestHeader(t, e.headers[t]);\n              });\n            var r,\n              o = function (e, r) {\n                return setTimeout(function () {\n                  n.abort(), t({ status: 0, content: r, isTimedOut: !0 });\n                }, 1e3 * e);\n              },\n              i = o(e.connectTimeout, \"Connection timeout\");\n            (n.onreadystatechange = function () {\n              n.readyState > n.OPENED &&\n                void 0 === r &&\n                (clearTimeout(i), (r = o(e.responseTimeout, \"Socket timeout\")));\n            }),\n              (n.onerror = function () {\n                0 === n.status &&\n                  (clearTimeout(i),\n                  clearTimeout(r),\n                  t({\n                    content: n.responseText || \"Network request failed\",\n                    status: n.status,\n                    isTimedOut: !1,\n                  }));\n              }),\n              (n.onload = function () {\n                clearTimeout(i),\n                  clearTimeout(r),\n                  t({\n                    content: n.responseText,\n                    status: n.status,\n                    isTimedOut: !1,\n                  });\n              }),\n              n.send(e.data);\n          });\n        },\n      },\n      logger:\n        (3,\n        {\n          debug: function (e, t) {\n            return Promise.resolve();\n          },\n          info: function (e, t) {\n            return Promise.resolve();\n          },\n          error: function (e, t) {\n            return console.error(e, t), Promise.resolve();\n          },\n        }),\n      responsesCache: Qr(),\n      requestsCache: Qr({ serializable: !1 }),\n      hostsCache: Zr({ caches: [$r({ key: \"4.19.1-\".concat(e) }), Qr()] }),\n      userAgent: lo(\"4.19.1\").add({ segment: \"Browser\", version: \"lite\" }),\n      authMode: eo,\n    };\n    return (function (e) {\n      var n = e.appId,\n        r = (function (e, t, n) {\n          var r = { \"x-algolia-api-key\": n, \"x-algolia-application-id\": t };\n          return {\n            headers: function () {\n              return e === to ? r : {};\n            },\n            queryParameters: function () {\n              return e === eo ? r : {};\n            },\n          };\n        })(void 0 !== e.authMode ? e.authMode : to, n, e.apiKey),\n        o = (function (e) {\n          var t = e.hostsCache,\n            n = e.logger,\n            r = e.requester,\n            o = e.requestsCache,\n            i = e.responsesCache,\n            a = e.timeouts,\n            u = e.userAgent,\n            l = e.hosts,\n            s = e.queryParameters,\n            f = {\n              hostsCache: t,\n              logger: n,\n              requester: r,\n              requestsCache: o,\n              responsesCache: i,\n              timeouts: a,\n              userAgent: u,\n              headers: e.headers,\n              queryParameters: s,\n              hosts: l.map(function (e) {\n                return io(e);\n              }),\n              read: function (e, t) {\n                var n = no(t, f.timeouts.read),\n                  r = function () {\n                    return uo(\n                      f,\n                      f.hosts.filter(function (e) {\n                        return 0 != (e.accept & ro.Read);\n                      }),\n                      e,\n                      n,\n                    );\n                  };\n                if (!0 !== (void 0 !== n.cacheable ? n.cacheable : e.cacheable))\n                  return r();\n                var o = {\n                  request: e,\n                  mappedRequestOptions: n,\n                  transporter: {\n                    queryParameters: f.queryParameters,\n                    headers: f.headers,\n                  },\n                };\n                return f.responsesCache.get(\n                  o,\n                  function () {\n                    return f.requestsCache.get(o, function () {\n                      return f.requestsCache\n                        .set(o, r())\n                        .then(\n                          function (e) {\n                            return Promise.all([f.requestsCache.delete(o), e]);\n                          },\n                          function (e) {\n                            return Promise.all([\n                              f.requestsCache.delete(o),\n                              Promise.reject(e),\n                            ]);\n                          },\n                        )\n                        .then(function (e) {\n                          var t = c(e, 2);\n                          return t[0], t[1];\n                        });\n                    });\n                  },\n                  {\n                    miss: function (e) {\n                      return f.responsesCache.set(o, e);\n                    },\n                  },\n                );\n              },\n              write: function (e, t) {\n                return uo(\n                  f,\n                  f.hosts.filter(function (e) {\n                    return 0 != (e.accept & ro.Write);\n                  }),\n                  e,\n                  no(t, f.timeouts.write),\n                );\n              },\n            };\n          return f;\n        })(\n          t(\n            t(\n              {\n                hosts: [\n                  { url: \"\".concat(n, \"-dsn.algolia.net\"), accept: ro.Read },\n                  { url: \"\".concat(n, \".algolia.net\"), accept: ro.Write },\n                ].concat(\n                  Yr([\n                    { url: \"\".concat(n, \"-1.algolianet.com\") },\n                    { url: \"\".concat(n, \"-2.algolianet.com\") },\n                    { url: \"\".concat(n, \"-3.algolianet.com\") },\n                  ]),\n                ),\n              },\n              e,\n            ),\n            {},\n            {\n              headers: t(\n                t({}, r.headers()),\n                {},\n                { \"content-type\": \"application/x-www-form-urlencoded\" },\n                e.headers,\n              ),\n              queryParameters: t(t({}, r.queryParameters()), e.queryParameters),\n            },\n          ),\n        ),\n        i = {\n          transporter: o,\n          appId: n,\n          addAlgoliaAgent: function (e, t) {\n            o.userAgent.add({ segment: e, version: t });\n          },\n          clearCache: function () {\n            return Promise.all([\n              o.requestsCache.clear(),\n              o.responsesCache.clear(),\n            ]).then(function () {});\n          },\n        };\n      return Gr(i, e.methods);\n    })(\n      t(\n        t(t({}, o), r),\n        {},\n        {\n          methods: {\n            search: yo,\n            searchForFacetValues: _o,\n            multipleQueries: yo,\n            multipleSearchForFacetValues: _o,\n            customRequest: vo,\n            initIndex: function (e) {\n              return function (t) {\n                return ho(e)(t, {\n                  methods: {\n                    search: go,\n                    searchForFacetValues: So,\n                    findAnswers: bo,\n                  },\n                });\n              };\n            },\n          },\n        },\n      ),\n    );\n  }\n  Oo.version = \"4.19.1\";\n  var wo = [\"footer\", \"searchBox\"];\n  function Eo(e) {\n    var t = e.appId,\n      n = e.apiKey,\n      r = e.indexName,\n      o = e.placeholder,\n      i = void 0 === o ? \"Search docs\" : o,\n      c = e.searchParameters,\n      a = e.maxResultsPerGroup,\n      u = e.onClose,\n      l = void 0 === u ? Rr : u,\n      s = e.transformItems,\n      f = void 0 === s ? Nr : s,\n      p = e.hitComponent,\n      m = void 0 === p ? pr : p,\n      d = e.resultsFooterComponent,\n      v =\n        void 0 === d\n          ? function () {\n              return null;\n            }\n          : d,\n      h = e.navigator,\n      y = e.initialScrollY,\n      _ = void 0 === y ? 0 : y,\n      b = e.transformSearchClient,\n      g = void 0 === b ? Nr : b,\n      S = e.disableUserPersonalization,\n      O = void 0 !== S && S,\n      w = e.initialQuery,\n      E = void 0 === w ? \"\" : w,\n      j = e.translations,\n      P = void 0 === j ? {} : j,\n      I = e.getMissingResultsUrl,\n      D = e.insights,\n      k = void 0 !== D && D,\n      C = P.footer,\n      A = P.searchBox,\n      x = $e(P, wo),\n      N = Ze(\n        Be.useState({\n          query: \"\",\n          collections: [],\n          completion: null,\n          context: {},\n          isOpen: !1,\n          activeItemId: null,\n          status: \"idle\",\n        }),\n        2,\n      ),\n      T = N[0],\n      R = N[1],\n      q = Be.useRef(null),\n      L = Be.useRef(null),\n      M = Be.useRef(null),\n      H = Be.useRef(null),\n      U = Be.useRef(null),\n      F = Be.useRef(10),\n      B = Be.useRef(\n        \"undefined\" != typeof window\n          ? window.getSelection().toString().slice(0, 64)\n          : \"\",\n      ).current,\n      V = Be.useRef(E || B).current,\n      K = (function (e, t, n) {\n        return Be.useMemo(\n          function () {\n            var r = Oo(e, t);\n            return (\n              r.addAlgoliaAgent(\"docsearch\", \"3.6.1\"),\n              !1 ===\n                /docsearch.js \\(.*\\)/.test(r.transporter.userAgent.value) &&\n                r.addAlgoliaAgent(\"docsearch-react\", \"3.6.1\"),\n              n(r)\n            );\n          },\n          [e, t, n],\n        );\n      })(t, n, g),\n      W = Be.useRef(\n        Jr({ key: \"__DOCSEARCH_FAVORITE_SEARCHES__\".concat(r), limit: 10 }),\n      ).current,\n      z = Be.useRef(\n        Jr({\n          key: \"__DOCSEARCH_RECENT_SEARCHES__\".concat(r),\n          limit: 0 === W.getAll().length ? 7 : 4,\n        }),\n      ).current,\n      J = Be.useCallback(\n        function (e) {\n          if (!O) {\n            var t = \"content\" === e.type ? e.__docsearch_parent : e;\n            t &&\n              -1 ===\n                W.getAll().findIndex(function (e) {\n                  return e.objectID === t.objectID;\n                }) &&\n              z.add(t);\n          }\n        },\n        [W, z, O],\n      ),\n      $ = Be.useCallback(\n        function (e) {\n          if (T.context.algoliaInsightsPlugin && e.__autocomplete_id) {\n            var t = e,\n              n = {\n                eventName: \"Item Selected\",\n                index: t.__autocomplete_indexName,\n                items: [t],\n                positions: [e.__autocomplete_id],\n                queryID: t.__autocomplete_queryID,\n              };\n            T.context.algoliaInsightsPlugin.insights.clickedObjectIDsAfterSearch(\n              n,\n            );\n          }\n        },\n        [T.context.algoliaInsightsPlugin],\n      ),\n      Z = Be.useMemo(\n        function () {\n          return ur({\n            id: \"docsearch\",\n            defaultActiveItemId: 0,\n            placeholder: i,\n            openOnFocus: !0,\n            initialState: { query: V, context: { searchSuggestions: [] } },\n            insights: k,\n            navigator: h,\n            onStateChange: function (e) {\n              R(e.state);\n            },\n            getSources: function (e) {\n              var o = e.query,\n                i = e.state,\n                u = e.setContext,\n                s = e.setStatus;\n              if (!o)\n                return O\n                  ? []\n                  : [\n                      {\n                        sourceId: \"recentSearches\",\n                        onSelect: function (e) {\n                          var t = e.item,\n                            n = e.event;\n                          J(t), Tr(n) || l();\n                        },\n                        getItemUrl: function (e) {\n                          return e.item.url;\n                        },\n                        getItems: function () {\n                          return z.getAll();\n                        },\n                      },\n                      {\n                        sourceId: \"favoriteSearches\",\n                        onSelect: function (e) {\n                          var t = e.item,\n                            n = e.event;\n                          J(t), Tr(n) || l();\n                        },\n                        getItemUrl: function (e) {\n                          return e.item.url;\n                        },\n                        getItems: function () {\n                          return W.getAll();\n                        },\n                      },\n                    ];\n              var p = Boolean(k);\n              return K.search([\n                {\n                  query: o,\n                  indexName: r,\n                  params: We(\n                    {\n                      attributesToRetrieve: [\n                        \"hierarchy.lvl0\",\n                        \"hierarchy.lvl1\",\n                        \"hierarchy.lvl2\",\n                        \"hierarchy.lvl3\",\n                        \"hierarchy.lvl4\",\n                        \"hierarchy.lvl5\",\n                        \"hierarchy.lvl6\",\n                        \"content\",\n                        \"type\",\n                        \"url\",\n                      ],\n                      attributesToSnippet: [\n                        \"hierarchy.lvl1:\".concat(F.current),\n                        \"hierarchy.lvl2:\".concat(F.current),\n                        \"hierarchy.lvl3:\".concat(F.current),\n                        \"hierarchy.lvl4:\".concat(F.current),\n                        \"hierarchy.lvl5:\".concat(F.current),\n                        \"hierarchy.lvl6:\".concat(F.current),\n                        \"content:\".concat(F.current),\n                      ],\n                      snippetEllipsisText: \"…\",\n                      highlightPreTag: \"<mark>\",\n                      highlightPostTag: \"</mark>\",\n                      hitsPerPage: 20,\n                      clickAnalytics: p,\n                    },\n                    c,\n                  ),\n                },\n              ])\n                .catch(function (e) {\n                  throw (\"RetryError\" === e.name && s(\"error\"), e);\n                })\n                .then(function (e) {\n                  var o = e.results[0],\n                    c = o.hits,\n                    s = o.nbHits,\n                    m = xr(\n                      c,\n                      function (e) {\n                        return Mr(e);\n                      },\n                      a,\n                    );\n                  i.context.searchSuggestions.length < Object.keys(m).length &&\n                    u({ searchSuggestions: Object.keys(m) }),\n                    u({ nbHits: s });\n                  var d = {};\n                  return (\n                    p &&\n                      (d = {\n                        __autocomplete_indexName: r,\n                        __autocomplete_queryID: o.queryID,\n                        __autocomplete_algoliaCredentials: {\n                          appId: t,\n                          apiKey: n,\n                        },\n                      }),\n                    Object.values(m).map(function (e, t) {\n                      return {\n                        sourceId: \"hits\".concat(t),\n                        onSelect: function (e) {\n                          var t = e.item,\n                            n = e.event;\n                          J(t), Tr(n) || l();\n                        },\n                        getItemUrl: function (e) {\n                          return e.item.url;\n                        },\n                        getItems: function () {\n                          return Object.values(\n                            xr(\n                              e,\n                              function (e) {\n                                return e.hierarchy.lvl1;\n                              },\n                              a,\n                            ),\n                          )\n                            .map(f)\n                            .map(function (e) {\n                              return e.map(function (t) {\n                                var n = null,\n                                  r = e.find(function (e) {\n                                    return (\n                                      \"lvl1\" === e.type &&\n                                      e.hierarchy.lvl1 === t.hierarchy.lvl1\n                                    );\n                                  });\n                                return (\n                                  \"lvl1\" !== t.type && r && (n = r),\n                                  We(\n                                    We({}, t),\n                                    {},\n                                    { __docsearch_parent: n },\n                                    d,\n                                  )\n                                );\n                              });\n                            })\n                            .flat();\n                        },\n                      };\n                    })\n                  );\n                });\n            },\n          });\n        },\n        [r, c, a, K, l, z, W, J, V, i, h, f, O, k, t, n],\n      ),\n      Q = Z.getEnvironmentProps,\n      Y = Z.getRootProps,\n      G = Z.refresh;\n    return (\n      (function (e) {\n        var t = e.getEnvironmentProps,\n          n = e.panelElement,\n          r = e.formElement,\n          o = e.inputElement;\n        Be.useEffect(\n          function () {\n            if (n && r && o) {\n              var e = t({ panelElement: n, formElement: r, inputElement: o }),\n                i = e.onTouchStart,\n                c = e.onTouchMove;\n              return (\n                window.addEventListener(\"touchstart\", i),\n                window.addEventListener(\"touchmove\", c),\n                function () {\n                  window.removeEventListener(\"touchstart\", i),\n                    window.removeEventListener(\"touchmove\", c);\n                }\n              );\n            }\n          },\n          [t, n, r, o],\n        );\n      })({\n        getEnvironmentProps: Q,\n        panelElement: H.current,\n        formElement: M.current,\n        inputElement: U.current,\n      }),\n      (function (e) {\n        var t = e.container;\n        Be.useEffect(\n          function () {\n            if (t) {\n              var e = t.querySelectorAll(\n                  \"a[href]:not([disabled]), button:not([disabled]), input:not([disabled])\",\n                ),\n                n = e[0],\n                r = e[e.length - 1];\n              return (\n                t.addEventListener(\"keydown\", o),\n                function () {\n                  t.removeEventListener(\"keydown\", o);\n                }\n              );\n            }\n            function o(e) {\n              \"Tab\" === e.key &&\n                (e.shiftKey\n                  ? document.activeElement === n &&\n                    (e.preventDefault(), r.focus())\n                  : document.activeElement === r &&\n                    (e.preventDefault(), n.focus()));\n            }\n          },\n          [t],\n        );\n      })({ container: q.current }),\n      Be.useEffect(function () {\n        return (\n          document.body.classList.add(\"DocSearch--active\"),\n          function () {\n            var e, t;\n            document.body.classList.remove(\"DocSearch--active\"),\n              null === (e = (t = window).scrollTo) ||\n                void 0 === e ||\n                e.call(t, 0, _);\n          }\n        );\n      }, []),\n      Be.useEffect(function () {\n        window.matchMedia(\"(max-width: 768px)\").matches && (F.current = 5);\n      }, []),\n      Be.useEffect(\n        function () {\n          H.current && (H.current.scrollTop = 0);\n        },\n        [T.query],\n      ),\n      Be.useEffect(\n        function () {\n          V.length > 0 && (G(), U.current && U.current.focus());\n        },\n        [V, G],\n      ),\n      Be.useEffect(function () {\n        function e() {\n          if (L.current) {\n            var e = 0.01 * window.innerHeight;\n            L.current.style.setProperty(\"--docsearch-vh\", \"\".concat(e, \"px\"));\n          }\n        }\n        return (\n          e(),\n          window.addEventListener(\"resize\", e),\n          function () {\n            window.removeEventListener(\"resize\", e);\n          }\n        );\n      }, []),\n      Be.createElement(\n        \"div\",\n        Je({ ref: q }, Y({ \"aria-expanded\": !0 }), {\n          className: [\n            \"DocSearch\",\n            \"DocSearch-Container\",\n            \"stalled\" === T.status && \"DocSearch-Container--Stalled\",\n            \"error\" === T.status && \"DocSearch-Container--Errored\",\n          ]\n            .filter(Boolean)\n            .join(\" \"),\n          role: \"button\",\n          tabIndex: 0,\n          onMouseDown: function (e) {\n            e.target === e.currentTarget && l();\n          },\n        }),\n        Be.createElement(\n          \"div\",\n          { className: \"DocSearch-Modal\", ref: L },\n          Be.createElement(\n            \"header\",\n            { className: \"DocSearch-SearchBar\", ref: M },\n            Be.createElement(\n              Wr,\n              Je({}, Z, {\n                state: T,\n                autoFocus: 0 === V.length,\n                inputRef: U,\n                isFromSelection: Boolean(V) && V === B,\n                translations: A,\n                onClose: l,\n              }),\n            ),\n          ),\n          Be.createElement(\n            \"div\",\n            { className: \"DocSearch-Dropdown\", ref: H },\n            Be.createElement(\n              Vr,\n              Je({}, Z, {\n                indexName: r,\n                state: T,\n                hitComponent: m,\n                resultsFooterComponent: v,\n                disableUserPersonalization: O,\n                recentSearches: z,\n                favoriteSearches: W,\n                inputRef: U,\n                translations: x,\n                getMissingResultsUrl: I,\n                onItemClick: function (e, t) {\n                  $(e), J(e), Tr(t) || l();\n                },\n              }),\n            ),\n          ),\n          Be.createElement(\n            \"footer\",\n            { className: \"DocSearch-Footer\" },\n            Be.createElement(fr, { translations: C }),\n          ),\n        ),\n      )\n    );\n  }\n  function jo(e) {\n    var t,\n      n,\n      r = Be.useRef(null),\n      o = Ze(Be.useState(!1), 2),\n      i = o[0],\n      c = o[1],\n      a = Ze(Be.useState((null == e ? void 0 : e.initialQuery) || void 0), 2),\n      u = a[0],\n      l = a[1],\n      s = Be.useCallback(\n        function () {\n          c(!0);\n        },\n        [c],\n      ),\n      f = Be.useCallback(\n        function () {\n          c(!1);\n        },\n        [c],\n      );\n    return (\n      (function (e) {\n        var t = e.isOpen,\n          n = e.onOpen,\n          r = e.onClose,\n          o = e.onInput,\n          i = e.searchButtonRef;\n        Be.useEffect(\n          function () {\n            function e(e) {\n              var c;\n              ((27 === e.keyCode && t) ||\n                (\"k\" ===\n                  (null === (c = e.key) || void 0 === c\n                    ? void 0\n                    : c.toLowerCase()) &&\n                  (e.metaKey || e.ctrlKey)) ||\n                (!(function (e) {\n                  var t = e.target,\n                    n = t.tagName;\n                  return (\n                    t.isContentEditable ||\n                    \"INPUT\" === n ||\n                    \"SELECT\" === n ||\n                    \"TEXTAREA\" === n\n                  );\n                })(e) &&\n                  \"/\" === e.key &&\n                  !t)) &&\n                (e.preventDefault(),\n                t\n                  ? r()\n                  : document.body.classList.contains(\"DocSearch--active\") ||\n                    document.body.classList.contains(\"DocSearch--active\") ||\n                    n()),\n                i &&\n                  i.current === document.activeElement &&\n                  o &&\n                  /[a-zA-Z0-9]/.test(String.fromCharCode(e.keyCode)) &&\n                  o(e);\n            }\n            return (\n              window.addEventListener(\"keydown\", e),\n              function () {\n                window.removeEventListener(\"keydown\", e);\n              }\n            );\n          },\n          [t, n, r, o, i],\n        );\n      })({\n        isOpen: i,\n        onOpen: s,\n        onClose: f,\n        onInput: Be.useCallback(\n          function (e) {\n            c(!0), l(e.key);\n          },\n          [c, l],\n        ),\n        searchButtonRef: r,\n      }),\n      Be.createElement(\n        Be.Fragment,\n        null,\n        Be.createElement(tt, {\n          ref: r,\n          translations:\n            null == e || null === (t = e.translations) || void 0 === t\n              ? void 0\n              : t.button,\n          onClick: s,\n        }),\n        i &&\n          Ie(\n            Be.createElement(\n              Eo,\n              Je({}, e, {\n                initialScrollY: window.scrollY,\n                initialQuery: u,\n                translations:\n                  null == e || null === (n = e.translations) || void 0 === n\n                    ? void 0\n                    : n.modal,\n                onClose: f,\n              }),\n            ),\n            document.body,\n          ),\n      )\n    );\n  }\n  return function (e) {\n    Ae(\n      Be.createElement(\n        jo,\n        o({}, e, {\n          transformSearchClient: function (t) {\n            return (\n              t.addAlgoliaAgent(\"docsearch.js\", \"3.6.1\"),\n              e.transformSearchClient ? e.transformSearchClient(t) : t\n            );\n          },\n        }),\n      ),\n      (function (e) {\n        var t =\n          arguments.length > 1 && void 0 !== arguments[1]\n            ? arguments[1]\n            : window;\n        return \"string\" == typeof e ? t.document.querySelector(e) : e;\n      })(e.container, e.environment),\n    );\n  };\n});\n//# sourceMappingURL=index.js.map\n\ndocsearch({\n  container: \"#docsearch\",\n  appId: \"74VN1YECLR\",\n  indexName: \"gpt-index\",\n  apiKey: \"c4b0e099fa9004f69855e474b3e7d3bb\",\n});\nconsole.log(docsearch);\nconsole.log(\"Docsearch loaded\");\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/_static/js/leadfeeder.js",
    "content": "(function (ss, ex) {\n  window.ldfdr =\n    window.ldfdr ||\n    function () {\n      (ldfdr._q = ldfdr._q || []).push([].slice.call(arguments));\n    };\n  (function (d, s) {\n    fs = d.getElementsByTagName(s)[0];\n    function ce(src) {\n      var cs = d.createElement(s);\n      cs.src = src;\n      cs.async = 1;\n      fs.parentNode.insertBefore(cs, fs);\n    }\n    ce(\n      \"https://sc.lfeeder.com/lftracker_v1_\" +\n        ss +\n        (ex ? \"_\" + ex : \"\") +\n        \".js\",\n    );\n  })(document, \"script\");\n})(\"Xbp1oaEnqwn8EdVj\");\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/context.md",
    "content": "::: workflows.context.Context\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      members:\n        - collect_events\n        - from_dict\n        - get_result\n        - is_running\n        - retry_info\n        - send_event\n        - store\n        - to_dict\n        - wait_for_event\n        - write_event_to_stream\n\n\n::: workflows.context.state_store\n    options:\n      members:\n        - DictState\n        - InMemoryStateStore\n\n::: workflows.context.serializers\n    options:\n      members:\n        - BaseSerializer\n        - JsonSerializer\n        - PickleSerializer\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/decorators.md",
    "content": "::: workflows.decorators\n    options:\n      members:\n        - step\n        - catch_error\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/errors.md",
    "content": "::: workflows.errors\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/events.md",
    "content": "::: workflows.events\n    options:\n      members:\n        - Event\n        - InputRequiredEvent\n        - HumanResponseEvent\n        - StartEvent\n        - StopEvent\n        - WorkflowTimedOutEvent\n        - WorkflowCancelledEvent\n        - WorkflowFailedEvent\n        - StepFailedEvent\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/handler.md",
    "content": "::: workflows.handler\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/index.md",
    "content": "# Workflows API reference\n\n\nFor conceptual guides, patterns, and end-to-end examples, see the main [Agent Workflows documentation](/python/llamaagents/workflows/). This API reference focuses on signatures, parameters, and behavior of individual classes and functions.\n\n## Installation\n\nInstall the core Workflows SDK from PyPI:\n\n```bash\npip install llama-index-workflows\n```\n\n## What this reference covers\n\n- **Workflow**: The event-driven orchestrator (`Workflow`) that defines and runs your application flows using typed steps. See [`Workflow`](./workflow/).\n- **Context**: Execution context and state management across steps and runs.\n- **Events**: Typed events that steps receive and emit (including `StartEvent`, `StopEvent`, and custom events).\n- **Decorators**: The `@step` decorator and related utilities for defining workflow steps.\n- **Handler**: `WorkflowHandler`, used to await results and stream intermediate events.\n- **Retry policy**: `RetryPolicy`, retry conditions, wait strategies, and stop conditions for per-step retry behavior.\n- **Errors**: Exception types raised by the runtime (validation, configuration, and runtime errors).\n- **Resource**: Resource and dependency injection primitives used by steps.\n\nUse this reference together with the live examples on the main docs site, such as the [`Workflow` page on developers.llamaindex.ai](https://developers.llamaindex.ai/python/workflows-api-reference/workflow/), when you need both conceptual context and exact API details.\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/resource.md",
    "content": "::: workflows.resource.Resource\n::: workflows.resource.ResourceConfig\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/retry_policy.md",
    "content": "# Retry Policy\n\nThe retry API is built from three families of callables modeled after\n[tenacity](https://tenacity.readthedocs.io/en/latest/):\n\n- **[Retry conditions](#retry-conditions)** decide whether an exception should be retried.\n- **[Wait strategies](#wait-strategies)** decide how long to sleep before the next attempt.\n- **[Stop conditions](#stop-conditions)** decide when retries should stop.\n\nYou can compose them into a policy with `retry_policy(retry=..., wait=..., stop=...)`.\nRetry conditions support `|` and `&`, wait strategies support `+`, and stop\nconditions support `|` and `&`.\n\n## Quick Example\n\n```python\nfrom workflows.retry_policy import (\n    retry_policy,\n    retry_if_exception_message,\n    retry_if_exception_type,\n    stop_after_attempt,\n    stop_before_delay,\n    wait_fixed,\n    wait_random,\n)\n\npolicy = retry_policy(\n    retry=retry_if_exception_type((TimeoutError, ConnectionError))\n    | retry_if_exception_message(match=\"rate limit|temporarily unavailable\"),\n    wait=wait_fixed(1) + wait_random(0, 1),\n    stop=stop_after_attempt(5) | stop_before_delay(30),\n)\n```\n\n## Policy Constructor\n\n::: workflows.retry_policy\n    options:\n      members:\n        - retry_policy\n        - RetryPolicy\n\n## Retry Introspection\n\nTypes exposed to step bodies via `Context.retry_info()` and to `@catch_error`\nhandlers via `StepFailedEvent.exception`.\n\n::: workflows.retry_policy\n    options:\n      members:\n        - RetryInfo\n\n## Retry Conditions\n\nModeled after [tenacity retry functions](https://tenacity.readthedocs.io/en/latest/api.html#retry-functions).\n\n::: workflows.retry_policy\n    options:\n      members:\n        - retry_if_exception\n        - retry_if_exception_type\n        - retry_if_not_exception_type\n        - retry_unless_exception_type\n        - retry_if_exception_message\n        - retry_if_not_exception_message\n        - retry_if_exception_cause_type\n        - retry_any\n        - retry_all\n        - retry_always\n        - retry_never\n\n## Wait Strategies\n\nModeled after [tenacity wait functions](https://tenacity.readthedocs.io/en/latest/api.html#wait-functions).\n\n::: workflows.retry_policy\n    options:\n      members:\n        - wait_fixed\n        - wait_none\n        - wait_exponential\n        - wait_incrementing\n        - wait_random\n        - wait_exponential_jitter\n        - wait_random_exponential\n        - wait_full_jitter\n        - wait_chain\n        - wait_combine\n\n## Stop Conditions\n\nModeled after [tenacity stop functions](https://tenacity.readthedocs.io/en/latest/api.html#stop-functions).\n\n::: workflows.retry_policy\n    options:\n      members:\n        - stop_after_attempt\n        - stop_after_delay\n        - stop_before_delay\n        - stop_any\n        - stop_all\n        - stop_never\n\n## Deprecated Constructors\n\nThe following helpers predate the composable API and are kept for\nbackwards compatibility. Prefer `retry_policy(...)` with explicit retry,\nwait, and stop arguments.\n\n::: workflows.retry_policy\n    options:\n      members:\n        - ConstantDelayRetryPolicy\n        - ExponentialBackoffRetryPolicy\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/workflow.md",
    "content": "::: workflows.workflow\n    options:\n      members:\n        - Workflow\n      filters: [\"!^_\", \"^__init__$\"]\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/workflow_server/client.md",
    "content": "::: llama_agents.client.WorkflowClient\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: false\n      filters: [\"!^get_result$\"]\n\n::: llama_agents.client.EventStream\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: false\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/workflow_server/models.md",
    "content": "::: llama_agents.client.EventEnvelopeWithMetadata\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: false\n\n::: llama_agents.client.HandlerData\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: false\n\n::: llama_agents.client.HandlersListResponse\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: false\n\n::: llama_agents.client.SendEventResponse\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: false\n\n::: llama_agents.client.CancelHandlerResponse\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: false\n"
  },
  {
    "path": "docs/api_docs/docs/api_reference/workflow_server/server.md",
    "content": "::: llama_agents.server.WorkflowServer\n    options:\n      show_root_heading: true\n      show_root_full_path: false\n      merge_init_into_class: true\n"
  },
  {
    "path": "docs/api_docs/docs/css/algolia.css",
    "content": "/**\n * Skipped minification because the original files appears to be already minified.\n * Original file: /npm/@docsearch/css@3.6.1/dist/style.css\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n/*! @docsearch/css 3.6.1 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */\n:root {\n  --docsearch-primary-color: #5468ff;\n  --docsearch-text-color: #1c1e21;\n  --docsearch-spacing: 12px;\n  --docsearch-icon-stroke-width: 1.4;\n  --docsearch-highlight-color: var(--docsearch-primary-color);\n  --docsearch-muted-color: #969faf;\n  --docsearch-container-background: rgba(101, 108, 133, 0.8);\n  --docsearch-logo-color: #5468ff;\n  --docsearch-modal-width: 560px;\n  --docsearch-modal-height: 600px;\n  --docsearch-modal-background: #f5f6f7;\n  --docsearch-modal-shadow: inset 1px 1px 0 0 hsla(0, 0%, 100%, 0.5),\n    0 3px 8px 0 #555a64;\n  --docsearch-searchbox-height: 56px;\n  --docsearch-searchbox-background: #ebedf0;\n  --docsearch-searchbox-focus-background: #fff;\n  --docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);\n  --docsearch-hit-height: 56px;\n  --docsearch-hit-color: #444950;\n  --docsearch-hit-active-color: #fff;\n  --docsearch-hit-background: #fff;\n  --docsearch-hit-shadow: 0 1px 3px 0 #d4d9e1;\n  --docsearch-key-gradient: linear-gradient(-225deg, #d5dbe4, #f8f8f8);\n  --docsearch-key-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff,\n    0 1px 2px 1px rgba(30, 35, 90, 0.4);\n  --docsearch-key-pressed-shadow: inset 0 -2px 0 0 #cdcde6,\n    inset 0 0 1px 1px #fff, 0 1px 1px 0 rgba(30, 35, 90, 0.4);\n  --docsearch-footer-height: 44px;\n  --docsearch-footer-background: #fff;\n  --docsearch-footer-shadow: 0 -1px 0 0 #e0e3e8,\n    0 -3px 6px 0 rgba(69, 98, 155, 0.12);\n}\n\nhtml[data-theme=\"dark\"] {\n  --docsearch-text-color: #f5f6f7;\n  --docsearch-container-background: rgba(9, 10, 17, 0.8);\n  --docsearch-modal-background: #15172a;\n  --docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;\n  --docsearch-searchbox-background: #090a11;\n  --docsearch-searchbox-focus-background: #000;\n  --docsearch-hit-color: #bec3c9;\n  --docsearch-hit-shadow: none;\n  --docsearch-hit-background: #090a11;\n  --docsearch-key-gradient: linear-gradient(-26.5deg, #565872, #31355b);\n  --docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d,\n    0 2px 2px 0 rgba(3, 4, 9, 0.3);\n  --docsearch-key-pressed-shadow: inset 0 -2px 0 0 #282d55,\n    inset 0 0 1px 1px #51577d, 0 1px 1px 0 rgba(3, 4, 9, 0.30196078431372547);\n  --docsearch-footer-background: #1e2136;\n  --docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),\n    0 -4px 8px 0 rgba(0, 0, 0, 0.2);\n  --docsearch-logo-color: #fff;\n  --docsearch-muted-color: #7f8497;\n}\n\n.DocSearch-Button {\n  align-items: center;\n  background: var(--docsearch-searchbox-background);\n  border: 0;\n  border-radius: 40px;\n  color: var(--docsearch-muted-color);\n  cursor: pointer;\n  display: flex;\n  font-weight: 500;\n  height: 36px;\n  justify-content: space-between;\n  margin: 0 0 0 16px;\n  padding: 0 8px;\n  user-select: none;\n}\n\n.DocSearch-Button:active,\n.DocSearch-Button:focus,\n.DocSearch-Button:hover {\n  background: var(--docsearch-searchbox-focus-background);\n  box-shadow: var(--docsearch-searchbox-shadow);\n  color: var(--docsearch-text-color);\n  outline: none;\n}\n\n.DocSearch-Button-Container {\n  align-items: center;\n  display: flex;\n}\n\n.DocSearch-Search-Icon {\n  stroke-width: 1.6;\n}\n\n.DocSearch-Button .DocSearch-Search-Icon {\n  color: var(--docsearch-text-color);\n}\n\n.DocSearch-Button-Placeholder {\n  font-size: 1rem;\n  padding: 0 12px 0 6px;\n}\n\n.DocSearch-Button-Keys {\n  display: flex;\n  min-width: calc(40px + 0.8em);\n}\n\n.DocSearch-Button-Key {\n  align-items: center;\n  background: var(--docsearch-key-gradient);\n  border-radius: 3px;\n  box-shadow: var(--docsearch-key-shadow);\n  color: var(--docsearch-muted-color);\n  display: flex;\n  height: 18px;\n  justify-content: center;\n  margin-right: 0.4em;\n  position: relative;\n  padding: 0 0 2px;\n  border: 0;\n  top: -1px;\n  width: 20px;\n}\n\n.DocSearch-Button-Key--pressed {\n  transform: translate3d(0, 1px, 0);\n  box-shadow: var(--docsearch-key-pressed-shadow);\n}\n\n@media (max-width: 768px) {\n  .DocSearch-Button-Keys,\n  .DocSearch-Button-Placeholder {\n    display: none;\n  }\n}\n\n.DocSearch--active {\n  overflow: hidden !important;\n}\n\n.DocSearch-Container,\n.DocSearch-Container * {\n  box-sizing: border-box;\n}\n\n.DocSearch-Container {\n  background-color: var(--docsearch-container-background);\n  height: 100vh;\n  left: 0;\n  position: fixed;\n  top: 0;\n  width: 100vw;\n  z-index: 200;\n}\n\n.DocSearch-Container a {\n  text-decoration: none;\n}\n\n.DocSearch-Link {\n  appearance: none;\n  background: none;\n  border: 0;\n  color: var(--docsearch-highlight-color);\n  cursor: pointer;\n  font: inherit;\n  margin: 0;\n  padding: 0;\n}\n\n.DocSearch-Modal {\n  background: var(--docsearch-modal-background);\n  border-radius: 6px;\n  box-shadow: var(--docsearch-modal-shadow);\n  flex-direction: column;\n  margin: 60px auto auto;\n  max-width: var(--docsearch-modal-width);\n  position: relative;\n}\n\n.DocSearch-SearchBar {\n  display: flex;\n  padding: var(--docsearch-spacing) var(--docsearch-spacing) 0;\n}\n\n.DocSearch-Form {\n  align-items: center;\n  background: var(--docsearch-searchbox-focus-background);\n  border-radius: 4px;\n  box-shadow: var(--docsearch-searchbox-shadow);\n  display: flex;\n  height: var(--docsearch-searchbox-height);\n  margin: 0;\n  padding: 0 var(--docsearch-spacing);\n  position: relative;\n  width: 100%;\n}\n\n.DocSearch-Input {\n  appearance: none;\n  background: transparent;\n  border: 0;\n  color: var(--docsearch-text-color);\n  flex: 1;\n  font: inherit;\n  font-size: 1.2em;\n  height: 100%;\n  outline: none;\n  padding: 0 0 0 8px;\n  width: 80%;\n}\n\n.DocSearch-Input::placeholder {\n  color: var(--docsearch-muted-color);\n  opacity: 1;\n}\n\n.DocSearch-Input::-webkit-search-cancel-button,\n.DocSearch-Input::-webkit-search-decoration,\n.DocSearch-Input::-webkit-search-results-button,\n.DocSearch-Input::-webkit-search-results-decoration {\n  display: none;\n}\n\n.DocSearch-LoadingIndicator,\n.DocSearch-MagnifierLabel,\n.DocSearch-Reset {\n  margin: 0;\n  padding: 0;\n}\n\n.DocSearch-MagnifierLabel,\n.DocSearch-Reset {\n  align-items: center;\n  color: var(--docsearch-highlight-color);\n  display: flex;\n  justify-content: center;\n}\n\n.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,\n.DocSearch-LoadingIndicator {\n  display: none;\n}\n\n.DocSearch-Container--Stalled .DocSearch-LoadingIndicator {\n  align-items: center;\n  color: var(--docsearch-highlight-color);\n  display: flex;\n  justify-content: center;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Reset {\n    animation: none;\n    appearance: none;\n    background: none;\n    border: 0;\n    border-radius: 50%;\n    color: var(--docsearch-icon-color);\n    cursor: pointer;\n    right: 0;\n    stroke-width: var(--docsearch-icon-stroke-width);\n  }\n}\n\n.DocSearch-Reset {\n  animation: fade-in 0.1s ease-in forwards;\n  appearance: none;\n  background: none;\n  border: 0;\n  border-radius: 50%;\n  color: var(--docsearch-icon-color);\n  cursor: pointer;\n  padding: 2px;\n  right: 0;\n  stroke-width: var(--docsearch-icon-stroke-width);\n}\n\n.DocSearch-Reset[hidden] {\n  display: none;\n}\n\n.DocSearch-Reset:hover {\n  color: var(--docsearch-highlight-color);\n}\n\n.DocSearch-LoadingIndicator svg,\n.DocSearch-MagnifierLabel svg {\n  height: 24px;\n  width: 24px;\n}\n\n.DocSearch-Cancel {\n  display: none;\n}\n\n.DocSearch-Dropdown {\n  max-height: calc(\n    var(--docsearch-modal-height) - var(--docsearch-searchbox-height) -\n      var(--docsearch-spacing) - var(--docsearch-footer-height)\n  );\n  min-height: var(--docsearch-spacing);\n  overflow-y: auto;\n  overflow-y: overlay;\n  padding: 0 var(--docsearch-spacing);\n  scrollbar-color: var(--docsearch-muted-color)\n    var(--docsearch-modal-background);\n  scrollbar-width: thin;\n}\n\n.DocSearch-Dropdown::-webkit-scrollbar {\n  width: 12px;\n}\n\n.DocSearch-Dropdown::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.DocSearch-Dropdown::-webkit-scrollbar-thumb {\n  background-color: var(--docsearch-muted-color);\n  border: 3px solid var(--docsearch-modal-background);\n  border-radius: 20px;\n}\n\n.DocSearch-Dropdown ul {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n.DocSearch-Label {\n  font-size: 0.75em;\n  line-height: 1.6em;\n}\n\n.DocSearch-Help,\n.DocSearch-Label {\n  color: var(--docsearch-muted-color);\n}\n\n.DocSearch-Help {\n  font-size: 0.9em;\n  margin: 0;\n  user-select: none;\n}\n\n.DocSearch-Title {\n  font-size: 1.2em;\n}\n\n.DocSearch-Logo a {\n  display: flex;\n}\n\n.DocSearch-Logo svg {\n  color: var(--docsearch-logo-color);\n  margin-left: 8px;\n}\n\n.DocSearch-Hits:last-of-type {\n  margin-bottom: 24px;\n}\n\n.DocSearch-Hits mark {\n  background: none;\n  color: var(--docsearch-highlight-color);\n}\n\n.DocSearch-HitsFooter {\n  color: var(--docsearch-muted-color);\n  display: flex;\n  font-size: 0.85em;\n  justify-content: center;\n  margin-bottom: var(--docsearch-spacing);\n  padding: var(--docsearch-spacing);\n}\n\n.DocSearch-HitsFooter a {\n  border-bottom: 1px solid;\n  color: inherit;\n}\n\n.DocSearch-Hit {\n  border-radius: 4px;\n  display: flex;\n  padding-bottom: 4px;\n  position: relative;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit--deleting {\n    transition: none;\n  }\n}\n\n.DocSearch-Hit--deleting {\n  opacity: 0;\n  transition: all 0.25s linear;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit--favoriting {\n    transition: none;\n  }\n}\n\n.DocSearch-Hit--favoriting {\n  transform: scale(0);\n  transform-origin: top center;\n  transition: all 0.25s linear;\n  transition-delay: 0.25s;\n}\n\n.DocSearch-Hit a {\n  background: var(--docsearch-hit-background);\n  border-radius: 4px;\n  box-shadow: var(--docsearch-hit-shadow);\n  display: block;\n  padding-left: var(--docsearch-spacing);\n  width: 100%;\n}\n\n.DocSearch-Hit-source {\n  background: var(--docsearch-modal-background);\n  color: var(--docsearch-highlight-color);\n  font-size: 0.85em;\n  font-weight: 600;\n  line-height: 32px;\n  margin: 0 -4px;\n  padding: 8px 4px 0;\n  position: sticky;\n  top: 0;\n  z-index: 10;\n}\n\n.DocSearch-Hit-Tree {\n  color: var(--docsearch-muted-color);\n  height: var(--docsearch-hit-height);\n  opacity: 0.5;\n  stroke-width: var(--docsearch-icon-stroke-width);\n  width: 24px;\n}\n\n.DocSearch-Hit[aria-selected=\"true\"] a {\n  background-color: var(--docsearch-highlight-color);\n}\n\n.DocSearch-Hit[aria-selected=\"true\"] mark {\n  text-decoration: underline;\n}\n\n.DocSearch-Hit-Container {\n  align-items: center;\n  color: var(--docsearch-hit-color);\n  display: flex;\n  flex-direction: row;\n  height: var(--docsearch-hit-height);\n  padding: 0 var(--docsearch-spacing) 0 0;\n}\n\n.DocSearch-Hit-icon {\n  height: 20px;\n  width: 20px;\n}\n\n.DocSearch-Hit-action,\n.DocSearch-Hit-icon {\n  color: var(--docsearch-muted-color);\n  stroke-width: var(--docsearch-icon-stroke-width);\n}\n\n.DocSearch-Hit-action {\n  align-items: center;\n  display: flex;\n  height: 22px;\n  width: 22px;\n}\n\n.DocSearch-Hit-action svg {\n  display: block;\n  height: 18px;\n  width: 18px;\n}\n\n.DocSearch-Hit-action + .DocSearch-Hit-action {\n  margin-left: 6px;\n}\n\n.DocSearch-Hit-action-button {\n  appearance: none;\n  background: none;\n  border: 0;\n  border-radius: 50%;\n  color: inherit;\n  cursor: pointer;\n  padding: 2px;\n}\n\nsvg.DocSearch-Hit-Select-Icon {\n  display: none;\n}\n\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-Select-Icon {\n  display: block;\n}\n\n.DocSearch-Hit-action-button:focus,\n.DocSearch-Hit-action-button:hover {\n  background: rgba(0, 0, 0, 0.2);\n  transition: background-color 0.1s ease-in;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit-action-button:focus,\n  .DocSearch-Hit-action-button:hover {\n    transition: none;\n  }\n}\n\n.DocSearch-Hit-action-button:focus path,\n.DocSearch-Hit-action-button:hover path {\n  fill: #fff;\n}\n\n.DocSearch-Hit-content-wrapper {\n  display: flex;\n  flex: 1 1 auto;\n  flex-direction: column;\n  font-weight: 500;\n  justify-content: center;\n  line-height: 1.7em;\n  margin: 0 8px;\n  overflow-x: hidden;\n  overflow-y: hidden;\n  position: relative;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  width: 80%;\n}\n\n.DocSearch-Hit-title {\n  font-size: 1.5em;\n}\n\n.DocSearch-Hit-path {\n  color: var(--docsearch-muted-color);\n  font-size: 0.75em;\n}\n\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-action,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-icon,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-path,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-text,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-title,\n.DocSearch-Hit[aria-selected=\"true\"] .DocSearch-Hit-Tree,\n.DocSearch-Hit[aria-selected=\"true\"] mark {\n  color: var(--docsearch-hit-active-color) !important;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n  .DocSearch-Hit-action-button:focus,\n  .DocSearch-Hit-action-button:hover {\n    background: rgba(0, 0, 0, 0.2);\n    transition: none;\n  }\n}\n\n.DocSearch-ErrorScreen,\n.DocSearch-NoResults,\n.DocSearch-StartScreen {\n  font-size: 0.9em;\n  margin: 0 auto;\n  padding: 36px 0;\n  text-align: center;\n  width: 80%;\n}\n\n.DocSearch-Screen-Icon {\n  color: var(--docsearch-muted-color);\n  padding-bottom: 12px;\n}\n\n.DocSearch-NoResults-Prefill-List {\n  display: inline-block;\n  padding-bottom: 24px;\n  text-align: left;\n}\n\n.DocSearch-NoResults-Prefill-List ul {\n  display: inline-block;\n  padding: 8px 0 0;\n}\n\n.DocSearch-NoResults-Prefill-List li {\n  list-style-position: inside;\n  list-style-type: \"» \";\n}\n\n.DocSearch-Prefill {\n  appearance: none;\n  background: none;\n  border: 0;\n  border-radius: 1em;\n  color: var(--docsearch-highlight-color);\n  cursor: pointer;\n  display: inline-block;\n  font-size: 1em;\n  font-weight: 700;\n  padding: 0;\n}\n\n.DocSearch-Prefill:focus,\n.DocSearch-Prefill:hover {\n  outline: none;\n  text-decoration: underline;\n}\n\n.DocSearch-Footer {\n  align-items: center;\n  background: var(--docsearch-footer-background);\n  border-radius: 0 0 8px 8px;\n  box-shadow: var(--docsearch-footer-shadow);\n  display: flex;\n  flex-direction: row-reverse;\n  flex-shrink: 0;\n  height: var(--docsearch-footer-height);\n  justify-content: space-between;\n  padding: 0 var(--docsearch-spacing);\n  position: relative;\n  user-select: none;\n  width: 100%;\n  z-index: 300;\n}\n\n.DocSearch-Commands {\n  color: var(--docsearch-muted-color);\n  display: flex;\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n.DocSearch-Commands li {\n  align-items: center;\n  display: flex;\n}\n\n.DocSearch-Commands li:not(:last-of-type) {\n  margin-right: 0.8em;\n}\n\n.DocSearch-Commands-Key {\n  align-items: center;\n  background: var(--docsearch-key-gradient);\n  border-radius: 2px;\n  box-shadow: var(--docsearch-key-shadow);\n  display: flex;\n  height: 18px;\n  justify-content: center;\n  margin-right: 0.4em;\n  padding: 0 0 1px;\n  color: var(--docsearch-muted-color);\n  border: 0;\n  width: 20px;\n}\n\n.DocSearch-VisuallyHiddenForAccessibility {\n  clip: rect(0 0 0 0);\n  clip-path: inset(50%);\n  height: 1px;\n  overflow: hidden;\n  position: absolute;\n  white-space: nowrap;\n  width: 1px;\n}\n\n@media (max-width: 768px) {\n  :root {\n    --docsearch-spacing: 10px;\n    --docsearch-footer-height: 40px;\n  }\n\n  .DocSearch-Dropdown {\n    height: 100%;\n  }\n\n  .DocSearch-Container {\n    height: 100vh;\n    height: -webkit-fill-available;\n    height: calc(var(--docsearch-vh, 1vh) * 100);\n    position: absolute;\n  }\n\n  .DocSearch-Footer {\n    border-radius: 0;\n    bottom: 0;\n    position: absolute;\n  }\n\n  .DocSearch-Hit-content-wrapper {\n    display: flex;\n    position: relative;\n    width: 80%;\n  }\n\n  .DocSearch-Modal {\n    border-radius: 0;\n    box-shadow: none;\n    height: 100vh;\n    height: -webkit-fill-available;\n    height: calc(var(--docsearch-vh, 1vh) * 100);\n    margin: 0;\n    max-width: 100%;\n    width: 100%;\n  }\n\n  .DocSearch-Dropdown {\n    max-height: calc(\n      var(--docsearch-vh, 1vh) * 100 - var(--docsearch-searchbox-height) -\n        var(--docsearch-spacing) - var(--docsearch-footer-height)\n    );\n  }\n\n  .DocSearch-Cancel {\n    appearance: none;\n    background: none;\n    border: 0;\n    color: var(--docsearch-highlight-color);\n    cursor: pointer;\n    display: inline-block;\n    flex: none;\n    font: inherit;\n    font-size: 1em;\n    font-weight: 500;\n    margin-left: var(--docsearch-spacing);\n    outline: none;\n    overflow: hidden;\n    padding: 0;\n    user-select: none;\n    white-space: nowrap;\n  }\n\n  .DocSearch-Commands,\n  .DocSearch-Hit-Tree {\n    display: none;\n  }\n}\n\n@keyframes fade-in {\n  0% {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n/* hide superfluous second search icon */\nlabel.md-icon[for=\"__search\"] {\n  display: none;\n}\n"
  },
  {
    "path": "docs/api_docs/docs/css/custom.css",
    "content": "#my-component-root *,\n#headlessui-portal-root * {\n  z-index: 1000000000000;\n  font-size: 100%;\n}\n\ntextarea {\n  border: 0;\n  padding: 0;\n}\n\narticle p {\n  margin-bottom: 10px !important;\n}\n"
  },
  {
    "path": "docs/api_docs/docs/css/style.css",
    "content": ".md-container .jp-Cell-outputWrapper .jp-OutputPrompt.jp-OutputArea-prompt,\n.md-container .jp-Cell-inputWrapper .jp-InputPrompt.jp-InputArea-prompt {\n  display: none !important;\n}\n\n/* CSS styles for side-by-side layout */\n.container {\n  display: flex-col;\n  justify-content: space-between;\n  margin-bottom: 20px; /* Adjust spacing between sections */\n  position: sticky;\n  top: 2.4rem;\n  z-index: 1000; /* Ensure it's above other content */\n  background-color: white; /* Match your page background */\n  padding: 0.2rem;\n}\n\n.example-heading {\n  margin: 0.2rem !important;\n}\n\n.usage-examples {\n  width: 100%; /* Adjust the width as needed */\n  border: 1px solid var(--md-default-fg-color--light);\n  border-radius: 2px;\n  padding: 0.2rem;\n}\n\n/* Additional styling for the toggle */\n.toggle-example {\n  cursor: pointer;\n  color: white;\n  text-decoration: underline;\n  background-color: var(--md-primary-fg-color);\n  padding: 0.2rem;\n  border-radius: 2px;\n}\n\n.hidden {\n  display: none;\n}\n\n/* mendable search styling */\n#my-component-root > div {\n  bottom: 100px;\n}\n"
  },
  {
    "path": "docs/api_docs/docs/javascript/algolia.js",
    "content": "/**\n * Skipped minification because the original files appears to be already minified.\n * Original file: /npm/@docsearch/js@3.6.1/dist/umd/index.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n/*! @docsearch/js 3.6.1 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */\n!(function (e, t) {\n  \"object\" == typeof exports && \"undefined\" != typeof module\n    ? (module.exports = t())\n    : \"function\" == typeof define && define.amd\n    ? define(t)\n    : ((e = e || self).docsearch = t());\n})(this, function () {\n  \"use strict\";\n  function e(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function t(t) {\n    for (var n = 1; n < arguments.length; n++) {\n      var o = null != arguments[n] ? arguments[n] : {};\n      n % 2\n        ? e(Object(o), !0).forEach(function (e) {\n            r(t, e, o[e]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(t, Object.getOwnPropertyDescriptors(o))\n        : e(Object(o)).forEach(function (e) {\n            Object.defineProperty(t, e, Object.getOwnPropertyDescriptor(o, e));\n          });\n    }\n    return t;\n  }\n  function n(e) {\n    return (\n      (n =\n        \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator\n          ? function (e) {\n              return typeof e;\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : typeof e;\n            }),\n      n(e)\n    );\n  }\n  function r(e, t, n) {\n    return (\n      t in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function o() {\n    return (\n      (o =\n        Object.assign ||\n        function (e) {\n          for (var t = 1; t < arguments.length; t++) {\n            var n = arguments[t];\n            for (var r in n)\n              Object.prototype.hasOwnProperty.call(n, r) && (e[r] = n[r]);\n          }\n          return e;\n        }),\n      o.apply(this, arguments)\n    );\n  }\n  function i(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function c(e, t) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return e;\n      })(e) ||\n      (function (e, t) {\n        var n =\n          null == e\n            ? null\n            : (\"undefined\" != typeof Symbol && e[Symbol.iterator]) ||\n              e[\"@@iterator\"];\n        if (null == n) return;\n        var r,\n          o,\n          i = [],\n          c = !0,\n          a = !1;\n        try {\n          for (\n            n = n.call(e);\n            !(c = (r = n.next()).done) &&\n            (i.push(r.value), !t || i.length !== t);\n            c = !0\n          );\n        } catch (e) {\n          (a = !0), (o = e);\n        } finally {\n          try {\n            c || null == n.return || n.return();\n          } finally {\n            if (a) throw o;\n          }\n        }\n        return i;\n      })(e, t) ||\n      u(e, t) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function a(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return l(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      u(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function u(e, t) {\n    if (e) {\n      if (\"string\" == typeof e) return l(e, t);\n      var n = Object.prototype.toString.call(e).slice(8, -1);\n      return (\n        \"Object\" === n && e.constructor && (n = e.constructor.name),\n        \"Map\" === n || \"Set\" === n\n          ? Array.from(e)\n          : \"Arguments\" === n ||\n            /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n          ? l(e, t)\n          : void 0\n      );\n    }\n  }\n  function l(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  var s,\n    f,\n    p,\n    m,\n    d,\n    v = {},\n    h = [],\n    y = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;\n  function _(e, t) {\n    for (var n in t) e[n] = t[n];\n    return e;\n  }\n  function b(e) {\n    var t = e.parentNode;\n    t && t.removeChild(e);\n  }\n  function g(e, t, n) {\n    var r,\n      o,\n      i,\n      c = arguments,\n      a = {};\n    for (i in t)\n      \"key\" == i ? (r = t[i]) : \"ref\" == i ? (o = t[i]) : (a[i] = t[i]);\n    if (arguments.length > 3)\n      for (n = [n], i = 3; i < arguments.length; i++) n.push(c[i]);\n    if (\n      (null != n && (a.children = n),\n      \"function\" == typeof e && null != e.defaultProps)\n    )\n      for (i in e.defaultProps) void 0 === a[i] && (a[i] = e.defaultProps[i]);\n    return S(e, a, r, o, null);\n  }\n  function S(e, t, n, r, o) {\n    var i = {\n      type: e,\n      props: t,\n      key: n,\n      ref: r,\n      __k: null,\n      __: null,\n      __b: 0,\n      __e: null,\n      __d: void 0,\n      __c: null,\n      __h: null,\n      constructor: void 0,\n      __v: null == o ? ++s.__v : o,\n    };\n    return null != s.vnode && s.vnode(i), i;\n  }\n  function O(e) {\n    return e.children;\n  }\n  function w(e, t) {\n    (this.props = e), (this.context = t);\n  }\n  function E(e, t) {\n    if (null == t) return e.__ ? E(e.__, e.__.__k.indexOf(e) + 1) : null;\n    for (var n; t < e.__k.length; t++)\n      if (null != (n = e.__k[t]) && null != n.__e) return n.__e;\n    return \"function\" == typeof e.type ? E(e) : null;\n  }\n  function j(e) {\n    var t, n;\n    if (null != (e = e.__) && null != e.__c) {\n      for (e.__e = e.__c.base = null, t = 0; t < e.__k.length; t++)\n        if (null != (n = e.__k[t]) && null != n.__e) {\n          e.__e = e.__c.base = n.__e;\n          break;\n        }\n      return j(e);\n    }\n  }\n  function P(e) {\n    ((!e.__d && (e.__d = !0) && f.push(e) && !I.__r++) ||\n      m !== s.debounceRendering) &&\n      ((m = s.debounceRendering) || p)(I);\n  }\n  function I() {\n    for (var e; (I.__r = f.length); )\n      (e = f.sort(function (e, t) {\n        return e.__v.__b - t.__v.__b;\n      })),\n        (f = []),\n        e.some(function (e) {\n          var t, n, r, o, i, c;\n          e.__d &&\n            ((i = (o = (t = e).__v).__e),\n            (c = t.__P) &&\n              ((n = []),\n              ((r = _({}, o)).__v = o.__v + 1),\n              q(\n                c,\n                o,\n                r,\n                t.__n,\n                void 0 !== c.ownerSVGElement,\n                null != o.__h ? [i] : null,\n                n,\n                null == i ? E(o) : i,\n                o.__h,\n              ),\n              L(n, o),\n              o.__e != i && j(o)));\n        });\n  }\n  function D(e, t, n, r, o, i, c, a, u, l) {\n    var s,\n      f,\n      p,\n      m,\n      d,\n      y,\n      _,\n      b = (r && r.__k) || h,\n      g = b.length;\n    for (n.__k = [], s = 0; s < t.length; s++)\n      if (\n        null !=\n        (m = n.__k[s] =\n          null == (m = t[s]) || \"boolean\" == typeof m\n            ? null\n            : \"string\" == typeof m || \"number\" == typeof m\n            ? S(null, m, null, null, m)\n            : Array.isArray(m)\n            ? S(O, { children: m }, null, null, null)\n            : m.__b > 0\n            ? S(m.type, m.props, m.key, null, m.__v)\n            : m)\n      ) {\n        if (\n          ((m.__ = n),\n          (m.__b = n.__b + 1),\n          null === (p = b[s]) || (p && m.key == p.key && m.type === p.type))\n        )\n          b[s] = void 0;\n        else\n          for (f = 0; f < g; f++) {\n            if ((p = b[f]) && m.key == p.key && m.type === p.type) {\n              b[f] = void 0;\n              break;\n            }\n            p = null;\n          }\n        q(e, m, (p = p || v), o, i, c, a, u, l),\n          (d = m.__e),\n          (f = m.ref) &&\n            p.ref != f &&\n            (_ || (_ = []),\n            p.ref && _.push(p.ref, null, m),\n            _.push(f, m.__c || d, m)),\n          null != d\n            ? (null == y && (y = d),\n              \"function\" == typeof m.type && null != m.__k && m.__k === p.__k\n                ? (m.__d = u = k(m, u, e))\n                : (u = A(e, m, p, b, d, u)),\n              l || \"option\" !== n.type\n                ? \"function\" == typeof n.type && (n.__d = u)\n                : (e.value = \"\"))\n            : u && p.__e == u && u.parentNode != e && (u = E(p));\n      }\n    for (n.__e = y, s = g; s--; )\n      null != b[s] &&\n        (\"function\" == typeof n.type &&\n          null != b[s].__e &&\n          b[s].__e == n.__d &&\n          (n.__d = E(r, s + 1)),\n        U(b[s], b[s]));\n    if (_) for (s = 0; s < _.length; s++) H(_[s], _[++s], _[++s]);\n  }\n  function k(e, t, n) {\n    var r, o;\n    for (r = 0; r < e.__k.length; r++)\n      (o = e.__k[r]) &&\n        ((o.__ = e),\n        (t =\n          \"function\" == typeof o.type\n            ? k(o, t, n)\n            : A(n, o, o, e.__k, o.__e, t)));\n    return t;\n  }\n  function C(e, t) {\n    return (\n      (t = t || []),\n      null == e ||\n        \"boolean\" == typeof e ||\n        (Array.isArray(e)\n          ? e.some(function (e) {\n              C(e, t);\n            })\n          : t.push(e)),\n      t\n    );\n  }\n  function A(e, t, n, r, o, i) {\n    var c, a, u;\n    if (void 0 !== t.__d) (c = t.__d), (t.__d = void 0);\n    else if (null == n || o != i || null == o.parentNode)\n      e: if (null == i || i.parentNode !== e) e.appendChild(o), (c = null);\n      else {\n        for (a = i, u = 0; (a = a.nextSibling) && u < r.length; u += 2)\n          if (a == o) break e;\n        e.insertBefore(o, i), (c = i);\n      }\n    return void 0 !== c ? c : o.nextSibling;\n  }\n  function x(e, t, n) {\n    \"-\" === t[0]\n      ? e.setProperty(t, n)\n      : (e[t] =\n          null == n ? \"\" : \"number\" != typeof n || y.test(t) ? n : n + \"px\");\n  }\n  function N(e, t, n, r, o) {\n    var i;\n    e: if (\"style\" === t)\n      if (\"string\" == typeof n) e.style.cssText = n;\n      else {\n        if ((\"string\" == typeof r && (e.style.cssText = r = \"\"), r))\n          for (t in r) (n && t in n) || x(e.style, t, \"\");\n        if (n) for (t in n) (r && n[t] === r[t]) || x(e.style, t, n[t]);\n      }\n    else if (\"o\" === t[0] && \"n\" === t[1])\n      (i = t !== (t = t.replace(/Capture$/, \"\"))),\n        (t = t.toLowerCase() in e ? t.toLowerCase().slice(2) : t.slice(2)),\n        e.l || (e.l = {}),\n        (e.l[t + i] = n),\n        n\n          ? r || e.addEventListener(t, i ? R : T, i)\n          : e.removeEventListener(t, i ? R : T, i);\n    else if (\"dangerouslySetInnerHTML\" !== t) {\n      if (o) t = t.replace(/xlink[H:h]/, \"h\").replace(/sName$/, \"s\");\n      else if (\n        \"href\" !== t &&\n        \"list\" !== t &&\n        \"form\" !== t &&\n        \"download\" !== t &&\n        t in e\n      )\n        try {\n          e[t] = null == n ? \"\" : n;\n          break e;\n        } catch (e) {}\n      \"function\" == typeof n ||\n        (null != n && (!1 !== n || (\"a\" === t[0] && \"r\" === t[1]))\n          ? e.setAttribute(t, n)\n          : e.removeAttribute(t));\n    }\n  }\n  function T(e) {\n    this.l[e.type + !1](s.event ? s.event(e) : e);\n  }\n  function R(e) {\n    this.l[e.type + !0](s.event ? s.event(e) : e);\n  }\n  function q(e, t, n, r, o, i, c, a, u) {\n    var l,\n      f,\n      p,\n      m,\n      d,\n      v,\n      h,\n      y,\n      b,\n      g,\n      S,\n      E = t.type;\n    if (void 0 !== t.constructor) return null;\n    null != n.__h &&\n      ((u = n.__h), (a = t.__e = n.__e), (t.__h = null), (i = [a])),\n      (l = s.__b) && l(t);\n    try {\n      e: if (\"function\" == typeof E) {\n        if (\n          ((y = t.props),\n          (b = (l = E.contextType) && r[l.__c]),\n          (g = l ? (b ? b.props.value : l.__) : r),\n          n.__c\n            ? (h = (f = t.__c = n.__c).__ = f.__E)\n            : (\"prototype\" in E && E.prototype.render\n                ? (t.__c = f = new E(y, g))\n                : ((t.__c = f = new w(y, g)),\n                  (f.constructor = E),\n                  (f.render = F)),\n              b && b.sub(f),\n              (f.props = y),\n              f.state || (f.state = {}),\n              (f.context = g),\n              (f.__n = r),\n              (p = f.__d = !0),\n              (f.__h = [])),\n          null == f.__s && (f.__s = f.state),\n          null != E.getDerivedStateFromProps &&\n            (f.__s == f.state && (f.__s = _({}, f.__s)),\n            _(f.__s, E.getDerivedStateFromProps(y, f.__s))),\n          (m = f.props),\n          (d = f.state),\n          p)\n        )\n          null == E.getDerivedStateFromProps &&\n            null != f.componentWillMount &&\n            f.componentWillMount(),\n            null != f.componentDidMount && f.__h.push(f.componentDidMount);\n        else {\n          if (\n            (null == E.getDerivedStateFromProps &&\n              y !== m &&\n              null != f.componentWillReceiveProps &&\n              f.componentWillReceiveProps(y, g),\n            (!f.__e &&\n              null != f.shouldComponentUpdate &&\n              !1 === f.shouldComponentUpdate(y, f.__s, g)) ||\n              t.__v === n.__v)\n          ) {\n            (f.props = y),\n              (f.state = f.__s),\n              t.__v !== n.__v && (f.__d = !1),\n              (f.__v = t),\n              (t.__e = n.__e),\n              (t.__k = n.__k),\n              f.__h.length && c.push(f);\n            break e;\n          }\n          null != f.componentWillUpdate && f.componentWillUpdate(y, f.__s, g),\n            null != f.componentDidUpdate &&\n              f.__h.push(function () {\n                f.componentDidUpdate(m, d, v);\n              });\n        }\n        (f.context = g),\n          (f.props = y),\n          (f.state = f.__s),\n          (l = s.__r) && l(t),\n          (f.__d = !1),\n          (f.__v = t),\n          (f.__P = e),\n          (l = f.render(f.props, f.state, f.context)),\n          (f.state = f.__s),\n          null != f.getChildContext && (r = _(_({}, r), f.getChildContext())),\n          p ||\n            null == f.getSnapshotBeforeUpdate ||\n            (v = f.getSnapshotBeforeUpdate(m, d)),\n          (S =\n            null != l && l.type === O && null == l.key ? l.props.children : l),\n          D(e, Array.isArray(S) ? S : [S], t, n, r, o, i, c, a, u),\n          (f.base = t.__e),\n          (t.__h = null),\n          f.__h.length && c.push(f),\n          h && (f.__E = f.__ = null),\n          (f.__e = !1);\n      } else\n        null == i && t.__v === n.__v\n          ? ((t.__k = n.__k), (t.__e = n.__e))\n          : (t.__e = M(n.__e, t, n, r, o, i, c, u));\n      (l = s.diffed) && l(t);\n    } catch (e) {\n      (t.__v = null),\n        (u || null != i) &&\n          ((t.__e = a), (t.__h = !!u), (i[i.indexOf(a)] = null)),\n        s.__e(e, t, n);\n    }\n  }\n  function L(e, t) {\n    s.__c && s.__c(t, e),\n      e.some(function (t) {\n        try {\n          (e = t.__h),\n            (t.__h = []),\n            e.some(function (e) {\n              e.call(t);\n            });\n        } catch (e) {\n          s.__e(e, t.__v);\n        }\n      });\n  }\n  function M(e, t, n, r, o, i, c, a) {\n    var u,\n      l,\n      s,\n      f,\n      p = n.props,\n      m = t.props,\n      d = t.type,\n      y = 0;\n    if ((\"svg\" === d && (o = !0), null != i))\n      for (; y < i.length; y++)\n        if (\n          (u = i[y]) &&\n          (u === e || (d ? u.localName == d : 3 == u.nodeType))\n        ) {\n          (e = u), (i[y] = null);\n          break;\n        }\n    if (null == e) {\n      if (null === d) return document.createTextNode(m);\n      (e = o\n        ? document.createElementNS(\"http://www.w3.org/2000/svg\", d)\n        : document.createElement(d, m.is && m)),\n        (i = null),\n        (a = !1);\n    }\n    if (null === d) p === m || (a && e.data === m) || (e.data = m);\n    else {\n      if (\n        ((i = i && h.slice.call(e.childNodes)),\n        (l = (p = n.props || v).dangerouslySetInnerHTML),\n        (s = m.dangerouslySetInnerHTML),\n        !a)\n      ) {\n        if (null != i)\n          for (p = {}, f = 0; f < e.attributes.length; f++)\n            p[e.attributes[f].name] = e.attributes[f].value;\n        (s || l) &&\n          ((s && ((l && s.__html == l.__html) || s.__html === e.innerHTML)) ||\n            (e.innerHTML = (s && s.__html) || \"\"));\n      }\n      if (\n        ((function (e, t, n, r, o) {\n          var i;\n          for (i in n)\n            \"children\" === i || \"key\" === i || i in t || N(e, i, null, n[i], r);\n          for (i in t)\n            (o && \"function\" != typeof t[i]) ||\n              \"children\" === i ||\n              \"key\" === i ||\n              \"value\" === i ||\n              \"checked\" === i ||\n              n[i] === t[i] ||\n              N(e, i, t[i], n[i], r);\n        })(e, m, p, o, a),\n        s)\n      )\n        t.__k = [];\n      else if (\n        ((y = t.props.children),\n        D(\n          e,\n          Array.isArray(y) ? y : [y],\n          t,\n          n,\n          r,\n          o && \"foreignObject\" !== d,\n          i,\n          c,\n          e.firstChild,\n          a,\n        ),\n        null != i)\n      )\n        for (y = i.length; y--; ) null != i[y] && b(i[y]);\n      a ||\n        (\"value\" in m &&\n          void 0 !== (y = m.value) &&\n          (y !== e.value || (\"progress\" === d && !y)) &&\n          N(e, \"value\", y, p.value, !1),\n        \"checked\" in m &&\n          void 0 !== (y = m.checked) &&\n          y !== e.checked &&\n          N(e, \"checked\", y, p.checked, !1));\n    }\n    return e;\n  }\n  function H(e, t, n) {\n    try {\n      \"function\" == typeof e ? e(t) : (e.current = t);\n    } catch (e) {\n      s.__e(e, n);\n    }\n  }\n  function U(e, t, n) {\n    var r, o, i;\n    if (\n      (s.unmount && s.unmount(e),\n      (r = e.ref) && ((r.current && r.current !== e.__e) || H(r, null, t)),\n      n || \"function\" == typeof e.type || (n = null != (o = e.__e)),\n      (e.__e = e.__d = void 0),\n      null != (r = e.__c))\n    ) {\n      if (r.componentWillUnmount)\n        try {\n          r.componentWillUnmount();\n        } catch (e) {\n          s.__e(e, t);\n        }\n      r.base = r.__P = null;\n    }\n    if ((r = e.__k)) for (i = 0; i < r.length; i++) r[i] && U(r[i], t, n);\n    null != o && b(o);\n  }\n  function F(e, t, n) {\n    return this.constructor(e, n);\n  }\n  function B(e, t, n) {\n    var r, o, i;\n    s.__ && s.__(e, t),\n      (o = (r = \"function\" == typeof n) ? null : (n && n.__k) || t.__k),\n      (i = []),\n      q(\n        t,\n        (e = ((!r && n) || t).__k = g(O, null, [e])),\n        o || v,\n        v,\n        void 0 !== t.ownerSVGElement,\n        !r && n\n          ? [n]\n          : o\n          ? null\n          : t.firstChild\n          ? h.slice.call(t.childNodes)\n          : null,\n        i,\n        !r && n ? n : o ? o.__e : t.firstChild,\n        r,\n      ),\n      L(i, e);\n  }\n  function V(e, t) {\n    B(e, t, V);\n  }\n  function K(e, t, n) {\n    var r,\n      o,\n      i,\n      c = arguments,\n      a = _({}, e.props);\n    for (i in t)\n      \"key\" == i ? (r = t[i]) : \"ref\" == i ? (o = t[i]) : (a[i] = t[i]);\n    if (arguments.length > 3)\n      for (n = [n], i = 3; i < arguments.length; i++) n.push(c[i]);\n    return (\n      null != n && (a.children = n), S(e.type, a, r || e.key, o || e.ref, null)\n    );\n  }\n  (s = {\n    __e: function (e, t) {\n      for (var n, r, o; (t = t.__); )\n        if ((n = t.__c) && !n.__)\n          try {\n            if (\n              ((r = n.constructor) &&\n                null != r.getDerivedStateFromError &&\n                (n.setState(r.getDerivedStateFromError(e)), (o = n.__d)),\n              null != n.componentDidCatch &&\n                (n.componentDidCatch(e), (o = n.__d)),\n              o)\n            )\n              return (n.__E = n);\n          } catch (t) {\n            e = t;\n          }\n      throw e;\n    },\n    __v: 0,\n  }),\n    (w.prototype.setState = function (e, t) {\n      var n;\n      (n =\n        null != this.__s && this.__s !== this.state\n          ? this.__s\n          : (this.__s = _({}, this.state))),\n        \"function\" == typeof e && (e = e(_({}, n), this.props)),\n        e && _(n, e),\n        null != e && this.__v && (t && this.__h.push(t), P(this));\n    }),\n    (w.prototype.forceUpdate = function (e) {\n      this.__v && ((this.__e = !0), e && this.__h.push(e), P(this));\n    }),\n    (w.prototype.render = O),\n    (f = []),\n    (p =\n      \"function\" == typeof Promise\n        ? Promise.prototype.then.bind(Promise.resolve())\n        : setTimeout),\n    (I.__r = 0),\n    (d = 0);\n  var W,\n    z,\n    J,\n    $ = 0,\n    Z = [],\n    Q = s.__b,\n    Y = s.__r,\n    G = s.diffed,\n    X = s.__c,\n    ee = s.unmount;\n  function te(e, t) {\n    s.__h && s.__h(z, e, $ || t), ($ = 0);\n    var n = z.__H || (z.__H = { __: [], __h: [] });\n    return e >= n.__.length && n.__.push({}), n.__[e];\n  }\n  function ne(e) {\n    return ($ = 1), re(pe, e);\n  }\n  function re(e, t, n) {\n    var r = te(W++, 2);\n    return (\n      (r.t = e),\n      r.__c ||\n        ((r.__ = [\n          n ? n(t) : pe(void 0, t),\n          function (e) {\n            var t = r.t(r.__[0], e);\n            r.__[0] !== t && ((r.__ = [t, r.__[1]]), r.__c.setState({}));\n          },\n        ]),\n        (r.__c = z)),\n      r.__\n    );\n  }\n  function oe(e, t) {\n    var n = te(W++, 3);\n    !s.__s && fe(n.__H, t) && ((n.__ = e), (n.__H = t), z.__H.__h.push(n));\n  }\n  function ie(e, t) {\n    var n = te(W++, 4);\n    !s.__s && fe(n.__H, t) && ((n.__ = e), (n.__H = t), z.__h.push(n));\n  }\n  function ce(e, t) {\n    var n = te(W++, 7);\n    return fe(n.__H, t) && ((n.__ = e()), (n.__H = t), (n.__h = e)), n.__;\n  }\n  function ae() {\n    Z.forEach(function (e) {\n      if (e.__P)\n        try {\n          e.__H.__h.forEach(le), e.__H.__h.forEach(se), (e.__H.__h = []);\n        } catch (t) {\n          (e.__H.__h = []), s.__e(t, e.__v);\n        }\n    }),\n      (Z = []);\n  }\n  (s.__b = function (e) {\n    (z = null), Q && Q(e);\n  }),\n    (s.__r = function (e) {\n      Y && Y(e), (W = 0);\n      var t = (z = e.__c).__H;\n      t && (t.__h.forEach(le), t.__h.forEach(se), (t.__h = []));\n    }),\n    (s.diffed = function (e) {\n      G && G(e);\n      var t = e.__c;\n      t &&\n        t.__H &&\n        t.__H.__h.length &&\n        ((1 !== Z.push(t) && J === s.requestAnimationFrame) ||\n          (\n            (J = s.requestAnimationFrame) ||\n            function (e) {\n              var t,\n                n = function () {\n                  clearTimeout(r), ue && cancelAnimationFrame(t), setTimeout(e);\n                },\n                r = setTimeout(n, 100);\n              ue && (t = requestAnimationFrame(n));\n            }\n          )(ae)),\n        (z = void 0);\n    }),\n    (s.__c = function (e, t) {\n      t.some(function (e) {\n        try {\n          e.__h.forEach(le),\n            (e.__h = e.__h.filter(function (e) {\n              return !e.__ || se(e);\n            }));\n        } catch (n) {\n          t.some(function (e) {\n            e.__h && (e.__h = []);\n          }),\n            (t = []),\n            s.__e(n, e.__v);\n        }\n      }),\n        X && X(e, t);\n    }),\n    (s.unmount = function (e) {\n      ee && ee(e);\n      var t = e.__c;\n      if (t && t.__H)\n        try {\n          t.__H.__.forEach(le);\n        } catch (e) {\n          s.__e(e, t.__v);\n        }\n    });\n  var ue = \"function\" == typeof requestAnimationFrame;\n  function le(e) {\n    var t = z;\n    \"function\" == typeof e.__c && e.__c(), (z = t);\n  }\n  function se(e) {\n    var t = z;\n    (e.__c = e.__()), (z = t);\n  }\n  function fe(e, t) {\n    return (\n      !e ||\n      e.length !== t.length ||\n      t.some(function (t, n) {\n        return t !== e[n];\n      })\n    );\n  }\n  function pe(e, t) {\n    return \"function\" == typeof t ? t(e) : t;\n  }\n  function me(e, t) {\n    for (var n in t) e[n] = t[n];\n    return e;\n  }\n  function de(e, t) {\n    for (var n in e) if (\"__source\" !== n && !(n in t)) return !0;\n    for (var r in t) if (\"__source\" !== r && e[r] !== t[r]) return !0;\n    return !1;\n  }\n  function ve(e) {\n    this.props = e;\n  }\n  ((ve.prototype = new w()).isPureReactComponent = !0),\n    (ve.prototype.shouldComponentUpdate = function (e, t) {\n      return de(this.props, e) || de(this.state, t);\n    });\n  var he = s.__b;\n  s.__b = function (e) {\n    e.type && e.type.__f && e.ref && ((e.props.ref = e.ref), (e.ref = null)),\n      he && he(e);\n  };\n  var ye =\n    (\"undefined\" != typeof Symbol &&\n      Symbol.for &&\n      Symbol.for(\"react.forward_ref\")) ||\n    3911;\n  var _e = function (e, t) {\n      return null == e ? null : C(C(e).map(t));\n    },\n    be = {\n      map: _e,\n      forEach: _e,\n      count: function (e) {\n        return e ? C(e).length : 0;\n      },\n      only: function (e) {\n        var t = C(e);\n        if (1 !== t.length) throw \"Children.only\";\n        return t[0];\n      },\n      toArray: C,\n    },\n    ge = s.__e;\n  function Se() {\n    (this.__u = 0), (this.t = null), (this.__b = null);\n  }\n  function Oe(e) {\n    var t = e.__.__c;\n    return t && t.__e && t.__e(e);\n  }\n  function we() {\n    (this.u = null), (this.o = null);\n  }\n  (s.__e = function (e, t, n) {\n    if (e.then)\n      for (var r, o = t; (o = o.__); )\n        if ((r = o.__c) && r.__c)\n          return (\n            null == t.__e && ((t.__e = n.__e), (t.__k = n.__k)), r.__c(e, t)\n          );\n    ge(e, t, n);\n  }),\n    ((Se.prototype = new w()).__c = function (e, t) {\n      var n = t.__c,\n        r = this;\n      null == r.t && (r.t = []), r.t.push(n);\n      var o = Oe(r.__v),\n        i = !1,\n        c = function () {\n          i || ((i = !0), (n.componentWillUnmount = n.__c), o ? o(a) : a());\n        };\n      (n.__c = n.componentWillUnmount),\n        (n.componentWillUnmount = function () {\n          c(), n.__c && n.__c();\n        });\n      var a = function () {\n          if (!--r.__u) {\n            if (r.state.__e) {\n              var e = r.state.__e;\n              r.__v.__k[0] = (function e(t, n, r) {\n                return (\n                  t &&\n                    ((t.__v = null),\n                    (t.__k =\n                      t.__k &&\n                      t.__k.map(function (t) {\n                        return e(t, n, r);\n                      })),\n                    t.__c &&\n                      t.__c.__P === n &&\n                      (t.__e && r.insertBefore(t.__e, t.__d),\n                      (t.__c.__e = !0),\n                      (t.__c.__P = r))),\n                  t\n                );\n              })(e, e.__c.__P, e.__c.__O);\n            }\n            var t;\n            for (r.setState({ __e: (r.__b = null) }); (t = r.t.pop()); )\n              t.forceUpdate();\n          }\n        },\n        u = !0 === t.__h;\n      r.__u++ || u || r.setState({ __e: (r.__b = r.__v.__k[0]) }), e.then(c, c);\n    }),\n    (Se.prototype.componentWillUnmount = function () {\n      this.t = [];\n    }),\n    (Se.prototype.render = function (e, t) {\n      if (this.__b) {\n        if (this.__v.__k) {\n          var n = document.createElement(\"div\"),\n            r = this.__v.__k[0].__c;\n          this.__v.__k[0] = (function e(t, n, r) {\n            return (\n              t &&\n                (t.__c &&\n                  t.__c.__H &&\n                  (t.__c.__H.__.forEach(function (e) {\n                    \"function\" == typeof e.__c && e.__c();\n                  }),\n                  (t.__c.__H = null)),\n                null != (t = me({}, t)).__c &&\n                  (t.__c.__P === r && (t.__c.__P = n), (t.__c = null)),\n                (t.__k =\n                  t.__k &&\n                  t.__k.map(function (t) {\n                    return e(t, n, r);\n                  }))),\n              t\n            );\n          })(this.__b, n, (r.__O = r.__P));\n        }\n        this.__b = null;\n      }\n      var o = t.__e && g(O, null, e.fallback);\n      return o && (o.__h = null), [g(O, null, t.__e ? null : e.children), o];\n    });\n  var Ee = function (e, t, n) {\n    if (\n      (++n[1] === n[0] && e.o.delete(t),\n      e.props.revealOrder && (\"t\" !== e.props.revealOrder[0] || !e.o.size))\n    )\n      for (n = e.u; n; ) {\n        for (; n.length > 3; ) n.pop()();\n        if (n[1] < n[0]) break;\n        e.u = n = n[2];\n      }\n  };\n  function je(e) {\n    return (\n      (this.getChildContext = function () {\n        return e.context;\n      }),\n      e.children\n    );\n  }\n  function Pe(e) {\n    var t = this,\n      n = e.i;\n    (t.componentWillUnmount = function () {\n      B(null, t.l), (t.l = null), (t.i = null);\n    }),\n      t.i && t.i !== n && t.componentWillUnmount(),\n      e.__v\n        ? (t.l ||\n            ((t.i = n),\n            (t.l = {\n              nodeType: 1,\n              parentNode: n,\n              childNodes: [],\n              appendChild: function (e) {\n                this.childNodes.push(e), t.i.appendChild(e);\n              },\n              insertBefore: function (e, n) {\n                this.childNodes.push(e), t.i.appendChild(e);\n              },\n              removeChild: function (e) {\n                this.childNodes.splice(this.childNodes.indexOf(e) >>> 1, 1),\n                  t.i.removeChild(e);\n              },\n            })),\n          B(g(je, { context: t.context }, e.__v), t.l))\n        : t.l && t.componentWillUnmount();\n  }\n  function Ie(e, t) {\n    return g(Pe, { __v: e, i: t });\n  }\n  ((we.prototype = new w()).__e = function (e) {\n    var t = this,\n      n = Oe(t.__v),\n      r = t.o.get(e);\n    return (\n      r[0]++,\n      function (o) {\n        var i = function () {\n          t.props.revealOrder ? (r.push(o), Ee(t, e, r)) : o();\n        };\n        n ? n(i) : i();\n      }\n    );\n  }),\n    (we.prototype.render = function (e) {\n      (this.u = null), (this.o = new Map());\n      var t = C(e.children);\n      e.revealOrder && \"b\" === e.revealOrder[0] && t.reverse();\n      for (var n = t.length; n--; ) this.o.set(t[n], (this.u = [1, 0, this.u]));\n      return e.children;\n    }),\n    (we.prototype.componentDidUpdate = we.prototype.componentDidMount =\n      function () {\n        var e = this;\n        this.o.forEach(function (t, n) {\n          Ee(e, n, t);\n        });\n      });\n  var De =\n      (\"undefined\" != typeof Symbol &&\n        Symbol.for &&\n        Symbol.for(\"react.element\")) ||\n      60103,\n    ke =\n      /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,\n    Ce = function (e) {\n      return (\n        \"undefined\" != typeof Symbol && \"symbol\" == n(Symbol())\n          ? /fil|che|rad/i\n          : /fil|che|ra/i\n      ).test(e);\n    };\n  function Ae(e, t, n) {\n    return (\n      null == t.__k && (t.textContent = \"\"),\n      B(e, t),\n      \"function\" == typeof n && n(),\n      e ? e.__c : null\n    );\n  }\n  (w.prototype.isReactComponent = {}),\n    [\n      \"componentWillMount\",\n      \"componentWillReceiveProps\",\n      \"componentWillUpdate\",\n    ].forEach(function (e) {\n      Object.defineProperty(w.prototype, e, {\n        configurable: !0,\n        get: function () {\n          return this[\"UNSAFE_\" + e];\n        },\n        set: function (t) {\n          Object.defineProperty(this, e, {\n            configurable: !0,\n            writable: !0,\n            value: t,\n          });\n        },\n      });\n    });\n  var xe = s.event;\n  function Ne() {}\n  function Te() {\n    return this.cancelBubble;\n  }\n  function Re() {\n    return this.defaultPrevented;\n  }\n  s.event = function (e) {\n    return (\n      xe && (e = xe(e)),\n      (e.persist = Ne),\n      (e.isPropagationStopped = Te),\n      (e.isDefaultPrevented = Re),\n      (e.nativeEvent = e)\n    );\n  };\n  var qe,\n    Le = {\n      configurable: !0,\n      get: function () {\n        return this.class;\n      },\n    },\n    Me = s.vnode;\n  s.vnode = function (e) {\n    var t = e.type,\n      n = e.props,\n      r = n;\n    if (\"string\" == typeof t) {\n      for (var o in ((r = {}), n)) {\n        var i = n[o];\n        (\"value\" === o && \"defaultValue\" in n && null == i) ||\n          (\"defaultValue\" === o && \"value\" in n && null == n.value\n            ? (o = \"value\")\n            : \"download\" === o && !0 === i\n            ? (i = \"\")\n            : /ondoubleclick/i.test(o)\n            ? (o = \"ondblclick\")\n            : /^onchange(textarea|input)/i.test(o + t) && !Ce(n.type)\n            ? (o = \"oninput\")\n            : /^on(Ani|Tra|Tou|BeforeInp)/.test(o)\n            ? (o = o.toLowerCase())\n            : ke.test(o)\n            ? (o = o.replace(/[A-Z0-9]/, \"-$&\").toLowerCase())\n            : null === i && (i = void 0),\n          (r[o] = i));\n      }\n      \"select\" == t &&\n        r.multiple &&\n        Array.isArray(r.value) &&\n        (r.value = C(n.children).forEach(function (e) {\n          e.props.selected = -1 != r.value.indexOf(e.props.value);\n        })),\n        \"select\" == t &&\n          null != r.defaultValue &&\n          (r.value = C(n.children).forEach(function (e) {\n            e.props.selected = r.multiple\n              ? -1 != r.defaultValue.indexOf(e.props.value)\n              : r.defaultValue == e.props.value;\n          })),\n        (e.props = r);\n    }\n    t &&\n      n.class != n.className &&\n      ((Le.enumerable = \"className\" in n),\n      null != n.className && (r.class = n.className),\n      Object.defineProperty(r, \"className\", Le)),\n      (e.$$typeof = De),\n      Me && Me(e);\n  };\n  var He = s.__r;\n  s.__r = function (e) {\n    He && He(e), (qe = e.__c);\n  };\n  var Ue = {\n    ReactCurrentDispatcher: {\n      current: {\n        readContext: function (e) {\n          return qe.__n[e.__c].props.value;\n        },\n      },\n    },\n  };\n  \"object\" ==\n    (\"undefined\" == typeof performance ? \"undefined\" : n(performance)) &&\n    \"function\" == typeof performance.now &&\n    performance.now.bind(performance);\n  function Fe(e) {\n    return !!e && e.$$typeof === De;\n  }\n  var Be = {\n      useState: ne,\n      useReducer: re,\n      useEffect: oe,\n      useLayoutEffect: ie,\n      useRef: function (e) {\n        return (\n          ($ = 5),\n          ce(function () {\n            return { current: e };\n          }, [])\n        );\n      },\n      useImperativeHandle: function (e, t, n) {\n        ($ = 6),\n          ie(\n            function () {\n              \"function\" == typeof e ? e(t()) : e && (e.current = t());\n            },\n            null == n ? n : n.concat(e),\n          );\n      },\n      useMemo: ce,\n      useCallback: function (e, t) {\n        return (\n          ($ = 8),\n          ce(function () {\n            return e;\n          }, t)\n        );\n      },\n      useContext: function (e) {\n        var t = z.context[e.__c],\n          n = te(W++, 9);\n        return (\n          (n.__c = e),\n          t ? (null == n.__ && ((n.__ = !0), t.sub(z)), t.props.value) : e.__\n        );\n      },\n      useDebugValue: function (e, t) {\n        s.useDebugValue && s.useDebugValue(t ? t(e) : e);\n      },\n      version: \"16.8.0\",\n      Children: be,\n      render: Ae,\n      hydrate: function (e, t, n) {\n        return V(e, t), \"function\" == typeof n && n(), e ? e.__c : null;\n      },\n      unmountComponentAtNode: function (e) {\n        return !!e.__k && (B(null, e), !0);\n      },\n      createPortal: Ie,\n      createElement: g,\n      createContext: function (e, t) {\n        var n = {\n          __c: (t = \"__cC\" + d++),\n          __: e,\n          Consumer: function (e, t) {\n            return e.children(t);\n          },\n          Provider: function (e) {\n            var n, r;\n            return (\n              this.getChildContext ||\n                ((n = []),\n                ((r = {})[t] = this),\n                (this.getChildContext = function () {\n                  return r;\n                }),\n                (this.shouldComponentUpdate = function (e) {\n                  this.props.value !== e.value && n.some(P);\n                }),\n                (this.sub = function (e) {\n                  n.push(e);\n                  var t = e.componentWillUnmount;\n                  e.componentWillUnmount = function () {\n                    n.splice(n.indexOf(e), 1), t && t.call(e);\n                  };\n                })),\n              e.children\n            );\n          },\n        };\n        return (n.Provider.__ = n.Consumer.contextType = n);\n      },\n      createFactory: function (e) {\n        return g.bind(null, e);\n      },\n      cloneElement: function (e) {\n        return Fe(e) ? K.apply(null, arguments) : e;\n      },\n      createRef: function () {\n        return { current: null };\n      },\n      Fragment: O,\n      isValidElement: Fe,\n      findDOMNode: function (e) {\n        return (e && (e.base || (1 === e.nodeType && e))) || null;\n      },\n      Component: w,\n      PureComponent: ve,\n      memo: function (e, t) {\n        function n(e) {\n          var n = this.props.ref,\n            r = n == e.ref;\n          return (\n            !r && n && (n.call ? n(null) : (n.current = null)),\n            t ? !t(this.props, e) || !r : de(this.props, e)\n          );\n        }\n        function r(t) {\n          return (this.shouldComponentUpdate = n), g(e, t);\n        }\n        return (\n          (r.displayName = \"Memo(\" + (e.displayName || e.name) + \")\"),\n          (r.prototype.isReactComponent = !0),\n          (r.__f = !0),\n          r\n        );\n      },\n      forwardRef: function (e) {\n        function t(t, r) {\n          var o = me({}, t);\n          return (\n            delete o.ref,\n            e(\n              o,\n              (r = t.ref || r) && (\"object\" != n(r) || \"current\" in r)\n                ? r\n                : null,\n            )\n          );\n        }\n        return (\n          (t.$$typeof = ye),\n          (t.render = t),\n          (t.prototype.isReactComponent = t.__f = !0),\n          (t.displayName = \"ForwardRef(\" + (e.displayName || e.name) + \")\"),\n          t\n        );\n      },\n      unstable_batchedUpdates: function (e, t) {\n        return e(t);\n      },\n      StrictMode: O,\n      Suspense: Se,\n      SuspenseList: we,\n      lazy: function (e) {\n        var t, n, r;\n        function o(o) {\n          if (\n            (t ||\n              (t = e()).then(\n                function (e) {\n                  n = e.default || e;\n                },\n                function (e) {\n                  r = e;\n                },\n              ),\n            r)\n          )\n            throw r;\n          if (!n) throw t;\n          return g(n, o);\n        }\n        return (o.displayName = \"Lazy\"), (o.__f = !0), o;\n      },\n      __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: Ue,\n    },\n    Ve = [\"facetName\", \"facetQuery\"];\n  function Ke(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function We(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Ke(Object(n), !0).forEach(function (t) {\n            ze(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Ke(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function ze(e, t, n) {\n    return (\n      t in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Je() {\n    return (\n      (Je =\n        Object.assign ||\n        function (e) {\n          for (var t = 1; t < arguments.length; t++) {\n            var n = arguments[t];\n            for (var r in n)\n              Object.prototype.hasOwnProperty.call(n, r) && (e[r] = n[r]);\n          }\n          return e;\n        }),\n      Je.apply(this, arguments)\n    );\n  }\n  function $e(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function Ze(e, t) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return e;\n      })(e) ||\n      (function (e, t) {\n        var n =\n          null == e\n            ? null\n            : (\"undefined\" != typeof Symbol && e[Symbol.iterator]) ||\n              e[\"@@iterator\"];\n        if (null != n) {\n          var r,\n            o,\n            i = [],\n            c = !0,\n            a = !1;\n          try {\n            for (\n              n = n.call(e);\n              !(c = (r = n.next()).done) &&\n              (i.push(r.value), !t || i.length !== t);\n              c = !0\n            );\n          } catch (e) {\n            (a = !0), (o = e);\n          } finally {\n            try {\n              c || null == n.return || n.return();\n            } finally {\n              if (a) throw o;\n            }\n          }\n          return i;\n        }\n      })(e, t) ||\n      Qe(e, t) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function Qe(e, t) {\n    if (e) {\n      if (\"string\" == typeof e) return Ye(e, t);\n      var n = Object.prototype.toString.call(e).slice(8, -1);\n      return (\n        \"Object\" === n && e.constructor && (n = e.constructor.name),\n        \"Map\" === n || \"Set\" === n\n          ? Array.from(e)\n          : \"Arguments\" === n ||\n            /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n          ? Ye(e, t)\n          : void 0\n      );\n    }\n  }\n  function Ye(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function Ge() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"15\", height: \"15\", className: \"DocSearch-Control-Key-Icon\" },\n      Be.createElement(\"path\", {\n        d: \"M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953\",\n        strokeWidth: \"1.2\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        strokeLinecap: \"square\",\n      }),\n    );\n  }\n  function Xe() {\n    return Be.createElement(\n      \"svg\",\n      {\n        width: \"20\",\n        height: \"20\",\n        className: \"DocSearch-Search-Icon\",\n        viewBox: \"0 0 20 20\",\n        \"aria-hidden\": \"true\",\n      },\n      Be.createElement(\"path\", {\n        d: \"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  var et = [\"translations\"],\n    tt = Be.forwardRef(function (e, t) {\n      var n = e.translations,\n        r = void 0 === n ? {} : n,\n        o = $e(e, et),\n        i = r.buttonText,\n        c = void 0 === i ? \"Search\" : i,\n        a = r.buttonAriaLabel,\n        u = void 0 === a ? \"Search\" : a,\n        l = Ze(ne(null), 2),\n        s = l[0],\n        f = l[1];\n      return (\n        oe(function () {\n          \"undefined\" != typeof navigator &&\n            (/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)\n              ? f(\"⌘\")\n              : f(\"Ctrl\"));\n        }, []),\n        Be.createElement(\n          \"button\",\n          Je(\n            {\n              type: \"button\",\n              className: \"DocSearch DocSearch-Button\",\n              \"aria-label\": u,\n            },\n            o,\n            { ref: t },\n          ),\n          Be.createElement(\n            \"span\",\n            { className: \"DocSearch-Button-Container\" },\n            Be.createElement(Xe, null),\n            Be.createElement(\n              \"span\",\n              { className: \"DocSearch-Button-Placeholder\" },\n              c,\n            ),\n          ),\n          Be.createElement(\n            \"span\",\n            { className: \"DocSearch-Button-Keys\" },\n            null !== s &&\n              Be.createElement(\n                Be.Fragment,\n                null,\n                Be.createElement(\n                  nt,\n                  { reactsToKey: \"Ctrl\" === s ? \"Ctrl\" : \"Meta\" },\n                  \"Ctrl\" === s ? Be.createElement(Ge, null) : s,\n                ),\n                Be.createElement(nt, { reactsToKey: \"k\" }, \"K\"),\n              ),\n          ),\n        )\n      );\n    });\n  function nt(e) {\n    var t = e.reactsToKey,\n      n = e.children,\n      r = Ze(ne(!1), 2),\n      o = r[0],\n      i = r[1];\n    return (\n      oe(\n        function () {\n          if (t)\n            return (\n              window.addEventListener(\"keydown\", e),\n              window.addEventListener(\"keyup\", n),\n              function () {\n                window.removeEventListener(\"keydown\", e),\n                  window.removeEventListener(\"keyup\", n);\n              }\n            );\n          function e(e) {\n            e.key === t && i(!0);\n          }\n          function n(e) {\n            (e.key !== t && \"Meta\" !== e.key) || i(!1);\n          }\n        },\n        [t],\n      ),\n      Be.createElement(\n        \"kbd\",\n        {\n          className: o\n            ? \"DocSearch-Button-Key DocSearch-Button-Key--pressed\"\n            : \"DocSearch-Button-Key\",\n        },\n        n,\n      )\n    );\n  }\n  function rt(e, t) {\n    var n = void 0;\n    return function () {\n      for (var r = arguments.length, o = new Array(r), i = 0; i < r; i++)\n        o[i] = arguments[i];\n      n && clearTimeout(n),\n        (n = setTimeout(function () {\n          return e.apply(void 0, o);\n        }, t));\n    };\n  }\n  function ot(e) {\n    return e.reduce(function (e, t) {\n      return e.concat(t);\n    }, []);\n  }\n  var it = 0;\n  function ct(e) {\n    return 0 === e.collections.length\n      ? 0\n      : e.collections.reduce(function (e, t) {\n          return e + t.items.length;\n        }, 0);\n  }\n  function at(e) {\n    return e !== Object(e);\n  }\n  function ut(e, t) {\n    if (e === t) return !0;\n    if (at(e) || at(t) || \"function\" == typeof e || \"function\" == typeof t)\n      return e === t;\n    if (Object.keys(e).length !== Object.keys(t).length) return !1;\n    for (var n = 0, r = Object.keys(e); n < r.length; n++) {\n      var o = r[n];\n      if (!(o in t)) return !1;\n      if (!ut(e[o], t[o])) return !1;\n    }\n    return !0;\n  }\n  var lt = function () {},\n    st = [{ segment: \"autocomplete-core\", version: \"1.9.3\" }];\n  function ft(e) {\n    var t = e.item,\n      n = e.items;\n    return {\n      index: t.__autocomplete_indexName,\n      items: [t],\n      positions: [\n        1 +\n          n.findIndex(function (e) {\n            return e.objectID === t.objectID;\n          }),\n      ],\n      queryID: t.__autocomplete_queryID,\n      algoliaSource: [\"autocomplete\"],\n    };\n  }\n  function pt(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  var mt = [\"items\"],\n    dt = [\"items\"];\n  function vt(e) {\n    return (\n      (vt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      vt(e)\n    );\n  }\n  function ht(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return yt(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      (function (e, t) {\n        if (e) {\n          if (\"string\" == typeof e) return yt(e, t);\n          var n = Object.prototype.toString.call(e).slice(8, -1);\n          return (\n            \"Object\" === n && e.constructor && (n = e.constructor.name),\n            \"Map\" === n || \"Set\" === n\n              ? Array.from(e)\n              : \"Arguments\" === n ||\n                /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n              ? yt(e, t)\n              : void 0\n          );\n        }\n      })(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function yt(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function _t(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function bt(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function gt(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? bt(Object(n), !0).forEach(function (t) {\n            St(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : bt(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function St(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== vt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== vt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === vt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Ot(e) {\n    for (\n      var t =\n          arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 20,\n        n = [],\n        r = 0;\n      r < e.objectIDs.length;\n      r += t\n    )\n      n.push(gt(gt({}, e), {}, { objectIDs: e.objectIDs.slice(r, r + t) }));\n    return n;\n  }\n  function wt(e) {\n    return e.map(function (e) {\n      var t = e.items,\n        n = _t(e, mt);\n      return gt(\n        gt({}, n),\n        {},\n        {\n          objectIDs:\n            (null == t\n              ? void 0\n              : t.map(function (e) {\n                  return e.objectID;\n                })) || n.objectIDs,\n        },\n      );\n    });\n  }\n  function Et(e) {\n    var t,\n      n,\n      r,\n      o =\n        ((t = (function (e, t) {\n          return (\n            (function (e) {\n              if (Array.isArray(e)) return e;\n            })(e) ||\n            (function (e, t) {\n              var n =\n                null == e\n                  ? null\n                  : (\"undefined\" != typeof Symbol && e[Symbol.iterator]) ||\n                    e[\"@@iterator\"];\n              if (null != n) {\n                var r,\n                  o,\n                  i,\n                  c,\n                  a = [],\n                  u = !0,\n                  l = !1;\n                try {\n                  if (((i = (n = n.call(e)).next), 0 === t)) {\n                    if (Object(n) !== n) return;\n                    u = !1;\n                  } else\n                    for (\n                      ;\n                      !(u = (r = i.call(n)).done) &&\n                      (a.push(r.value), a.length !== t);\n                      u = !0\n                    );\n                } catch (e) {\n                  (l = !0), (o = e);\n                } finally {\n                  try {\n                    if (\n                      !u &&\n                      null != n.return &&\n                      ((c = n.return()), Object(c) !== c)\n                    )\n                      return;\n                  } finally {\n                    if (l) throw o;\n                  }\n                }\n                return a;\n              }\n            })(e, t) ||\n            (function (e, t) {\n              if (e) {\n                if (\"string\" == typeof e) return pt(e, t);\n                var n = Object.prototype.toString.call(e).slice(8, -1);\n                return (\n                  \"Object\" === n && e.constructor && (n = e.constructor.name),\n                  \"Map\" === n || \"Set\" === n\n                    ? Array.from(e)\n                    : \"Arguments\" === n ||\n                      /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n                    ? pt(e, t)\n                    : void 0\n                );\n              }\n            })(e, t) ||\n            (function () {\n              throw new TypeError(\n                \"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n              );\n            })()\n          );\n        })((e.version || \"\").split(\".\").map(Number), 2)),\n        (n = t[0]),\n        (r = t[1]),\n        n >= 3 || (2 === n && r >= 4) || (1 === n && r >= 10));\n    function i(t, n, r) {\n      if (o && void 0 !== r) {\n        var i = r[0].__autocomplete_algoliaCredentials,\n          c = {\n            \"X-Algolia-Application-Id\": i.appId,\n            \"X-Algolia-API-Key\": i.apiKey,\n          };\n        e.apply(void 0, [t].concat(ht(n), [{ headers: c }]));\n      } else e.apply(void 0, [t].concat(ht(n)));\n    }\n    return {\n      init: function (t, n) {\n        e(\"init\", { appId: t, apiKey: n });\n      },\n      setUserToken: function (t) {\n        e(\"setUserToken\", t);\n      },\n      clickedObjectIDsAfterSearch: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"clickedObjectIDsAfterSearch\", wt(t), t[0].items);\n      },\n      clickedObjectIDs: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"clickedObjectIDs\", wt(t), t[0].items);\n      },\n      clickedFilters: function () {\n        for (var t = arguments.length, n = new Array(t), r = 0; r < t; r++)\n          n[r] = arguments[r];\n        n.length > 0 && e.apply(void 0, [\"clickedFilters\"].concat(n));\n      },\n      convertedObjectIDsAfterSearch: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"convertedObjectIDsAfterSearch\", wt(t), t[0].items);\n      },\n      convertedObjectIDs: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 && i(\"convertedObjectIDs\", wt(t), t[0].items);\n      },\n      convertedFilters: function () {\n        for (var t = arguments.length, n = new Array(t), r = 0; r < t; r++)\n          n[r] = arguments[r];\n        n.length > 0 && e.apply(void 0, [\"convertedFilters\"].concat(n));\n      },\n      viewedObjectIDs: function () {\n        for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)\n          t[n] = arguments[n];\n        t.length > 0 &&\n          t\n            .reduce(function (e, t) {\n              var n = t.items,\n                r = _t(t, dt);\n              return [].concat(\n                ht(e),\n                ht(\n                  Ot(\n                    gt(\n                      gt({}, r),\n                      {},\n                      {\n                        objectIDs:\n                          (null == n\n                            ? void 0\n                            : n.map(function (e) {\n                                return e.objectID;\n                              })) || r.objectIDs,\n                      },\n                    ),\n                  ).map(function (e) {\n                    return { items: n, payload: e };\n                  }),\n                ),\n              );\n            }, [])\n            .forEach(function (e) {\n              var t = e.items;\n              return i(\"viewedObjectIDs\", [e.payload], t);\n            });\n      },\n      viewedFilters: function () {\n        for (var t = arguments.length, n = new Array(t), r = 0; r < t; r++)\n          n[r] = arguments[r];\n        n.length > 0 && e.apply(void 0, [\"viewedFilters\"].concat(n));\n      },\n    };\n  }\n  function jt(e) {\n    var t = e.items.reduce(function (e, t) {\n      var n;\n      return (\n        (e[t.__autocomplete_indexName] = (\n          null !== (n = e[t.__autocomplete_indexName]) && void 0 !== n ? n : []\n        ).concat(t)),\n        e\n      );\n    }, {});\n    return Object.keys(t).map(function (e) {\n      return { index: e, items: t[e], algoliaSource: [\"autocomplete\"] };\n    });\n  }\n  function Pt(e) {\n    return e.objectID && e.__autocomplete_indexName && e.__autocomplete_queryID;\n  }\n  function It(e) {\n    return (\n      (It =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      It(e)\n    );\n  }\n  function Dt(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return kt(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      (function (e, t) {\n        if (e) {\n          if (\"string\" == typeof e) return kt(e, t);\n          var n = Object.prototype.toString.call(e).slice(8, -1);\n          return (\n            \"Object\" === n && e.constructor && (n = e.constructor.name),\n            \"Map\" === n || \"Set\" === n\n              ? Array.from(e)\n              : \"Arguments\" === n ||\n                /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n              ? kt(e, t)\n              : void 0\n          );\n        }\n      })(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function kt(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function Ct(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function At(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Ct(Object(n), !0).forEach(function (t) {\n            xt(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Ct(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function xt(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== It(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== It(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === It(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  var Nt = \"https://cdn.jsdelivr.net/npm/search-insights@\".concat(\n      \"2.6.0\",\n      \"/dist/search-insights.min.js\",\n    ),\n    Tt = rt(function (e) {\n      var t = e.onItemsChange,\n        n = e.items,\n        r = e.insights,\n        o = e.state;\n      t({\n        insights: r,\n        insightsEvents: jt({ items: n }).map(function (e) {\n          return At({ eventName: \"Items Viewed\" }, e);\n        }),\n        state: o,\n      });\n    }, 400);\n  function Rt(e) {\n    var t = (function (e) {\n        return At(\n          {\n            onItemsChange: function (e) {\n              var t = e.insights,\n                n = e.insightsEvents;\n              t.viewedObjectIDs.apply(\n                t,\n                Dt(\n                  n.map(function (e) {\n                    return At(\n                      At({}, e),\n                      {},\n                      {\n                        algoliaSource: [].concat(Dt(e.algoliaSource || []), [\n                          \"autocomplete-internal\",\n                        ]),\n                      },\n                    );\n                  }),\n                ),\n              );\n            },\n            onSelect: function (e) {\n              var t = e.insights,\n                n = e.insightsEvents;\n              t.clickedObjectIDsAfterSearch.apply(\n                t,\n                Dt(\n                  n.map(function (e) {\n                    return At(\n                      At({}, e),\n                      {},\n                      {\n                        algoliaSource: [].concat(Dt(e.algoliaSource || []), [\n                          \"autocomplete-internal\",\n                        ]),\n                      },\n                    );\n                  }),\n                ),\n              );\n            },\n            onActive: lt,\n          },\n          e,\n        );\n      })(e),\n      n = t.insightsClient,\n      r = t.onItemsChange,\n      o = t.onSelect,\n      i = t.onActive,\n      c = n;\n    n ||\n      (\"undefined\" != typeof window &&\n        (function (e) {\n          var t = e.window,\n            n = t.AlgoliaAnalyticsObject || \"aa\";\n          \"string\" == typeof n && (c = t[n]),\n            c ||\n              ((t.AlgoliaAnalyticsObject = n),\n              t[n] ||\n                (t[n] = function () {\n                  t[n].queue || (t[n].queue = []);\n                  for (\n                    var e = arguments.length, r = new Array(e), o = 0;\n                    o < e;\n                    o++\n                  )\n                    r[o] = arguments[o];\n                  t[n].queue.push(r);\n                }),\n              (t[n].version = \"2.6.0\"),\n              (c = t[n]),\n              (function (e) {\n                var t =\n                  \"[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete\";\n                try {\n                  var n = e.document.createElement(\"script\");\n                  (n.async = !0),\n                    (n.src = Nt),\n                    (n.onerror = function () {\n                      console.error(t);\n                    }),\n                    document.body.appendChild(n);\n                } catch (e) {\n                  console.error(t);\n                }\n              })(t));\n        })({ window: window }));\n    var a = Et(c),\n      u = { current: [] },\n      l = rt(function (e) {\n        var t = e.state;\n        if (t.isOpen) {\n          var n = t.collections\n            .reduce(function (e, t) {\n              return [].concat(Dt(e), Dt(t.items));\n            }, [])\n            .filter(Pt);\n          ut(\n            u.current.map(function (e) {\n              return e.objectID;\n            }),\n            n.map(function (e) {\n              return e.objectID;\n            }),\n          ) ||\n            ((u.current = n),\n            n.length > 0 &&\n              Tt({ onItemsChange: r, items: n, insights: a, state: t }));\n        }\n      }, 0);\n    return {\n      name: \"aa.algoliaInsightsPlugin\",\n      subscribe: function (e) {\n        var t = e.setContext,\n          n = e.onSelect,\n          r = e.onActive;\n        c(\"addAlgoliaAgent\", \"insights-plugin\"),\n          t({\n            algoliaInsightsPlugin: {\n              __algoliaSearchParameters: { clickAnalytics: !0 },\n              insights: a,\n            },\n          }),\n          n(function (e) {\n            var t = e.item,\n              n = e.state,\n              r = e.event;\n            Pt(t) &&\n              o({\n                state: n,\n                event: r,\n                insights: a,\n                item: t,\n                insightsEvents: [\n                  At(\n                    { eventName: \"Item Selected\" },\n                    ft({ item: t, items: u.current }),\n                  ),\n                ],\n              });\n          }),\n          r(function (e) {\n            var t = e.item,\n              n = e.state,\n              r = e.event;\n            Pt(t) &&\n              i({\n                state: n,\n                event: r,\n                insights: a,\n                item: t,\n                insightsEvents: [\n                  At(\n                    { eventName: \"Item Active\" },\n                    ft({ item: t, items: u.current }),\n                  ),\n                ],\n              });\n          });\n      },\n      onStateChange: function (e) {\n        var t = e.state;\n        l({ state: t });\n      },\n      __autocomplete_pluginOptions: e,\n    };\n  }\n  function qt(e, t) {\n    var n = t;\n    return {\n      then: function (t, r) {\n        return qt(e.then(Mt(t, n, e), Mt(r, n, e)), n);\n      },\n      catch: function (t) {\n        return qt(e.catch(Mt(t, n, e)), n);\n      },\n      finally: function (t) {\n        return (\n          t && n.onCancelList.push(t),\n          qt(\n            e.finally(\n              Mt(\n                t &&\n                  function () {\n                    return (n.onCancelList = []), t();\n                  },\n                n,\n                e,\n              ),\n            ),\n            n,\n          )\n        );\n      },\n      cancel: function () {\n        n.isCanceled = !0;\n        var e = n.onCancelList;\n        (n.onCancelList = []),\n          e.forEach(function (e) {\n            e();\n          });\n      },\n      isCanceled: function () {\n        return !0 === n.isCanceled;\n      },\n    };\n  }\n  function Lt(e) {\n    return qt(e, { isCanceled: !1, onCancelList: [] });\n  }\n  function Mt(e, t, n) {\n    return e\n      ? function (n) {\n          return t.isCanceled ? n : e(n);\n        }\n      : n;\n  }\n  function Ht(e, t, n, r) {\n    if (!n) return null;\n    if (e < 0 && (null === t || (null !== r && 0 === t))) return n + e;\n    var o = (null === t ? -1 : t) + e;\n    return o <= -1 || o >= n ? (null === r ? null : 0) : o;\n  }\n  function Ut(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Ft(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Ut(Object(n), !0).forEach(function (t) {\n            Bt(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Ut(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Bt(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Vt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Vt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Vt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Vt(e) {\n    return (\n      (Vt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Vt(e)\n    );\n  }\n  function Kt(e) {\n    var t = (function (e) {\n      var t = e.collections\n        .map(function (e) {\n          return e.items.length;\n        })\n        .reduce(function (e, t, n) {\n          var r = (e[n - 1] || 0) + t;\n          return e.push(r), e;\n        }, [])\n        .reduce(function (t, n) {\n          return n <= e.activeItemId ? t + 1 : t;\n        }, 0);\n      return e.collections[t];\n    })(e);\n    if (!t) return null;\n    var n =\n        t.items[\n          (function (e) {\n            for (\n              var t = e.state, n = e.collection, r = !1, o = 0, i = 0;\n              !1 === r;\n\n            ) {\n              var c = t.collections[o];\n              if (c === n) {\n                r = !0;\n                break;\n              }\n              (i += c.items.length), o++;\n            }\n            return t.activeItemId - i;\n          })({ state: e, collection: t })\n        ],\n      r = t.source;\n    return {\n      item: n,\n      itemInputValue: r.getItemInputValue({ item: n, state: e }),\n      itemUrl: r.getItemUrl({ item: n, state: e }),\n      source: r,\n    };\n  }\n  var Wt = /((gt|sm)-|galaxy nexus)|samsung[- ]|samsungbrowser/i;\n  function zt(e) {\n    return (\n      (zt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      zt(e)\n    );\n  }\n  function Jt(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function $t(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== zt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== zt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === zt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Zt(e) {\n    return (\n      (Zt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Zt(e)\n    );\n  }\n  function Qt(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Yt(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Qt(Object(n), !0).forEach(function (t) {\n            Gt(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Qt(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Gt(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Zt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Zt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Zt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Xt(e) {\n    return (\n      (Xt =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Xt(e)\n    );\n  }\n  function en(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function tn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function nn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? tn(Object(n), !0).forEach(function (t) {\n            rn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : tn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function rn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Xt(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Xt(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Xt(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function on(e, t) {\n    var n,\n      r = \"undefined\" != typeof window ? window : {},\n      o = e.plugins || [];\n    return nn(\n      nn(\n        {\n          debug: !1,\n          openOnFocus: !1,\n          placeholder: \"\",\n          autoFocus: !1,\n          defaultActiveItemId: null,\n          stallThreshold: 300,\n          insights: !1,\n          environment: r,\n          shouldPanelOpen: function (e) {\n            return ct(e.state) > 0;\n          },\n          reshape: function (e) {\n            return e.sources;\n          },\n        },\n        e,\n      ),\n      {},\n      {\n        id:\n          null !== (n = e.id) && void 0 !== n\n            ? n\n            : \"autocomplete-\".concat(it++),\n        plugins: o,\n        initialState: nn(\n          {\n            activeItemId: null,\n            query: \"\",\n            completion: null,\n            collections: [],\n            isOpen: !1,\n            status: \"idle\",\n            context: {},\n          },\n          e.initialState,\n        ),\n        onStateChange: function (t) {\n          var n;\n          null === (n = e.onStateChange) || void 0 === n || n.call(e, t),\n            o.forEach(function (e) {\n              var n;\n              return null === (n = e.onStateChange) || void 0 === n\n                ? void 0\n                : n.call(e, t);\n            });\n        },\n        onSubmit: function (t) {\n          var n;\n          null === (n = e.onSubmit) || void 0 === n || n.call(e, t),\n            o.forEach(function (e) {\n              var n;\n              return null === (n = e.onSubmit) || void 0 === n\n                ? void 0\n                : n.call(e, t);\n            });\n        },\n        onReset: function (t) {\n          var n;\n          null === (n = e.onReset) || void 0 === n || n.call(e, t),\n            o.forEach(function (e) {\n              var n;\n              return null === (n = e.onReset) || void 0 === n\n                ? void 0\n                : n.call(e, t);\n            });\n        },\n        getSources: function (n) {\n          return Promise.all(\n            []\n              .concat(\n                (function (e) {\n                  return (\n                    (function (e) {\n                      if (Array.isArray(e)) return en(e);\n                    })(e) ||\n                    (function (e) {\n                      if (\n                        (\"undefined\" != typeof Symbol &&\n                          null != e[Symbol.iterator]) ||\n                        null != e[\"@@iterator\"]\n                      )\n                        return Array.from(e);\n                    })(e) ||\n                    (function (e, t) {\n                      if (e) {\n                        if (\"string\" == typeof e) return en(e, t);\n                        var n = Object.prototype.toString.call(e).slice(8, -1);\n                        return (\n                          \"Object\" === n &&\n                            e.constructor &&\n                            (n = e.constructor.name),\n                          \"Map\" === n || \"Set\" === n\n                            ? Array.from(e)\n                            : \"Arguments\" === n ||\n                              /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n                            ? en(e, t)\n                            : void 0\n                        );\n                      }\n                    })(e) ||\n                    (function () {\n                      throw new TypeError(\n                        \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n                      );\n                    })()\n                  );\n                })(\n                  o.map(function (e) {\n                    return e.getSources;\n                  }),\n                ),\n                [e.getSources],\n              )\n              .filter(Boolean)\n              .map(function (e) {\n                return (function (e, t) {\n                  var n = [];\n                  return Promise.resolve(e(t)).then(function (e) {\n                    return Promise.all(\n                      e\n                        .filter(function (e) {\n                          return Boolean(e);\n                        })\n                        .map(function (e) {\n                          if ((e.sourceId, n.includes(e.sourceId)))\n                            throw new Error(\n                              \"[Autocomplete] The `sourceId` \".concat(\n                                JSON.stringify(e.sourceId),\n                                \" is not unique.\",\n                              ),\n                            );\n                          n.push(e.sourceId);\n                          var t = {\n                            getItemInputValue: function (e) {\n                              return e.state.query;\n                            },\n                            getItemUrl: function () {},\n                            onSelect: function (e) {\n                              (0, e.setIsOpen)(!1);\n                            },\n                            onActive: lt,\n                            onResolve: lt,\n                          };\n                          Object.keys(t).forEach(function (e) {\n                            t[e].__default = !0;\n                          });\n                          var r = Ft(Ft({}, t), e);\n                          return Promise.resolve(r);\n                        }),\n                    );\n                  });\n                })(e, n);\n              }),\n          )\n            .then(function (e) {\n              return ot(e);\n            })\n            .then(function (e) {\n              return e.map(function (e) {\n                return nn(\n                  nn({}, e),\n                  {},\n                  {\n                    onSelect: function (n) {\n                      e.onSelect(n),\n                        t.forEach(function (e) {\n                          var t;\n                          return null === (t = e.onSelect) || void 0 === t\n                            ? void 0\n                            : t.call(e, n);\n                        });\n                    },\n                    onActive: function (n) {\n                      e.onActive(n),\n                        t.forEach(function (e) {\n                          var t;\n                          return null === (t = e.onActive) || void 0 === t\n                            ? void 0\n                            : t.call(e, n);\n                        });\n                    },\n                    onResolve: function (n) {\n                      e.onResolve(n),\n                        t.forEach(function (e) {\n                          var t;\n                          return null === (t = e.onResolve) || void 0 === t\n                            ? void 0\n                            : t.call(e, n);\n                        });\n                    },\n                  },\n                );\n              });\n            });\n        },\n        navigator: nn(\n          {\n            navigate: function (e) {\n              var t = e.itemUrl;\n              r.location.assign(t);\n            },\n            navigateNewTab: function (e) {\n              var t = e.itemUrl,\n                n = r.open(t, \"_blank\", \"noopener\");\n              null == n || n.focus();\n            },\n            navigateNewWindow: function (e) {\n              var t = e.itemUrl;\n              r.open(t, \"_blank\", \"noopener\");\n            },\n          },\n          e.navigator,\n        ),\n      },\n    );\n  }\n  function cn(e) {\n    return (\n      (cn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      cn(e)\n    );\n  }\n  function an(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function un(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? an(Object(n), !0).forEach(function (t) {\n            ln(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : an(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function ln(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== cn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== cn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === cn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function sn(e) {\n    return (\n      (sn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      sn(e)\n    );\n  }\n  function fn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function pn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? fn(Object(n), !0).forEach(function (t) {\n            mn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : fn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function mn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== sn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== sn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === sn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function dn(e) {\n    return (\n      (function (e) {\n        if (Array.isArray(e)) return vn(e);\n      })(e) ||\n      (function (e) {\n        if (\n          (\"undefined\" != typeof Symbol && null != e[Symbol.iterator]) ||\n          null != e[\"@@iterator\"]\n        )\n          return Array.from(e);\n      })(e) ||\n      (function (e, t) {\n        if (e) {\n          if (\"string\" == typeof e) return vn(e, t);\n          var n = Object.prototype.toString.call(e).slice(8, -1);\n          return (\n            \"Object\" === n && e.constructor && (n = e.constructor.name),\n            \"Map\" === n || \"Set\" === n\n              ? Array.from(e)\n              : \"Arguments\" === n ||\n                /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n              ? vn(e, t)\n              : void 0\n          );\n        }\n      })(e) ||\n      (function () {\n        throw new TypeError(\n          \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n        );\n      })()\n    );\n  }\n  function vn(e, t) {\n    (null == t || t > e.length) && (t = e.length);\n    for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];\n    return r;\n  }\n  function hn(e) {\n    return Boolean(e.execute);\n  }\n  function yn(e) {\n    var t = e\n      .reduce(function (e, t) {\n        if (!hn(t)) return e.push(t), e;\n        var n = t.searchClient,\n          r = t.execute,\n          o = t.requesterId,\n          i = t.requests,\n          c = e.find(function (e) {\n            return (\n              hn(t) &&\n              hn(e) &&\n              e.searchClient === n &&\n              Boolean(o) &&\n              e.requesterId === o\n            );\n          });\n        if (c) {\n          var a;\n          (a = c.items).push.apply(a, dn(i));\n        } else {\n          var u = { execute: r, requesterId: o, items: i, searchClient: n };\n          e.push(u);\n        }\n        return e;\n      }, [])\n      .map(function (e) {\n        if (!hn(e)) return Promise.resolve(e);\n        var t = e,\n          n = t.execute,\n          r = t.items;\n        return n({ searchClient: t.searchClient, requests: r });\n      });\n    return Promise.all(t).then(function (e) {\n      return ot(e);\n    });\n  }\n  function _n(e) {\n    return (\n      (_n =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      _n(e)\n    );\n  }\n  var bn = [\"event\", \"nextState\", \"props\", \"query\", \"refresh\", \"store\"];\n  function gn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Sn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? gn(Object(n), !0).forEach(function (t) {\n            On(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : gn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function On(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== _n(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== _n(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === _n(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  var wn,\n    En,\n    jn,\n    Pn = null,\n    In =\n      ((wn = -1),\n      (En = -1),\n      (jn = void 0),\n      function (e) {\n        var t = ++wn;\n        return Promise.resolve(e).then(function (e) {\n          return jn && t < En ? jn : ((En = t), (jn = e), e);\n        });\n      });\n  function Dn(e) {\n    var t = e.event,\n      n = e.nextState,\n      r = void 0 === n ? {} : n,\n      o = e.props,\n      i = e.query,\n      c = e.refresh,\n      a = e.store,\n      u = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = (function (e, t) {\n            if (null == e) return {};\n            var n,\n              r,\n              o = {},\n              i = Object.keys(e);\n            for (r = 0; r < i.length; r++)\n              (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n            return o;\n          })(e, t);\n        if (Object.getOwnPropertySymbols) {\n          var i = Object.getOwnPropertySymbols(e);\n          for (r = 0; r < i.length; r++)\n            (n = i[r]),\n              t.indexOf(n) >= 0 ||\n                (Object.prototype.propertyIsEnumerable.call(e, n) &&\n                  (o[n] = e[n]));\n        }\n        return o;\n      })(e, bn);\n    Pn && o.environment.clearTimeout(Pn);\n    var l = u.setCollections,\n      s = u.setIsOpen,\n      f = u.setQuery,\n      p = u.setActiveItemId,\n      m = u.setStatus;\n    if ((f(i), p(o.defaultActiveItemId), !i && !1 === o.openOnFocus)) {\n      var d,\n        v = a.getState().collections.map(function (e) {\n          return Sn(Sn({}, e), {}, { items: [] });\n        });\n      m(\"idle\"),\n        l(v),\n        s(\n          null !== (d = r.isOpen) && void 0 !== d\n            ? d\n            : o.shouldPanelOpen({ state: a.getState() }),\n        );\n      var h = Lt(\n        In(v).then(function () {\n          return Promise.resolve();\n        }),\n      );\n      return a.pendingRequests.add(h);\n    }\n    m(\"loading\"),\n      (Pn = o.environment.setTimeout(function () {\n        m(\"stalled\");\n      }, o.stallThreshold));\n    var y = Lt(\n      In(\n        o\n          .getSources(Sn({ query: i, refresh: c, state: a.getState() }, u))\n          .then(function (e) {\n            return Promise.all(\n              e.map(function (e) {\n                return Promise.resolve(\n                  e.getItems(\n                    Sn({ query: i, refresh: c, state: a.getState() }, u),\n                  ),\n                ).then(function (t) {\n                  return (function (e, t, n) {\n                    if (((o = e), Boolean(null == o ? void 0 : o.execute))) {\n                      var r =\n                        \"algolia\" === e.requesterId\n                          ? Object.assign.apply(\n                              Object,\n                              [{}].concat(\n                                dn(\n                                  Object.keys(n.context).map(function (e) {\n                                    var t;\n                                    return null === (t = n.context[e]) ||\n                                      void 0 === t\n                                      ? void 0\n                                      : t.__algoliaSearchParameters;\n                                  }),\n                                ),\n                              ),\n                            )\n                          : {};\n                      return pn(\n                        pn({}, e),\n                        {},\n                        {\n                          requests: e.queries.map(function (n) {\n                            return {\n                              query:\n                                \"algolia\" === e.requesterId\n                                  ? pn(\n                                      pn({}, n),\n                                      {},\n                                      { params: pn(pn({}, r), n.params) },\n                                    )\n                                  : n,\n                              sourceId: t,\n                              transformResponse: e.transformResponse,\n                            };\n                          }),\n                        },\n                      );\n                    }\n                    var o;\n                    return { items: e, sourceId: t };\n                  })(t, e.sourceId, a.getState());\n                });\n              }),\n            )\n              .then(yn)\n              .then(function (t) {\n                return (function (e, t, n) {\n                  return t.map(function (t) {\n                    var r,\n                      o = e.filter(function (e) {\n                        return e.sourceId === t.sourceId;\n                      }),\n                      i = o.map(function (e) {\n                        return e.items;\n                      }),\n                      c = o[0].transformResponse,\n                      a = c\n                        ? c({\n                            results: (r = i),\n                            hits: r\n                              .map(function (e) {\n                                return e.hits;\n                              })\n                              .filter(Boolean),\n                            facetHits: r\n                              .map(function (e) {\n                                var t;\n                                return null === (t = e.facetHits) ||\n                                  void 0 === t\n                                  ? void 0\n                                  : t.map(function (e) {\n                                      return {\n                                        label: e.value,\n                                        count: e.count,\n                                        _highlightResult: {\n                                          label: { value: e.highlighted },\n                                        },\n                                      };\n                                    });\n                              })\n                              .filter(Boolean),\n                          })\n                        : i;\n                    return (\n                      t.onResolve({\n                        source: t,\n                        results: i,\n                        items: a,\n                        state: n.getState(),\n                      }),\n                      a.every(Boolean),\n                      'The `getItems` function from source \"'\n                        .concat(\n                          t.sourceId,\n                          '\" must return an array of items but returned ',\n                        )\n                        .concat(\n                          JSON.stringify(void 0),\n                          \".\\n\\nDid you forget to return items?\\n\\nSee: https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/sources/#param-getitems\",\n                        ),\n                      { source: t, items: a }\n                    );\n                  });\n                })(t, e, a);\n              })\n              .then(function (e) {\n                return (function (e) {\n                  var t = e.props,\n                    n = e.state,\n                    r = e.collections.reduce(function (e, t) {\n                      return un(\n                        un({}, e),\n                        {},\n                        ln(\n                          {},\n                          t.source.sourceId,\n                          un(\n                            un({}, t.source),\n                            {},\n                            {\n                              getItems: function () {\n                                return ot(t.items);\n                              },\n                            },\n                          ),\n                        ),\n                      );\n                    }, {}),\n                    o = t.plugins.reduce(\n                      function (e, t) {\n                        return t.reshape ? t.reshape(e) : e;\n                      },\n                      { sourcesBySourceId: r, state: n },\n                    ).sourcesBySourceId;\n                  return ot(\n                    t.reshape({\n                      sourcesBySourceId: o,\n                      sources: Object.values(o),\n                      state: n,\n                    }),\n                  )\n                    .filter(Boolean)\n                    .map(function (e) {\n                      return { source: e, items: e.getItems() };\n                    });\n                })({ collections: e, props: o, state: a.getState() });\n              });\n          }),\n      ),\n    )\n      .then(function (e) {\n        var n;\n        m(\"idle\"), l(e);\n        var f = o.shouldPanelOpen({ state: a.getState() });\n        s(\n          null !== (n = r.isOpen) && void 0 !== n\n            ? n\n            : (o.openOnFocus && !i && f) || f,\n        );\n        var p = Kt(a.getState());\n        if (null !== a.getState().activeItemId && p) {\n          var d = p.item,\n            v = p.itemInputValue,\n            h = p.itemUrl,\n            y = p.source;\n          y.onActive(\n            Sn(\n              {\n                event: t,\n                item: d,\n                itemInputValue: v,\n                itemUrl: h,\n                refresh: c,\n                source: y,\n                state: a.getState(),\n              },\n              u,\n            ),\n          );\n        }\n      })\n      .finally(function () {\n        m(\"idle\"), Pn && o.environment.clearTimeout(Pn);\n      });\n    return a.pendingRequests.add(y);\n  }\n  function kn(e) {\n    return (\n      (kn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      kn(e)\n    );\n  }\n  var Cn = [\"event\", \"props\", \"refresh\", \"store\"];\n  function An(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function xn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? An(Object(n), !0).forEach(function (t) {\n            Nn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : An(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Nn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== kn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== kn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === kn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Tn(e) {\n    return (\n      (Tn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Tn(e)\n    );\n  }\n  var Rn = [\"props\", \"refresh\", \"store\"],\n    qn = [\"inputElement\", \"formElement\", \"panelElement\"],\n    Ln = [\"inputElement\"],\n    Mn = [\"inputElement\", \"maxLength\"],\n    Hn = [\"sourceIndex\"],\n    Un = [\"sourceIndex\"],\n    Fn = [\"item\", \"source\", \"sourceIndex\"];\n  function Bn(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Vn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? Bn(Object(n), !0).forEach(function (t) {\n            Kn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : Bn(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Kn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Tn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Tn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Tn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Wn(e, t) {\n    if (null == e) return {};\n    var n,\n      r,\n      o = (function (e, t) {\n        if (null == e) return {};\n        var n,\n          r,\n          o = {},\n          i = Object.keys(e);\n        for (r = 0; r < i.length; r++)\n          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n        return o;\n      })(e, t);\n    if (Object.getOwnPropertySymbols) {\n      var i = Object.getOwnPropertySymbols(e);\n      for (r = 0; r < i.length; r++)\n        (n = i[r]),\n          t.indexOf(n) >= 0 ||\n            (Object.prototype.propertyIsEnumerable.call(e, n) && (o[n] = e[n]));\n    }\n    return o;\n  }\n  function zn(e) {\n    var t = e.props,\n      n = e.refresh,\n      r = e.store,\n      o = Wn(e, Rn),\n      i = function (e, t) {\n        return void 0 !== t ? \"\".concat(e, \"-\").concat(t) : e;\n      };\n    return {\n      getEnvironmentProps: function (e) {\n        var n = e.inputElement,\n          o = e.formElement,\n          i = e.panelElement;\n        function c(e) {\n          (!r.getState().isOpen && r.pendingRequests.isEmpty()) ||\n            e.target === n ||\n            (!1 ===\n              [o, i].some(function (t) {\n                return (n = t) === (r = e.target) || n.contains(r);\n                var n, r;\n              }) &&\n              (r.dispatch(\"blur\", null),\n              t.debug || r.pendingRequests.cancelAll()));\n        }\n        return Vn(\n          {\n            onTouchStart: c,\n            onMouseDown: c,\n            onTouchMove: function (e) {\n              !1 !== r.getState().isOpen &&\n                n === t.environment.document.activeElement &&\n                e.target !== n &&\n                n.blur();\n            },\n          },\n          Wn(e, qn),\n        );\n      },\n      getRootProps: function (e) {\n        return Vn(\n          {\n            role: \"combobox\",\n            \"aria-expanded\": r.getState().isOpen,\n            \"aria-haspopup\": \"listbox\",\n            \"aria-owns\": r.getState().isOpen\n              ? \"\".concat(t.id, \"-list\")\n              : void 0,\n            \"aria-labelledby\": \"\".concat(t.id, \"-label\"),\n          },\n          e,\n        );\n      },\n      getFormProps: function (e) {\n        return (\n          e.inputElement,\n          Vn(\n            {\n              action: \"\",\n              noValidate: !0,\n              role: \"search\",\n              onSubmit: function (i) {\n                var c;\n                i.preventDefault(),\n                  t.onSubmit(\n                    Vn({ event: i, refresh: n, state: r.getState() }, o),\n                  ),\n                  r.dispatch(\"submit\", null),\n                  null === (c = e.inputElement) || void 0 === c || c.blur();\n              },\n              onReset: function (i) {\n                var c;\n                i.preventDefault(),\n                  t.onReset(\n                    Vn({ event: i, refresh: n, state: r.getState() }, o),\n                  ),\n                  r.dispatch(\"reset\", null),\n                  null === (c = e.inputElement) || void 0 === c || c.focus();\n              },\n            },\n            Wn(e, Ln),\n          )\n        );\n      },\n      getLabelProps: function (e) {\n        var n = e || {},\n          r = n.sourceIndex,\n          o = Wn(n, Hn);\n        return Vn(\n          {\n            htmlFor: \"\".concat(i(t.id, r), \"-input\"),\n            id: \"\".concat(i(t.id, r), \"-label\"),\n          },\n          o,\n        );\n      },\n      getInputProps: function (e) {\n        var i;\n        function c(e) {\n          (t.openOnFocus || Boolean(r.getState().query)) &&\n            Dn(\n              Vn(\n                {\n                  event: e,\n                  props: t,\n                  query: r.getState().completion || r.getState().query,\n                  refresh: n,\n                  store: r,\n                },\n                o,\n              ),\n            ),\n            r.dispatch(\"focus\", null);\n        }\n        var a = e || {},\n          u = (a.inputElement, a.maxLength),\n          l = void 0 === u ? 512 : u,\n          s = Wn(a, Mn),\n          f = Kt(r.getState()),\n          p = (function (e) {\n            return Boolean(e && e.match(Wt));\n          })(\n            (null === (i = t.environment.navigator) || void 0 === i\n              ? void 0\n              : i.userAgent) || \"\",\n          ),\n          m = null != f && f.itemUrl && !p ? \"go\" : \"search\";\n        return Vn(\n          {\n            \"aria-autocomplete\": \"both\",\n            \"aria-activedescendant\":\n              r.getState().isOpen && null !== r.getState().activeItemId\n                ? \"\".concat(t.id, \"-item-\").concat(r.getState().activeItemId)\n                : void 0,\n            \"aria-controls\": r.getState().isOpen\n              ? \"\".concat(t.id, \"-list\")\n              : void 0,\n            \"aria-labelledby\": \"\".concat(t.id, \"-label\"),\n            value: r.getState().completion || r.getState().query,\n            id: \"\".concat(t.id, \"-input\"),\n            autoComplete: \"off\",\n            autoCorrect: \"off\",\n            autoCapitalize: \"off\",\n            enterKeyHint: m,\n            spellCheck: \"false\",\n            autoFocus: t.autoFocus,\n            placeholder: t.placeholder,\n            maxLength: l,\n            type: \"search\",\n            onChange: function (e) {\n              Dn(\n                Vn(\n                  {\n                    event: e,\n                    props: t,\n                    query: e.currentTarget.value.slice(0, l),\n                    refresh: n,\n                    store: r,\n                  },\n                  o,\n                ),\n              );\n            },\n            onKeyDown: function (e) {\n              !(function (e) {\n                var t = e.event,\n                  n = e.props,\n                  r = e.refresh,\n                  o = e.store,\n                  i = (function (e, t) {\n                    if (null == e) return {};\n                    var n,\n                      r,\n                      o = (function (e, t) {\n                        if (null == e) return {};\n                        var n,\n                          r,\n                          o = {},\n                          i = Object.keys(e);\n                        for (r = 0; r < i.length; r++)\n                          (n = i[r]), t.indexOf(n) >= 0 || (o[n] = e[n]);\n                        return o;\n                      })(e, t);\n                    if (Object.getOwnPropertySymbols) {\n                      var i = Object.getOwnPropertySymbols(e);\n                      for (r = 0; r < i.length; r++)\n                        (n = i[r]),\n                          t.indexOf(n) >= 0 ||\n                            (Object.prototype.propertyIsEnumerable.call(e, n) &&\n                              (o[n] = e[n]));\n                    }\n                    return o;\n                  })(e, Cn);\n                if (\"ArrowUp\" === t.key || \"ArrowDown\" === t.key) {\n                  var c = function () {\n                      var e = n.environment.document.getElementById(\n                        \"\"\n                          .concat(n.id, \"-item-\")\n                          .concat(o.getState().activeItemId),\n                      );\n                      e &&\n                        (e.scrollIntoViewIfNeeded\n                          ? e.scrollIntoViewIfNeeded(!1)\n                          : e.scrollIntoView(!1));\n                    },\n                    a = function () {\n                      var e = Kt(o.getState());\n                      if (null !== o.getState().activeItemId && e) {\n                        var n = e.item,\n                          c = e.itemInputValue,\n                          a = e.itemUrl,\n                          u = e.source;\n                        u.onActive(\n                          xn(\n                            {\n                              event: t,\n                              item: n,\n                              itemInputValue: c,\n                              itemUrl: a,\n                              refresh: r,\n                              source: u,\n                              state: o.getState(),\n                            },\n                            i,\n                          ),\n                        );\n                      }\n                    };\n                  t.preventDefault(),\n                    !1 === o.getState().isOpen &&\n                    (n.openOnFocus || Boolean(o.getState().query))\n                      ? Dn(\n                          xn(\n                            {\n                              event: t,\n                              props: n,\n                              query: o.getState().query,\n                              refresh: r,\n                              store: o,\n                            },\n                            i,\n                          ),\n                        ).then(function () {\n                          o.dispatch(t.key, {\n                            nextActiveItemId: n.defaultActiveItemId,\n                          }),\n                            a(),\n                            setTimeout(c, 0);\n                        })\n                      : (o.dispatch(t.key, {}), a(), c());\n                } else if (\"Escape\" === t.key)\n                  t.preventDefault(),\n                    o.dispatch(t.key, null),\n                    o.pendingRequests.cancelAll();\n                else if (\"Tab\" === t.key)\n                  o.dispatch(\"blur\", null), o.pendingRequests.cancelAll();\n                else if (\"Enter\" === t.key) {\n                  if (\n                    null === o.getState().activeItemId ||\n                    o.getState().collections.every(function (e) {\n                      return 0 === e.items.length;\n                    })\n                  )\n                    return void (n.debug || o.pendingRequests.cancelAll());\n                  t.preventDefault();\n                  var u = Kt(o.getState()),\n                    l = u.item,\n                    s = u.itemInputValue,\n                    f = u.itemUrl,\n                    p = u.source;\n                  if (t.metaKey || t.ctrlKey)\n                    void 0 !== f &&\n                      (p.onSelect(\n                        xn(\n                          {\n                            event: t,\n                            item: l,\n                            itemInputValue: s,\n                            itemUrl: f,\n                            refresh: r,\n                            source: p,\n                            state: o.getState(),\n                          },\n                          i,\n                        ),\n                      ),\n                      n.navigator.navigateNewTab({\n                        itemUrl: f,\n                        item: l,\n                        state: o.getState(),\n                      }));\n                  else if (t.shiftKey)\n                    void 0 !== f &&\n                      (p.onSelect(\n                        xn(\n                          {\n                            event: t,\n                            item: l,\n                            itemInputValue: s,\n                            itemUrl: f,\n                            refresh: r,\n                            source: p,\n                            state: o.getState(),\n                          },\n                          i,\n                        ),\n                      ),\n                      n.navigator.navigateNewWindow({\n                        itemUrl: f,\n                        item: l,\n                        state: o.getState(),\n                      }));\n                  else if (t.altKey);\n                  else {\n                    if (void 0 !== f)\n                      return (\n                        p.onSelect(\n                          xn(\n                            {\n                              event: t,\n                              item: l,\n                              itemInputValue: s,\n                              itemUrl: f,\n                              refresh: r,\n                              source: p,\n                              state: o.getState(),\n                            },\n                            i,\n                          ),\n                        ),\n                        void n.navigator.navigate({\n                          itemUrl: f,\n                          item: l,\n                          state: o.getState(),\n                        })\n                      );\n                    Dn(\n                      xn(\n                        {\n                          event: t,\n                          nextState: { isOpen: !1 },\n                          props: n,\n                          query: s,\n                          refresh: r,\n                          store: o,\n                        },\n                        i,\n                      ),\n                    ).then(function () {\n                      p.onSelect(\n                        xn(\n                          {\n                            event: t,\n                            item: l,\n                            itemInputValue: s,\n                            itemUrl: f,\n                            refresh: r,\n                            source: p,\n                            state: o.getState(),\n                          },\n                          i,\n                        ),\n                      );\n                    });\n                  }\n                }\n              })(Vn({ event: e, props: t, refresh: n, store: r }, o));\n            },\n            onFocus: c,\n            onBlur: lt,\n            onClick: function (n) {\n              e.inputElement !== t.environment.document.activeElement ||\n                r.getState().isOpen ||\n                c(n);\n            },\n          },\n          s,\n        );\n      },\n      getPanelProps: function (e) {\n        return Vn(\n          {\n            onMouseDown: function (e) {\n              e.preventDefault();\n            },\n            onMouseLeave: function () {\n              r.dispatch(\"mouseleave\", null);\n            },\n          },\n          e,\n        );\n      },\n      getListProps: function (e) {\n        var n = e || {},\n          r = n.sourceIndex,\n          o = Wn(n, Un);\n        return Vn(\n          {\n            role: \"listbox\",\n            \"aria-labelledby\": \"\".concat(i(t.id, r), \"-label\"),\n            id: \"\".concat(i(t.id, r), \"-list\"),\n          },\n          o,\n        );\n      },\n      getItemProps: function (e) {\n        var c = e.item,\n          a = e.source,\n          u = e.sourceIndex,\n          l = Wn(e, Fn);\n        return Vn(\n          {\n            id: \"\".concat(i(t.id, u), \"-item-\").concat(c.__autocomplete_id),\n            role: \"option\",\n            \"aria-selected\": r.getState().activeItemId === c.__autocomplete_id,\n            onMouseMove: function (e) {\n              if (c.__autocomplete_id !== r.getState().activeItemId) {\n                r.dispatch(\"mousemove\", c.__autocomplete_id);\n                var t = Kt(r.getState());\n                if (null !== r.getState().activeItemId && t) {\n                  var i = t.item,\n                    a = t.itemInputValue,\n                    u = t.itemUrl,\n                    l = t.source;\n                  l.onActive(\n                    Vn(\n                      {\n                        event: e,\n                        item: i,\n                        itemInputValue: a,\n                        itemUrl: u,\n                        refresh: n,\n                        source: l,\n                        state: r.getState(),\n                      },\n                      o,\n                    ),\n                  );\n                }\n              }\n            },\n            onMouseDown: function (e) {\n              e.preventDefault();\n            },\n            onClick: function (e) {\n              var i = a.getItemInputValue({ item: c, state: r.getState() }),\n                u = a.getItemUrl({ item: c, state: r.getState() });\n              (u\n                ? Promise.resolve()\n                : Dn(\n                    Vn(\n                      {\n                        event: e,\n                        nextState: { isOpen: !1 },\n                        props: t,\n                        query: i,\n                        refresh: n,\n                        store: r,\n                      },\n                      o,\n                    ),\n                  )\n              ).then(function () {\n                a.onSelect(\n                  Vn(\n                    {\n                      event: e,\n                      item: c,\n                      itemInputValue: i,\n                      itemUrl: u,\n                      refresh: n,\n                      source: a,\n                      state: r.getState(),\n                    },\n                    o,\n                  ),\n                );\n              });\n            },\n          },\n          l,\n        );\n      },\n    };\n  }\n  function Jn(e) {\n    return (\n      (Jn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Jn(e)\n    );\n  }\n  function $n(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function Zn(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? $n(Object(n), !0).forEach(function (t) {\n            Qn(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : $n(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function Qn(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Jn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Jn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Jn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function Yn(e) {\n    var t,\n      n,\n      r,\n      o,\n      i = e.plugins,\n      c = e.options,\n      a =\n        null ===\n          (t = ((null === (n = c.__autocomplete_metadata) || void 0 === n\n            ? void 0\n            : n.userAgents) || [])[0]) || void 0 === t\n          ? void 0\n          : t.segment,\n      u = a\n        ? Qn(\n            {},\n            a,\n            Object.keys(\n              (null === (r = c.__autocomplete_metadata) || void 0 === r\n                ? void 0\n                : r.options) || {},\n            ),\n          )\n        : {};\n    return {\n      plugins: i.map(function (e) {\n        return {\n          name: e.name,\n          options: Object.keys(e.__autocomplete_pluginOptions || []),\n        };\n      }),\n      options: Zn({ \"autocomplete-core\": Object.keys(c) }, u),\n      ua: st.concat(\n        (null === (o = c.__autocomplete_metadata) || void 0 === o\n          ? void 0\n          : o.userAgents) || [],\n      ),\n    };\n  }\n  function Gn(e) {\n    var t,\n      n = e.state;\n    return !1 === n.isOpen || null === n.activeItemId\n      ? null\n      : (null === (t = Kt(n)) || void 0 === t ? void 0 : t.itemInputValue) ||\n          null;\n  }\n  function Xn(e) {\n    return (\n      (Xn =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      Xn(e)\n    );\n  }\n  function er(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function tr(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? er(Object(n), !0).forEach(function (t) {\n            nr(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : er(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function nr(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== Xn(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== Xn(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === Xn(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  var rr = function (e, t) {\n    switch (t.type) {\n      case \"setActiveItemId\":\n      case \"mousemove\":\n        return tr(tr({}, e), {}, { activeItemId: t.payload });\n      case \"setQuery\":\n        return tr(tr({}, e), {}, { query: t.payload, completion: null });\n      case \"setCollections\":\n        return tr(tr({}, e), {}, { collections: t.payload });\n      case \"setIsOpen\":\n        return tr(tr({}, e), {}, { isOpen: t.payload });\n      case \"setStatus\":\n        return tr(tr({}, e), {}, { status: t.payload });\n      case \"setContext\":\n        return tr(tr({}, e), {}, { context: tr(tr({}, e.context), t.payload) });\n      case \"ArrowDown\":\n        var n = tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId: t.payload.hasOwnProperty(\"nextActiveItemId\")\n              ? t.payload.nextActiveItemId\n              : Ht(1, e.activeItemId, ct(e), t.props.defaultActiveItemId),\n          },\n        );\n        return tr(tr({}, n), {}, { completion: Gn({ state: n }) });\n      case \"ArrowUp\":\n        var r = tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId: Ht(\n              -1,\n              e.activeItemId,\n              ct(e),\n              t.props.defaultActiveItemId,\n            ),\n          },\n        );\n        return tr(tr({}, r), {}, { completion: Gn({ state: r }) });\n      case \"Escape\":\n        return e.isOpen\n          ? tr(\n              tr({}, e),\n              {},\n              { activeItemId: null, isOpen: !1, completion: null },\n            )\n          : tr(\n              tr({}, e),\n              {},\n              {\n                activeItemId: null,\n                query: \"\",\n                status: \"idle\",\n                collections: [],\n              },\n            );\n      case \"submit\":\n        return tr(\n          tr({}, e),\n          {},\n          { activeItemId: null, isOpen: !1, status: \"idle\" },\n        );\n      case \"reset\":\n        return tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId:\n              !0 === t.props.openOnFocus ? t.props.defaultActiveItemId : null,\n            status: \"idle\",\n            query: \"\",\n          },\n        );\n      case \"focus\":\n        return tr(\n          tr({}, e),\n          {},\n          {\n            activeItemId: t.props.defaultActiveItemId,\n            isOpen:\n              (t.props.openOnFocus || Boolean(e.query)) &&\n              t.props.shouldPanelOpen({ state: e }),\n          },\n        );\n      case \"blur\":\n        return t.props.debug\n          ? e\n          : tr(tr({}, e), {}, { isOpen: !1, activeItemId: null });\n      case \"mouseleave\":\n        return tr(tr({}, e), {}, { activeItemId: t.props.defaultActiveItemId });\n      default:\n        return (\n          \"The reducer action \".concat(\n            JSON.stringify(t.type),\n            \" is not supported.\",\n          ),\n          e\n        );\n    }\n  };\n  function or(e) {\n    return (\n      (or =\n        \"function\" == typeof Symbol && \"symbol\" == n(Symbol.iterator)\n          ? function (e) {\n              return n(e);\n            }\n          : function (e) {\n              return e &&\n                \"function\" == typeof Symbol &&\n                e.constructor === Symbol &&\n                e !== Symbol.prototype\n                ? \"symbol\"\n                : n(e);\n            }),\n      or(e)\n    );\n  }\n  function ir(e, t) {\n    var n = Object.keys(e);\n    if (Object.getOwnPropertySymbols) {\n      var r = Object.getOwnPropertySymbols(e);\n      t &&\n        (r = r.filter(function (t) {\n          return Object.getOwnPropertyDescriptor(e, t).enumerable;\n        })),\n        n.push.apply(n, r);\n    }\n    return n;\n  }\n  function cr(e) {\n    for (var t = 1; t < arguments.length; t++) {\n      var n = null != arguments[t] ? arguments[t] : {};\n      t % 2\n        ? ir(Object(n), !0).forEach(function (t) {\n            ar(e, t, n[t]);\n          })\n        : Object.getOwnPropertyDescriptors\n        ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n))\n        : ir(Object(n)).forEach(function (t) {\n            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n          });\n    }\n    return e;\n  }\n  function ar(e, t, n) {\n    return (\n      (t = (function (e) {\n        var t = (function (e, t) {\n          if (\"object\" !== or(e) || null === e) return e;\n          var n = e[Symbol.toPrimitive];\n          if (void 0 !== n) {\n            var r = n.call(e, t);\n            if (\"object\" !== or(r)) return r;\n            throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n          }\n          return String(e);\n        })(e, \"string\");\n        return \"symbol\" === or(t) ? t : String(t);\n      })(t)) in e\n        ? Object.defineProperty(e, t, {\n            value: n,\n            enumerable: !0,\n            configurable: !0,\n            writable: !0,\n          })\n        : (e[t] = n),\n      e\n    );\n  }\n  function ur(e) {\n    var t = [],\n      n = on(e, t),\n      r = (function (e, t, n) {\n        var r,\n          o = t.initialState;\n        return {\n          getState: function () {\n            return o;\n          },\n          dispatch: function (r, i) {\n            var c = (function (e) {\n              for (var t = 1; t < arguments.length; t++) {\n                var n = null != arguments[t] ? arguments[t] : {};\n                t % 2\n                  ? Jt(Object(n), !0).forEach(function (t) {\n                      $t(e, t, n[t]);\n                    })\n                  : Object.getOwnPropertyDescriptors\n                  ? Object.defineProperties(\n                      e,\n                      Object.getOwnPropertyDescriptors(n),\n                    )\n                  : Jt(Object(n)).forEach(function (t) {\n                      Object.defineProperty(\n                        e,\n                        t,\n                        Object.getOwnPropertyDescriptor(n, t),\n                      );\n                    });\n              }\n              return e;\n            })({}, o);\n            (o = e(o, { type: r, props: t, payload: i })),\n              n({ state: o, prevState: c });\n          },\n          pendingRequests:\n            ((r = []),\n            {\n              add: function (e) {\n                return (\n                  r.push(e),\n                  e.finally(function () {\n                    r = r.filter(function (t) {\n                      return t !== e;\n                    });\n                  })\n                );\n              },\n              cancelAll: function () {\n                r.forEach(function (e) {\n                  return e.cancel();\n                });\n              },\n              isEmpty: function () {\n                return 0 === r.length;\n              },\n            }),\n        };\n      })(rr, n, function (e) {\n        var t = e.prevState,\n          r = e.state;\n        n.onStateChange(\n          cr({ prevState: t, state: r, refresh: c, navigator: n.navigator }, o),\n        );\n      }),\n      o = (function (e) {\n        var t = e.store;\n        return {\n          setActiveItemId: function (e) {\n            t.dispatch(\"setActiveItemId\", e);\n          },\n          setQuery: function (e) {\n            t.dispatch(\"setQuery\", e);\n          },\n          setCollections: function (e) {\n            var n = 0,\n              r = e.map(function (e) {\n                return Yt(\n                  Yt({}, e),\n                  {},\n                  {\n                    items: ot(e.items).map(function (e) {\n                      return Yt(Yt({}, e), {}, { __autocomplete_id: n++ });\n                    }),\n                  },\n                );\n              });\n            t.dispatch(\"setCollections\", r);\n          },\n          setIsOpen: function (e) {\n            t.dispatch(\"setIsOpen\", e);\n          },\n          setStatus: function (e) {\n            t.dispatch(\"setStatus\", e);\n          },\n          setContext: function (e) {\n            t.dispatch(\"setContext\", e);\n          },\n        };\n      })({ store: r }),\n      i = zn(cr({ props: n, refresh: c, store: r, navigator: n.navigator }, o));\n    function c() {\n      return Dn(\n        cr(\n          {\n            event: new Event(\"input\"),\n            nextState: { isOpen: r.getState().isOpen },\n            props: n,\n            navigator: n.navigator,\n            query: r.getState().query,\n            refresh: c,\n            store: r,\n          },\n          o,\n        ),\n      );\n    }\n    if (\n      e.insights &&\n      !n.plugins.some(function (e) {\n        return \"aa.algoliaInsightsPlugin\" === e.name;\n      })\n    ) {\n      var a = \"boolean\" == typeof e.insights ? {} : e.insights;\n      n.plugins.push(Rt(a));\n    }\n    return (\n      n.plugins.forEach(function (e) {\n        var r;\n        return null === (r = e.subscribe) || void 0 === r\n          ? void 0\n          : r.call(\n              e,\n              cr(\n                cr({}, o),\n                {},\n                {\n                  navigator: n.navigator,\n                  refresh: c,\n                  onSelect: function (e) {\n                    t.push({ onSelect: e });\n                  },\n                  onActive: function (e) {\n                    t.push({ onActive: e });\n                  },\n                  onResolve: function (e) {\n                    t.push({ onResolve: e });\n                  },\n                },\n              ),\n            );\n      }),\n      (function (e) {\n        var t,\n          n,\n          r = e.metadata,\n          o = e.environment;\n        if (\n          null === (t = o.navigator) ||\n          void 0 === t ||\n          null === (n = t.userAgent) ||\n          void 0 === n\n            ? void 0\n            : n.includes(\"Algolia Crawler\")\n        ) {\n          var i = o.document.createElement(\"meta\"),\n            c = o.document.querySelector(\"head\");\n          (i.name = \"algolia:metadata\"),\n            setTimeout(function () {\n              (i.content = JSON.stringify(r)), c.appendChild(i);\n            }, 0);\n        }\n      })({\n        metadata: Yn({ plugins: n.plugins, options: e }),\n        environment: n.environment,\n      }),\n      cr(cr({ refresh: c, navigator: n.navigator }, i), o)\n    );\n  }\n  function lr(e) {\n    var t = e.translations,\n      n = (void 0 === t ? {} : t).searchByText,\n      r = void 0 === n ? \"Search by\" : n;\n    return Be.createElement(\n      \"a\",\n      {\n        href: \"https://www.algolia.com/ref/docsearch/?utm_source=\".concat(\n          window.location.hostname,\n          \"&utm_medium=referral&utm_content=powered_by&utm_campaign=docsearch\",\n        ),\n        target: \"_blank\",\n        rel: \"noopener noreferrer\",\n      },\n      Be.createElement(\"span\", { className: \"DocSearch-Label\" }, r),\n      Be.createElement(\n        \"svg\",\n        {\n          width: \"77\",\n          height: \"19\",\n          \"aria-label\": \"Algolia\",\n          role: \"img\",\n          id: \"Layer_1\",\n          xmlns: \"http://www.w3.org/2000/svg\",\n          viewBox: \"0 0 2196.2 500\",\n        },\n        Be.createElement(\n          \"defs\",\n          null,\n          Be.createElement(\n            \"style\",\n            null,\n            \".cls-1,.cls-2{fill:#003dff;}.cls-2{fill-rule:evenodd;}\",\n          ),\n        ),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z\",\n        }),\n        Be.createElement(\"rect\", {\n          className: \"cls-1\",\n          x: \"1845.88\",\n          y: \"104.73\",\n          width: \"62.58\",\n          height: \"277.9\",\n          rx: \"5.9\",\n          ry: \"5.9\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-2\",\n          d: \"M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z\",\n        }),\n        Be.createElement(\"path\", {\n          className: \"cls-1\",\n          d: \"M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z\",\n        }),\n      ),\n    );\n  }\n  function sr(e) {\n    return Be.createElement(\n      \"svg\",\n      { width: \"15\", height: \"15\", \"aria-label\": e.ariaLabel, role: \"img\" },\n      Be.createElement(\n        \"g\",\n        {\n          fill: \"none\",\n          stroke: \"currentColor\",\n          strokeLinecap: \"round\",\n          strokeLinejoin: \"round\",\n          strokeWidth: \"1.2\",\n        },\n        e.children,\n      ),\n    );\n  }\n  function fr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = n.selectText,\n      o = void 0 === r ? \"to select\" : r,\n      i = n.selectKeyAriaLabel,\n      c = void 0 === i ? \"Enter key\" : i,\n      a = n.navigateText,\n      u = void 0 === a ? \"to navigate\" : a,\n      l = n.navigateUpKeyAriaLabel,\n      s = void 0 === l ? \"Arrow up\" : l,\n      f = n.navigateDownKeyAriaLabel,\n      p = void 0 === f ? \"Arrow down\" : f,\n      m = n.closeText,\n      d = void 0 === m ? \"to close\" : m,\n      v = n.closeKeyAriaLabel,\n      h = void 0 === v ? \"Escape key\" : v,\n      y = n.searchByText,\n      _ = void 0 === y ? \"Search by\" : y;\n    return Be.createElement(\n      Be.Fragment,\n      null,\n      Be.createElement(\n        \"div\",\n        { className: \"DocSearch-Logo\" },\n        Be.createElement(lr, { translations: { searchByText: _ } }),\n      ),\n      Be.createElement(\n        \"ul\",\n        { className: \"DocSearch-Commands\" },\n        Be.createElement(\n          \"li\",\n          null,\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: c },\n              Be.createElement(\"path\", {\n                d: \"M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3\",\n              }),\n            ),\n          ),\n          Be.createElement(\"span\", { className: \"DocSearch-Label\" }, o),\n        ),\n        Be.createElement(\n          \"li\",\n          null,\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: p },\n              Be.createElement(\"path\", { d: \"M7.5 3.5v8M10.5 8.5l-3 3-3-3\" }),\n            ),\n          ),\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: s },\n              Be.createElement(\"path\", { d: \"M7.5 11.5v-8M10.5 6.5l-3-3-3 3\" }),\n            ),\n          ),\n          Be.createElement(\"span\", { className: \"DocSearch-Label\" }, u),\n        ),\n        Be.createElement(\n          \"li\",\n          null,\n          Be.createElement(\n            \"kbd\",\n            { className: \"DocSearch-Commands-Key\" },\n            Be.createElement(\n              sr,\n              { ariaLabel: h },\n              Be.createElement(\"path\", {\n                d: \"M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956\",\n              }),\n            ),\n          ),\n          Be.createElement(\"span\", { className: \"DocSearch-Label\" }, d),\n        ),\n      ),\n    );\n  }\n  function pr(e) {\n    var t = e.hit,\n      n = e.children;\n    return Be.createElement(\"a\", { href: t.url }, n);\n  }\n  function mr() {\n    return Be.createElement(\n      \"svg\",\n      { viewBox: \"0 0 38 38\", stroke: \"currentColor\", strokeOpacity: \".5\" },\n      Be.createElement(\n        \"g\",\n        { fill: \"none\", fillRule: \"evenodd\" },\n        Be.createElement(\n          \"g\",\n          { transform: \"translate(1 1)\", strokeWidth: \"2\" },\n          Be.createElement(\"circle\", {\n            strokeOpacity: \".3\",\n            cx: \"18\",\n            cy: \"18\",\n            r: \"18\",\n          }),\n          Be.createElement(\n            \"path\",\n            { d: \"M36 18c0-9.94-8.06-18-18-18\" },\n            Be.createElement(\"animateTransform\", {\n              attributeName: \"transform\",\n              type: \"rotate\",\n              from: \"0 18 18\",\n              to: \"360 18 18\",\n              dur: \"1s\",\n              repeatCount: \"indefinite\",\n            }),\n          ),\n        ),\n      ),\n    );\n  }\n  function dr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\n        \"g\",\n        {\n          stroke: \"currentColor\",\n          fill: \"none\",\n          fillRule: \"evenodd\",\n          strokeLinecap: \"round\",\n          strokeLinejoin: \"round\",\n        },\n        Be.createElement(\"path\", {\n          d: \"M3.18 6.6a8.23 8.23 0 1112.93 9.94h0a8.23 8.23 0 01-11.63 0\",\n        }),\n        Be.createElement(\"path\", {\n          d: \"M6.44 7.25H2.55V3.36M10.45 6v5.6M10.45 11.6L13 13\",\n        }),\n      ),\n    );\n  }\n  function vr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function hr() {\n    return Be.createElement(\n      \"svg\",\n      {\n        className: \"DocSearch-Hit-Select-Icon\",\n        width: \"20\",\n        height: \"20\",\n        viewBox: \"0 0 20 20\",\n      },\n      Be.createElement(\n        \"g\",\n        {\n          stroke: \"currentColor\",\n          fill: \"none\",\n          fillRule: \"evenodd\",\n          strokeLinecap: \"round\",\n          strokeLinejoin: \"round\",\n        },\n        Be.createElement(\"path\", { d: \"M18 3v4c0 2-2 4-4 4H2\" }),\n        Be.createElement(\"path\", { d: \"M8 17l-6-6 6-6\" }),\n      ),\n    );\n  }\n  var yr = function () {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M17 6v12c0 .52-.2 1-1 1H4c-.7 0-1-.33-1-1V2c0-.55.42-1 1-1h8l5 5zM14 8h-3.13c-.51 0-.87-.34-.87-.87V4\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  };\n  function _r(e) {\n    switch (e.type) {\n      case \"lvl1\":\n        return Be.createElement(yr, null);\n      case \"content\":\n        return Be.createElement(gr, null);\n      default:\n        return Be.createElement(br, null);\n    }\n  }\n  function br() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M13 13h4-4V8H7v5h6v4-4H7V8H3h4V3v5h6V3v5h4-4v5zm-6 0v4-4H3h4z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function gr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M17 5H3h14zm0 5H3h14zm0 5H3h14z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function Sr() {\n    return Be.createElement(\n      \"svg\",\n      { width: \"20\", height: \"20\", viewBox: \"0 0 20 20\" },\n      Be.createElement(\"path\", {\n        d: \"M10 14.2L5 17l1-5.6-4-4 5.5-.7 2.5-5 2.5 5 5.6.8-4 4 .9 5.5z\",\n        stroke: \"currentColor\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        strokeLinejoin: \"round\",\n      }),\n    );\n  }\n  function Or() {\n    return Be.createElement(\n      \"svg\",\n      {\n        width: \"40\",\n        height: \"40\",\n        viewBox: \"0 0 20 20\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        stroke: \"currentColor\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      },\n      Be.createElement(\"path\", {\n        d: \"M19 4.8a16 16 0 00-2-1.2m-3.3-1.2A16 16 0 001.1 4.7M16.7 8a12 12 0 00-2.8-1.4M10 6a12 12 0 00-6.7 2M12.3 14.7a4 4 0 00-4.5 0M14.5 11.4A8 8 0 0010 10M3 16L18 2M10 18h0\",\n      }),\n    );\n  }\n  function wr() {\n    return Be.createElement(\n      \"svg\",\n      {\n        width: \"40\",\n        height: \"40\",\n        viewBox: \"0 0 20 20\",\n        fill: \"none\",\n        fillRule: \"evenodd\",\n        stroke: \"currentColor\",\n        strokeLinecap: \"round\",\n        strokeLinejoin: \"round\",\n      },\n      Be.createElement(\"path\", {\n        d: \"M15.5 4.8c2 3 1.7 7-1 9.7h0l4.3 4.3-4.3-4.3a7.8 7.8 0 01-9.8 1m-2.2-2.2A7.8 7.8 0 0113.2 2.4M2 18L18 2\",\n      }),\n    );\n  }\n  function Er(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = n.titleText,\n      o = void 0 === r ? \"Unable to fetch results\" : r,\n      i = n.helpText,\n      c = void 0 === i ? \"You might want to check your network connection.\" : i;\n    return Be.createElement(\n      \"div\",\n      { className: \"DocSearch-ErrorScreen\" },\n      Be.createElement(\n        \"div\",\n        { className: \"DocSearch-Screen-Icon\" },\n        Be.createElement(Or, null),\n      ),\n      Be.createElement(\"p\", { className: \"DocSearch-Title\" }, o),\n      Be.createElement(\"p\", { className: \"DocSearch-Help\" }, c),\n    );\n  }\n  var jr = [\"translations\"];\n  function Pr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = $e(e, jr),\n      o = n.noResultsText,\n      i = void 0 === o ? \"No results for\" : o,\n      c = n.suggestedQueryText,\n      a = void 0 === c ? \"Try searching for\" : c,\n      u = n.reportMissingResultsText,\n      l = void 0 === u ? \"Believe this query should return results?\" : u,\n      s = n.reportMissingResultsLinkText,\n      f = void 0 === s ? \"Let us know.\" : s,\n      p = r.state.context.searchSuggestions;\n    return Be.createElement(\n      \"div\",\n      { className: \"DocSearch-NoResults\" },\n      Be.createElement(\n        \"div\",\n        { className: \"DocSearch-Screen-Icon\" },\n        Be.createElement(wr, null),\n      ),\n      Be.createElement(\n        \"p\",\n        { className: \"DocSearch-Title\" },\n        i,\n        ' \"',\n        Be.createElement(\"strong\", null, r.state.query),\n        '\"',\n      ),\n      p &&\n        p.length > 0 &&\n        Be.createElement(\n          \"div\",\n          { className: \"DocSearch-NoResults-Prefill-List\" },\n          Be.createElement(\"p\", { className: \"DocSearch-Help\" }, a, \":\"),\n          Be.createElement(\n            \"ul\",\n            null,\n            p.slice(0, 3).reduce(function (e, t) {\n              return [].concat(\n                (function (e) {\n                  return (\n                    (function (e) {\n                      if (Array.isArray(e)) return Ye(e);\n                    })(e) ||\n                    (function (e) {\n                      if (\n                        (\"undefined\" != typeof Symbol &&\n                          null != e[Symbol.iterator]) ||\n                        null != e[\"@@iterator\"]\n                      )\n                        return Array.from(e);\n                    })(e) ||\n                    Qe(e) ||\n                    (function () {\n                      throw new TypeError(\n                        \"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\",\n                      );\n                    })()\n                  );\n                })(e),\n                [\n                  Be.createElement(\n                    \"li\",\n                    { key: t },\n                    Be.createElement(\n                      \"button\",\n                      {\n                        className: \"DocSearch-Prefill\",\n                        key: t,\n                        type: \"button\",\n                        onClick: function () {\n                          r.setQuery(t.toLowerCase() + \" \"),\n                            r.refresh(),\n                            r.inputRef.current.focus();\n                        },\n                      },\n                      t,\n                    ),\n                  ),\n                ],\n              );\n            }, []),\n          ),\n        ),\n      r.getMissingResultsUrl &&\n        Be.createElement(\n          \"p\",\n          { className: \"DocSearch-Help\" },\n          \"\".concat(l, \" \"),\n          Be.createElement(\n            \"a\",\n            {\n              href: r.getMissingResultsUrl({ query: r.state.query }),\n              target: \"_blank\",\n              rel: \"noopener noreferrer\",\n            },\n            f,\n          ),\n        ),\n    );\n  }\n  var Ir = [\"hit\", \"attribute\", \"tagName\"];\n  function Dr(e, t) {\n    return t.split(\".\").reduce(function (e, t) {\n      return null != e && e[t] ? e[t] : null;\n    }, e);\n  }\n  function kr(e) {\n    var t = e.hit,\n      n = e.attribute,\n      r = e.tagName;\n    return g(\n      void 0 === r ? \"span\" : r,\n      We(\n        We({}, $e(e, Ir)),\n        {},\n        {\n          dangerouslySetInnerHTML: {\n            __html: Dr(t, \"_snippetResult.\".concat(n, \".value\")) || Dr(t, n),\n          },\n        },\n      ),\n    );\n  }\n  function Cr(e) {\n    return e.collection && 0 !== e.collection.items.length\n      ? Be.createElement(\n          \"section\",\n          { className: \"DocSearch-Hits\" },\n          Be.createElement(\n            \"div\",\n            { className: \"DocSearch-Hit-source\" },\n            e.title,\n          ),\n          Be.createElement(\n            \"ul\",\n            e.getListProps(),\n            e.collection.items.map(function (t, n) {\n              return Be.createElement(\n                Ar,\n                Je(\n                  { key: [e.title, t.objectID].join(\":\"), item: t, index: n },\n                  e,\n                ),\n              );\n            }),\n          ),\n        )\n      : null;\n  }\n  function Ar(e) {\n    var t = e.item,\n      n = e.index,\n      r = e.renderIcon,\n      o = e.renderAction,\n      i = e.getItemProps,\n      c = e.onItemClick,\n      a = e.collection,\n      u = e.hitComponent,\n      l = Ze(Be.useState(!1), 2),\n      s = l[0],\n      f = l[1],\n      p = Ze(Be.useState(!1), 2),\n      m = p[0],\n      d = p[1],\n      v = Be.useRef(null),\n      h = u;\n    return Be.createElement(\n      \"li\",\n      Je(\n        {\n          className: [\n            \"DocSearch-Hit\",\n            t.__docsearch_parent && \"DocSearch-Hit--Child\",\n            s && \"DocSearch-Hit--deleting\",\n            m && \"DocSearch-Hit--favoriting\",\n          ]\n            .filter(Boolean)\n            .join(\" \"),\n          onTransitionEnd: function () {\n            v.current && v.current();\n          },\n        },\n        i({\n          item: t,\n          source: a.source,\n          onClick: function (e) {\n            c(t, e);\n          },\n        }),\n      ),\n      Be.createElement(\n        h,\n        { hit: t },\n        Be.createElement(\n          \"div\",\n          { className: \"DocSearch-Hit-Container\" },\n          r({ item: t, index: n }),\n          t.hierarchy[t.type] &&\n            \"lvl1\" === t.type &&\n            Be.createElement(\n              \"div\",\n              { className: \"DocSearch-Hit-content-wrapper\" },\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-title\",\n                hit: t,\n                attribute: \"hierarchy.lvl1\",\n              }),\n              t.content &&\n                Be.createElement(kr, {\n                  className: \"DocSearch-Hit-path\",\n                  hit: t,\n                  attribute: \"content\",\n                }),\n            ),\n          t.hierarchy[t.type] &&\n            (\"lvl2\" === t.type ||\n              \"lvl3\" === t.type ||\n              \"lvl4\" === t.type ||\n              \"lvl5\" === t.type ||\n              \"lvl6\" === t.type) &&\n            Be.createElement(\n              \"div\",\n              { className: \"DocSearch-Hit-content-wrapper\" },\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-title\",\n                hit: t,\n                attribute: \"hierarchy.\".concat(t.type),\n              }),\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-path\",\n                hit: t,\n                attribute: \"hierarchy.lvl1\",\n              }),\n            ),\n          \"content\" === t.type &&\n            Be.createElement(\n              \"div\",\n              { className: \"DocSearch-Hit-content-wrapper\" },\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-title\",\n                hit: t,\n                attribute: \"content\",\n              }),\n              Be.createElement(kr, {\n                className: \"DocSearch-Hit-path\",\n                hit: t,\n                attribute: \"hierarchy.lvl1\",\n              }),\n            ),\n          o({\n            item: t,\n            runDeleteTransition: function (e) {\n              f(!0), (v.current = e);\n            },\n            runFavoriteTransition: function (e) {\n              d(!0), (v.current = e);\n            },\n          }),\n        ),\n      ),\n    );\n  }\n  function xr(e, t, n) {\n    return e.reduce(function (e, r) {\n      var o = t(r);\n      return (\n        e.hasOwnProperty(o) || (e[o] = []),\n        e[o].length < (n || 5) && e[o].push(r),\n        e\n      );\n    }, {});\n  }\n  function Nr(e) {\n    return e;\n  }\n  function Tr(e) {\n    return 1 === e.button || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;\n  }\n  function Rr() {}\n  var qr = /(<mark>|<\\/mark>)/g,\n    Lr = RegExp(qr.source);\n  function Mr(e) {\n    var t,\n      n,\n      r = e;\n    if (!r.__docsearch_parent && !e._highlightResult) return e.hierarchy.lvl0;\n    var o = (\n      (r.__docsearch_parent\n        ? null === (t = r.__docsearch_parent) ||\n          void 0 === t ||\n          null === (t = t._highlightResult) ||\n          void 0 === t ||\n          null === (t = t.hierarchy) ||\n          void 0 === t\n          ? void 0\n          : t.lvl0\n        : null === (n = e._highlightResult) ||\n          void 0 === n ||\n          null === (n = n.hierarchy) ||\n          void 0 === n\n        ? void 0\n        : n.lvl0) || {}\n    ).value;\n    return o && Lr.test(o) ? o.replace(qr, \"\") : o;\n  }\n  function Hr(e) {\n    return Be.createElement(\n      \"div\",\n      { className: \"DocSearch-Dropdown-Container\" },\n      e.state.collections.map(function (t) {\n        if (0 === t.items.length) return null;\n        var n = Mr(t.items[0]);\n        return Be.createElement(\n          Cr,\n          Je({}, e, {\n            key: t.source.sourceId,\n            title: n,\n            collection: t,\n            renderIcon: function (e) {\n              var n,\n                r = e.item,\n                o = e.index;\n              return Be.createElement(\n                Be.Fragment,\n                null,\n                r.__docsearch_parent &&\n                  Be.createElement(\n                    \"svg\",\n                    { className: \"DocSearch-Hit-Tree\", viewBox: \"0 0 24 54\" },\n                    Be.createElement(\n                      \"g\",\n                      {\n                        stroke: \"currentColor\",\n                        fill: \"none\",\n                        fillRule: \"evenodd\",\n                        strokeLinecap: \"round\",\n                        strokeLinejoin: \"round\",\n                      },\n                      r.__docsearch_parent !==\n                        (null === (n = t.items[o + 1]) || void 0 === n\n                          ? void 0\n                          : n.__docsearch_parent)\n                        ? Be.createElement(\"path\", { d: \"M8 6v21M20 27H8.3\" })\n                        : Be.createElement(\"path\", { d: \"M8 6v42M20 27H8.3\" }),\n                    ),\n                  ),\n                Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-icon\" },\n                  Be.createElement(_r, { type: r.type }),\n                ),\n              );\n            },\n            renderAction: function () {\n              return Be.createElement(\n                \"div\",\n                { className: \"DocSearch-Hit-action\" },\n                Be.createElement(hr, null),\n              );\n            },\n          }),\n        );\n      }),\n      e.resultsFooterComponent &&\n        Be.createElement(\n          \"section\",\n          { className: \"DocSearch-HitsFooter\" },\n          Be.createElement(e.resultsFooterComponent, { state: e.state }),\n        ),\n    );\n  }\n  var Ur = [\"translations\"];\n  function Fr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = $e(e, Ur),\n      o = n.recentSearchesTitle,\n      i = void 0 === o ? \"Recent\" : o,\n      c = n.noRecentSearchesText,\n      a = void 0 === c ? \"No recent searches\" : c,\n      u = n.saveRecentSearchButtonTitle,\n      l = void 0 === u ? \"Save this search\" : u,\n      s = n.removeRecentSearchButtonTitle,\n      f = void 0 === s ? \"Remove this search from history\" : s,\n      p = n.favoriteSearchesTitle,\n      m = void 0 === p ? \"Favorite\" : p,\n      d = n.removeFavoriteSearchButtonTitle,\n      v = void 0 === d ? \"Remove this search from favorites\" : d;\n    return \"idle\" === r.state.status && !1 === r.hasCollections\n      ? r.disableUserPersonalization\n        ? null\n        : Be.createElement(\n            \"div\",\n            { className: \"DocSearch-StartScreen\" },\n            Be.createElement(\"p\", { className: \"DocSearch-Help\" }, a),\n          )\n      : !1 === r.hasCollections\n      ? null\n      : Be.createElement(\n          \"div\",\n          { className: \"DocSearch-Dropdown-Container\" },\n          Be.createElement(\n            Cr,\n            Je({}, r, {\n              title: i,\n              collection: r.state.collections[0],\n              renderIcon: function () {\n                return Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-icon\" },\n                  Be.createElement(dr, null),\n                );\n              },\n              renderAction: function (e) {\n                var t = e.item,\n                  n = e.runFavoriteTransition,\n                  o = e.runDeleteTransition;\n                return Be.createElement(\n                  Be.Fragment,\n                  null,\n                  Be.createElement(\n                    \"div\",\n                    { className: \"DocSearch-Hit-action\" },\n                    Be.createElement(\n                      \"button\",\n                      {\n                        className: \"DocSearch-Hit-action-button\",\n                        title: l,\n                        type: \"submit\",\n                        onClick: function (e) {\n                          e.preventDefault(),\n                            e.stopPropagation(),\n                            n(function () {\n                              r.favoriteSearches.add(t),\n                                r.recentSearches.remove(t),\n                                r.refresh();\n                            });\n                        },\n                      },\n                      Be.createElement(Sr, null),\n                    ),\n                  ),\n                  Be.createElement(\n                    \"div\",\n                    { className: \"DocSearch-Hit-action\" },\n                    Be.createElement(\n                      \"button\",\n                      {\n                        className: \"DocSearch-Hit-action-button\",\n                        title: f,\n                        type: \"submit\",\n                        onClick: function (e) {\n                          e.preventDefault(),\n                            e.stopPropagation(),\n                            o(function () {\n                              r.recentSearches.remove(t), r.refresh();\n                            });\n                        },\n                      },\n                      Be.createElement(vr, null),\n                    ),\n                  ),\n                );\n              },\n            }),\n          ),\n          Be.createElement(\n            Cr,\n            Je({}, r, {\n              title: m,\n              collection: r.state.collections[1],\n              renderIcon: function () {\n                return Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-icon\" },\n                  Be.createElement(Sr, null),\n                );\n              },\n              renderAction: function (e) {\n                var t = e.item,\n                  n = e.runDeleteTransition;\n                return Be.createElement(\n                  \"div\",\n                  { className: \"DocSearch-Hit-action\" },\n                  Be.createElement(\n                    \"button\",\n                    {\n                      className: \"DocSearch-Hit-action-button\",\n                      title: v,\n                      type: \"submit\",\n                      onClick: function (e) {\n                        e.preventDefault(),\n                          e.stopPropagation(),\n                          n(function () {\n                            r.favoriteSearches.remove(t), r.refresh();\n                          });\n                      },\n                    },\n                    Be.createElement(vr, null),\n                  ),\n                );\n              },\n            }),\n          ),\n        );\n  }\n  var Br = [\"translations\"],\n    Vr = Be.memo(\n      function (e) {\n        var t = e.translations,\n          n = void 0 === t ? {} : t,\n          r = $e(e, Br);\n        if (\"error\" === r.state.status)\n          return Be.createElement(Er, {\n            translations: null == n ? void 0 : n.errorScreen,\n          });\n        var o = r.state.collections.some(function (e) {\n          return e.items.length > 0;\n        });\n        return r.state.query\n          ? !1 === o\n            ? Be.createElement(\n                Pr,\n                Je({}, r, {\n                  translations: null == n ? void 0 : n.noResultsScreen,\n                }),\n              )\n            : Be.createElement(Hr, r)\n          : Be.createElement(\n              Fr,\n              Je({}, r, {\n                hasCollections: o,\n                translations: null == n ? void 0 : n.startScreen,\n              }),\n            );\n      },\n      function (e, t) {\n        return \"loading\" === t.state.status || \"stalled\" === t.state.status;\n      },\n    ),\n    Kr = [\"translations\"];\n  function Wr(e) {\n    var t = e.translations,\n      n = void 0 === t ? {} : t,\n      r = $e(e, Kr),\n      o = n.resetButtonTitle,\n      i = void 0 === o ? \"Clear the query\" : o,\n      c = n.resetButtonAriaLabel,\n      a = void 0 === c ? \"Clear the query\" : c,\n      u = n.cancelButtonText,\n      l = void 0 === u ? \"Cancel\" : u,\n      s = n.cancelButtonAriaLabel,\n      f = void 0 === s ? \"Cancel\" : s,\n      p = n.searchInputLabel,\n      m = void 0 === p ? \"Search\" : p,\n      d = r.getFormProps({ inputElement: r.inputRef.current }).onReset;\n    return (\n      Be.useEffect(\n        function () {\n          r.autoFocus && r.inputRef.current && r.inputRef.current.focus();\n        },\n        [r.autoFocus, r.inputRef],\n      ),\n      Be.useEffect(\n        function () {\n          r.isFromSelection &&\n            r.inputRef.current &&\n            r.inputRef.current.select();\n        },\n        [r.isFromSelection, r.inputRef],\n      ),\n      Be.createElement(\n        Be.Fragment,\n        null,\n        Be.createElement(\n          \"form\",\n          {\n            className: \"DocSearch-Form\",\n            onSubmit: function (e) {\n              e.preventDefault();\n            },\n            onReset: d,\n          },\n          Be.createElement(\n            \"label\",\n            Je({ className: \"DocSearch-MagnifierLabel\" }, r.getLabelProps()),\n            Be.createElement(Xe, null),\n            Be.createElement(\n              \"span\",\n              { className: \"DocSearch-VisuallyHiddenForAccessibility\" },\n              m,\n            ),\n          ),\n          Be.createElement(\n            \"div\",\n            { className: \"DocSearch-LoadingIndicator\" },\n            Be.createElement(mr, null),\n          ),\n          Be.createElement(\n            \"input\",\n            Je(\n              { className: \"DocSearch-Input\", ref: r.inputRef },\n              r.getInputProps({\n                inputElement: r.inputRef.current,\n                autoFocus: r.autoFocus,\n                maxLength: 64,\n              }),\n            ),\n          ),\n          Be.createElement(\n            \"button\",\n            {\n              type: \"reset\",\n              title: i,\n              className: \"DocSearch-Reset\",\n              \"aria-label\": a,\n              hidden: !r.state.query,\n            },\n            Be.createElement(vr, null),\n          ),\n        ),\n        Be.createElement(\n          \"button\",\n          {\n            className: \"DocSearch-Cancel\",\n            type: \"reset\",\n            \"aria-label\": f,\n            onClick: r.onClose,\n          },\n          l,\n        ),\n      )\n    );\n  }\n  var zr = [\"_highlightResult\", \"_snippetResult\"];\n  function Jr(e) {\n    var t = e.key,\n      n = e.limit,\n      r = void 0 === n ? 5 : n,\n      o = (function (e) {\n        return !1 ===\n          (function () {\n            var e = \"__TEST_KEY__\";\n            try {\n              return (\n                localStorage.setItem(e, \"\"), localStorage.removeItem(e), !0\n              );\n            } catch (e) {\n              return !1;\n            }\n          })()\n          ? {\n              setItem: function () {},\n              getItem: function () {\n                return [];\n              },\n            }\n          : {\n              setItem: function (t) {\n                return window.localStorage.setItem(e, JSON.stringify(t));\n              },\n              getItem: function () {\n                var t = window.localStorage.getItem(e);\n                return t ? JSON.parse(t) : [];\n              },\n            };\n      })(t),\n      i = o.getItem().slice(0, r);\n    return {\n      add: function (e) {\n        var t = e,\n          n = (t._highlightResult, t._snippetResult, $e(t, zr)),\n          c = i.findIndex(function (e) {\n            return e.objectID === n.objectID;\n          });\n        c > -1 && i.splice(c, 1),\n          i.unshift(n),\n          (i = i.slice(0, r)),\n          o.setItem(i);\n      },\n      remove: function (e) {\n        (i = i.filter(function (t) {\n          return t.objectID !== e.objectID;\n        })),\n          o.setItem(i);\n      },\n      getAll: function () {\n        return i;\n      },\n    };\n  }\n  function $r(e) {\n    var t,\n      n = \"algoliasearch-client-js-\".concat(e.key),\n      r = function () {\n        return void 0 === t && (t = e.localStorage || window.localStorage), t;\n      },\n      o = function () {\n        return JSON.parse(r().getItem(n) || \"{}\");\n      },\n      i = function (e) {\n        r().setItem(n, JSON.stringify(e));\n      };\n    return {\n      get: function (t, n) {\n        var r =\n          arguments.length > 2 && void 0 !== arguments[2]\n            ? arguments[2]\n            : {\n                miss: function () {\n                  return Promise.resolve();\n                },\n              };\n        return Promise.resolve()\n          .then(function () {\n            !(function () {\n              var t = e.timeToLive ? 1e3 * e.timeToLive : null,\n                n = o(),\n                r = Object.fromEntries(\n                  Object.entries(n).filter(function (e) {\n                    return void 0 !== c(e, 2)[1].timestamp;\n                  }),\n                );\n              if ((i(r), t)) {\n                var a = Object.fromEntries(\n                  Object.entries(r).filter(function (e) {\n                    var n = c(e, 2)[1],\n                      r = new Date().getTime();\n                    return !(n.timestamp + t < r);\n                  }),\n                );\n                i(a);\n              }\n            })();\n            var n = JSON.stringify(t);\n            return o()[n];\n          })\n          .then(function (e) {\n            return Promise.all([e ? e.value : n(), void 0 !== e]);\n          })\n          .then(function (e) {\n            var t = c(e, 2),\n              n = t[0],\n              o = t[1];\n            return Promise.all([n, o || r.miss(n)]);\n          })\n          .then(function (e) {\n            return c(e, 1)[0];\n          });\n      },\n      set: function (e, t) {\n        return Promise.resolve().then(function () {\n          var i = o();\n          return (\n            (i[JSON.stringify(e)] = {\n              timestamp: new Date().getTime(),\n              value: t,\n            }),\n            r().setItem(n, JSON.stringify(i)),\n            t\n          );\n        });\n      },\n      delete: function (e) {\n        return Promise.resolve().then(function () {\n          var t = o();\n          delete t[JSON.stringify(e)], r().setItem(n, JSON.stringify(t));\n        });\n      },\n      clear: function () {\n        return Promise.resolve().then(function () {\n          r().removeItem(n);\n        });\n      },\n    };\n  }\n  function Zr(e) {\n    var t = a(e.caches),\n      n = t.shift();\n    return void 0 === n\n      ? {\n          get: function (e, t) {\n            var n =\n              arguments.length > 2 && void 0 !== arguments[2]\n                ? arguments[2]\n                : {\n                    miss: function () {\n                      return Promise.resolve();\n                    },\n                  };\n            return t()\n              .then(function (e) {\n                return Promise.all([e, n.miss(e)]);\n              })\n              .then(function (e) {\n                return c(e, 1)[0];\n              });\n          },\n          set: function (e, t) {\n            return Promise.resolve(t);\n          },\n          delete: function (e) {\n            return Promise.resolve();\n          },\n          clear: function () {\n            return Promise.resolve();\n          },\n        }\n      : {\n          get: function (e, r) {\n            var o =\n              arguments.length > 2 && void 0 !== arguments[2]\n                ? arguments[2]\n                : {\n                    miss: function () {\n                      return Promise.resolve();\n                    },\n                  };\n            return n.get(e, r, o).catch(function () {\n              return Zr({ caches: t }).get(e, r, o);\n            });\n          },\n          set: function (e, r) {\n            return n.set(e, r).catch(function () {\n              return Zr({ caches: t }).set(e, r);\n            });\n          },\n          delete: function (e) {\n            return n.delete(e).catch(function () {\n              return Zr({ caches: t }).delete(e);\n            });\n          },\n          clear: function () {\n            return n.clear().catch(function () {\n              return Zr({ caches: t }).clear();\n            });\n          },\n        };\n  }\n  function Qr() {\n    var e =\n        arguments.length > 0 && void 0 !== arguments[0]\n          ? arguments[0]\n          : { serializable: !0 },\n      t = {};\n    return {\n      get: function (n, r) {\n        var o =\n            arguments.length > 2 && void 0 !== arguments[2]\n              ? arguments[2]\n              : {\n                  miss: function () {\n                    return Promise.resolve();\n                  },\n                },\n          i = JSON.stringify(n);\n        if (i in t)\n          return Promise.resolve(e.serializable ? JSON.parse(t[i]) : t[i]);\n        var c = r(),\n          a =\n            (o && o.miss) ||\n            function () {\n              return Promise.resolve();\n            };\n        return c\n          .then(function (e) {\n            return a(e);\n          })\n          .then(function () {\n            return c;\n          });\n      },\n      set: function (n, r) {\n        return (\n          (t[JSON.stringify(n)] = e.serializable ? JSON.stringify(r) : r),\n          Promise.resolve(r)\n        );\n      },\n      delete: function (e) {\n        return delete t[JSON.stringify(e)], Promise.resolve();\n      },\n      clear: function () {\n        return (t = {}), Promise.resolve();\n      },\n    };\n  }\n  function Yr(e) {\n    for (var t = e.length - 1; t > 0; t--) {\n      var n = Math.floor(Math.random() * (t + 1)),\n        r = e[t];\n      (e[t] = e[n]), (e[n] = r);\n    }\n    return e;\n  }\n  function Gr(e, t) {\n    return t\n      ? (Object.keys(t).forEach(function (n) {\n          e[n] = t[n](e);\n        }),\n        e)\n      : e;\n  }\n  function Xr(e) {\n    for (\n      var t = arguments.length, n = new Array(t > 1 ? t - 1 : 0), r = 1;\n      r < t;\n      r++\n    )\n      n[r - 1] = arguments[r];\n    var o = 0;\n    return e.replace(/%s/g, function () {\n      return encodeURIComponent(n[o++]);\n    });\n  }\n  var eo = 0,\n    to = 1;\n  function no(e, t) {\n    var n = e || {},\n      r = n.data || {};\n    return (\n      Object.keys(n).forEach(function (e) {\n        -1 ===\n          [\n            \"timeout\",\n            \"headers\",\n            \"queryParameters\",\n            \"data\",\n            \"cacheable\",\n          ].indexOf(e) && (r[e] = n[e]);\n      }),\n      {\n        data: Object.entries(r).length > 0 ? r : void 0,\n        timeout: n.timeout || t,\n        headers: n.headers || {},\n        queryParameters: n.queryParameters || {},\n        cacheable: n.cacheable,\n      }\n    );\n  }\n  var ro = { Read: 1, Write: 2, Any: 3 };\n  function oo(e) {\n    var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 1;\n    return t(t({}, e), {}, { status: n, lastUpdate: Date.now() });\n  }\n  function io(e) {\n    return \"string\" == typeof e\n      ? { protocol: \"https\", url: e, accept: ro.Any }\n      : {\n          protocol: e.protocol || \"https\",\n          url: e.url,\n          accept: e.accept || ro.Any,\n        };\n  }\n  var co = \"GET\",\n    ao = \"POST\";\n  function uo(e, n, r, o) {\n    var i = [],\n      c = (function (e, n) {\n        if (e.method !== co && (void 0 !== e.data || void 0 !== n.data)) {\n          var r = Array.isArray(e.data) ? e.data : t(t({}, e.data), n.data);\n          return JSON.stringify(r);\n        }\n      })(r, o),\n      u = (function (e, n) {\n        var r = t(t({}, e.headers), n.headers),\n          o = {};\n        return (\n          Object.keys(r).forEach(function (e) {\n            var t = r[e];\n            o[e.toLowerCase()] = t;\n          }),\n          o\n        );\n      })(e, o),\n      l = r.method,\n      s = r.method !== co ? {} : t(t({}, r.data), o.data),\n      f = t(\n        t(t({ \"x-algolia-agent\": e.userAgent.value }, e.queryParameters), s),\n        o.queryParameters,\n      ),\n      p = 0,\n      m = function t(n, a) {\n        var s = n.pop();\n        if (void 0 === s)\n          throw {\n            name: \"RetryError\",\n            message:\n              \"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.\",\n            transporterStackTrace: po(i),\n          };\n        var m = {\n            data: c,\n            headers: u,\n            method: l,\n            url: so(s, r.path, f),\n            connectTimeout: a(p, e.timeouts.connect),\n            responseTimeout: a(p, o.timeout),\n          },\n          d = function (e) {\n            var t = { request: m, response: e, host: s, triesLeft: n.length };\n            return i.push(t), t;\n          },\n          v = {\n            onSuccess: function (e) {\n              return (function (e) {\n                try {\n                  return JSON.parse(e.content);\n                } catch (t) {\n                  throw (function (e, t) {\n                    return {\n                      name: \"DeserializationError\",\n                      message: e,\n                      response: t,\n                    };\n                  })(t.message, e);\n                }\n              })(e);\n            },\n            onRetry: function (r) {\n              var o = d(r);\n              return (\n                r.isTimedOut && p++,\n                Promise.all([\n                  e.logger.info(\"Retryable failure\", mo(o)),\n                  e.hostsCache.set(s, oo(s, r.isTimedOut ? 3 : 2)),\n                ]).then(function () {\n                  return t(n, a);\n                })\n              );\n            },\n            onFail: function (e) {\n              throw (\n                (d(e),\n                (function (e, t) {\n                  var n = e.content,\n                    r = e.status,\n                    o = n;\n                  try {\n                    o = JSON.parse(n).message;\n                  } catch (n) {}\n                  return (function (e, t, n) {\n                    return {\n                      name: \"ApiError\",\n                      message: e,\n                      status: t,\n                      transporterStackTrace: n,\n                    };\n                  })(o, r, t);\n                })(e, po(i)))\n              );\n            },\n          };\n        return e.requester.send(m).then(function (e) {\n          return (function (e, t) {\n            return (function (e) {\n              var t = e.status;\n              return (\n                e.isTimedOut ||\n                (function (e) {\n                  var t = e.isTimedOut,\n                    n = e.status;\n                  return !t && 0 == ~~n;\n                })(e) ||\n                (2 != ~~(t / 100) && 4 != ~~(t / 100))\n              );\n            })(e)\n              ? t.onRetry(e)\n              : ((n = e),\n                2 == ~~(n.status / 100) ? t.onSuccess(e) : t.onFail(e));\n            var n;\n          })(e, v);\n        });\n      };\n    return (function (e, t) {\n      return Promise.all(\n        t.map(function (t) {\n          return e.get(t, function () {\n            return Promise.resolve(oo(t));\n          });\n        }),\n      ).then(function (e) {\n        var n = e.filter(function (e) {\n            return (function (e) {\n              return 1 === e.status || Date.now() - e.lastUpdate > 12e4;\n            })(e);\n          }),\n          r = e.filter(function (e) {\n            return (function (e) {\n              return 3 === e.status && Date.now() - e.lastUpdate <= 12e4;\n            })(e);\n          }),\n          o = [].concat(a(n), a(r));\n        return {\n          getTimeout: function (e, t) {\n            return (0 === r.length && 0 === e ? 1 : r.length + 3 + e) * t;\n          },\n          statelessHosts:\n            o.length > 0\n              ? o.map(function (e) {\n                  return io(e);\n                })\n              : t,\n        };\n      });\n    })(e.hostsCache, n).then(function (e) {\n      return m(a(e.statelessHosts).reverse(), e.getTimeout);\n    });\n  }\n  function lo(e) {\n    var t = {\n      value: \"Algolia for JavaScript (\".concat(e, \")\"),\n      add: function (e) {\n        var n = \"; \"\n          .concat(e.segment)\n          .concat(void 0 !== e.version ? \" (\".concat(e.version, \")\") : \"\");\n        return (\n          -1 === t.value.indexOf(n) && (t.value = \"\".concat(t.value).concat(n)),\n          t\n        );\n      },\n    };\n    return t;\n  }\n  function so(e, t, n) {\n    var r = fo(n),\n      o = \"\"\n        .concat(e.protocol, \"://\")\n        .concat(e.url, \"/\")\n        .concat(\"/\" === t.charAt(0) ? t.substr(1) : t);\n    return r.length && (o += \"?\".concat(r)), o;\n  }\n  function fo(e) {\n    return Object.keys(e)\n      .map(function (t) {\n        return Xr(\n          \"%s=%s\",\n          t,\n          ((n = e[t]),\n          \"[object Object]\" === Object.prototype.toString.call(n) ||\n          \"[object Array]\" === Object.prototype.toString.call(n)\n            ? JSON.stringify(e[t])\n            : e[t]),\n        );\n        var n;\n      })\n      .join(\"&\");\n  }\n  function po(e) {\n    return e.map(function (e) {\n      return mo(e);\n    });\n  }\n  function mo(e) {\n    var n = e.request.headers[\"x-algolia-api-key\"]\n      ? { \"x-algolia-api-key\": \"*****\" }\n      : {};\n    return t(\n      t({}, e),\n      {},\n      {\n        request: t(\n          t({}, e.request),\n          {},\n          { headers: t(t({}, e.request.headers), n) },\n        ),\n      },\n    );\n  }\n  var vo = function (e) {\n      return function (t, n) {\n        return t.method === co\n          ? e.transporter.read(t, n)\n          : e.transporter.write(t, n);\n      };\n    },\n    ho = function (e) {\n      return function (t) {\n        var n =\n          arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {};\n        return Gr(\n          { transporter: e.transporter, appId: e.appId, indexName: t },\n          n.methods,\n        );\n      };\n    },\n    yo = function (e) {\n      return function (n, r) {\n        var o = n.map(function (e) {\n          return t(t({}, e), {}, { params: fo(e.params || {}) });\n        });\n        return e.transporter.read(\n          {\n            method: ao,\n            path: \"1/indexes/*/queries\",\n            data: { requests: o },\n            cacheable: !0,\n          },\n          r,\n        );\n      };\n    },\n    _o = function (e) {\n      return function (n, r) {\n        return Promise.all(\n          n.map(function (n) {\n            var o = n.params,\n              c = o.facetName,\n              a = o.facetQuery,\n              u = i(o, Ve);\n            return ho(e)(n.indexName, {\n              methods: { searchForFacetValues: So },\n            }).searchForFacetValues(c, a, t(t({}, r), u));\n          }),\n        );\n      };\n    },\n    bo = function (e) {\n      return function (t, n, r) {\n        return e.transporter.read(\n          {\n            method: ao,\n            path: Xr(\"1/answers/%s/prediction\", e.indexName),\n            data: { query: t, queryLanguages: n },\n            cacheable: !0,\n          },\n          r,\n        );\n      };\n    },\n    go = function (e) {\n      return function (t, n) {\n        return e.transporter.read(\n          {\n            method: ao,\n            path: Xr(\"1/indexes/%s/query\", e.indexName),\n            data: { query: t },\n            cacheable: !0,\n          },\n          n,\n        );\n      };\n    },\n    So = function (e) {\n      return function (t, n, r) {\n        return e.transporter.read(\n          {\n            method: ao,\n            path: Xr(\"1/indexes/%s/facets/%s/query\", e.indexName, t),\n            data: { facetQuery: n },\n            cacheable: !0,\n          },\n          r,\n        );\n      };\n    };\n  function Oo(e, n, r) {\n    var o = {\n      appId: e,\n      apiKey: n,\n      timeouts: { connect: 1, read: 2, write: 30 },\n      requester: {\n        send: function (e) {\n          return new Promise(function (t) {\n            var n = new XMLHttpRequest();\n            n.open(e.method, e.url, !0),\n              Object.keys(e.headers).forEach(function (t) {\n                return n.setRequestHeader(t, e.headers[t]);\n              });\n            var r,\n              o = function (e, r) {\n                return setTimeout(function () {\n                  n.abort(), t({ status: 0, content: r, isTimedOut: !0 });\n                }, 1e3 * e);\n              },\n              i = o(e.connectTimeout, \"Connection timeout\");\n            (n.onreadystatechange = function () {\n              n.readyState > n.OPENED &&\n                void 0 === r &&\n                (clearTimeout(i), (r = o(e.responseTimeout, \"Socket timeout\")));\n            }),\n              (n.onerror = function () {\n                0 === n.status &&\n                  (clearTimeout(i),\n                  clearTimeout(r),\n                  t({\n                    content: n.responseText || \"Network request failed\",\n                    status: n.status,\n                    isTimedOut: !1,\n                  }));\n              }),\n              (n.onload = function () {\n                clearTimeout(i),\n                  clearTimeout(r),\n                  t({\n                    content: n.responseText,\n                    status: n.status,\n                    isTimedOut: !1,\n                  });\n              }),\n              n.send(e.data);\n          });\n        },\n      },\n      logger:\n        (3,\n        {\n          debug: function (e, t) {\n            return Promise.resolve();\n          },\n          info: function (e, t) {\n            return Promise.resolve();\n          },\n          error: function (e, t) {\n            return console.error(e, t), Promise.resolve();\n          },\n        }),\n      responsesCache: Qr(),\n      requestsCache: Qr({ serializable: !1 }),\n      hostsCache: Zr({ caches: [$r({ key: \"4.19.1-\".concat(e) }), Qr()] }),\n      userAgent: lo(\"4.19.1\").add({ segment: \"Browser\", version: \"lite\" }),\n      authMode: eo,\n    };\n    return (function (e) {\n      var n = e.appId,\n        r = (function (e, t, n) {\n          var r = { \"x-algolia-api-key\": n, \"x-algolia-application-id\": t };\n          return {\n            headers: function () {\n              return e === to ? r : {};\n            },\n            queryParameters: function () {\n              return e === eo ? r : {};\n            },\n          };\n        })(void 0 !== e.authMode ? e.authMode : to, n, e.apiKey),\n        o = (function (e) {\n          var t = e.hostsCache,\n            n = e.logger,\n            r = e.requester,\n            o = e.requestsCache,\n            i = e.responsesCache,\n            a = e.timeouts,\n            u = e.userAgent,\n            l = e.hosts,\n            s = e.queryParameters,\n            f = {\n              hostsCache: t,\n              logger: n,\n              requester: r,\n              requestsCache: o,\n              responsesCache: i,\n              timeouts: a,\n              userAgent: u,\n              headers: e.headers,\n              queryParameters: s,\n              hosts: l.map(function (e) {\n                return io(e);\n              }),\n              read: function (e, t) {\n                var n = no(t, f.timeouts.read),\n                  r = function () {\n                    return uo(\n                      f,\n                      f.hosts.filter(function (e) {\n                        return 0 != (e.accept & ro.Read);\n                      }),\n                      e,\n                      n,\n                    );\n                  };\n                if (!0 !== (void 0 !== n.cacheable ? n.cacheable : e.cacheable))\n                  return r();\n                var o = {\n                  request: e,\n                  mappedRequestOptions: n,\n                  transporter: {\n                    queryParameters: f.queryParameters,\n                    headers: f.headers,\n                  },\n                };\n                return f.responsesCache.get(\n                  o,\n                  function () {\n                    return f.requestsCache.get(o, function () {\n                      return f.requestsCache\n                        .set(o, r())\n                        .then(\n                          function (e) {\n                            return Promise.all([f.requestsCache.delete(o), e]);\n                          },\n                          function (e) {\n                            return Promise.all([\n                              f.requestsCache.delete(o),\n                              Promise.reject(e),\n                            ]);\n                          },\n                        )\n                        .then(function (e) {\n                          var t = c(e, 2);\n                          return t[0], t[1];\n                        });\n                    });\n                  },\n                  {\n                    miss: function (e) {\n                      return f.responsesCache.set(o, e);\n                    },\n                  },\n                );\n              },\n              write: function (e, t) {\n                return uo(\n                  f,\n                  f.hosts.filter(function (e) {\n                    return 0 != (e.accept & ro.Write);\n                  }),\n                  e,\n                  no(t, f.timeouts.write),\n                );\n              },\n            };\n          return f;\n        })(\n          t(\n            t(\n              {\n                hosts: [\n                  { url: \"\".concat(n, \"-dsn.algolia.net\"), accept: ro.Read },\n                  { url: \"\".concat(n, \".algolia.net\"), accept: ro.Write },\n                ].concat(\n                  Yr([\n                    { url: \"\".concat(n, \"-1.algolianet.com\") },\n                    { url: \"\".concat(n, \"-2.algolianet.com\") },\n                    { url: \"\".concat(n, \"-3.algolianet.com\") },\n                  ]),\n                ),\n              },\n              e,\n            ),\n            {},\n            {\n              headers: t(\n                t({}, r.headers()),\n                {},\n                { \"content-type\": \"application/x-www-form-urlencoded\" },\n                e.headers,\n              ),\n              queryParameters: t(t({}, r.queryParameters()), e.queryParameters),\n            },\n          ),\n        ),\n        i = {\n          transporter: o,\n          appId: n,\n          addAlgoliaAgent: function (e, t) {\n            o.userAgent.add({ segment: e, version: t });\n          },\n          clearCache: function () {\n            return Promise.all([\n              o.requestsCache.clear(),\n              o.responsesCache.clear(),\n            ]).then(function () {});\n          },\n        };\n      return Gr(i, e.methods);\n    })(\n      t(\n        t(t({}, o), r),\n        {},\n        {\n          methods: {\n            search: yo,\n            searchForFacetValues: _o,\n            multipleQueries: yo,\n            multipleSearchForFacetValues: _o,\n            customRequest: vo,\n            initIndex: function (e) {\n              return function (t) {\n                return ho(e)(t, {\n                  methods: {\n                    search: go,\n                    searchForFacetValues: So,\n                    findAnswers: bo,\n                  },\n                });\n              };\n            },\n          },\n        },\n      ),\n    );\n  }\n  Oo.version = \"4.19.1\";\n  var wo = [\"footer\", \"searchBox\"];\n  function Eo(e) {\n    var t = e.appId,\n      n = e.apiKey,\n      r = e.indexName,\n      o = e.placeholder,\n      i = void 0 === o ? \"Search docs\" : o,\n      c = e.searchParameters,\n      a = e.maxResultsPerGroup,\n      u = e.onClose,\n      l = void 0 === u ? Rr : u,\n      s = e.transformItems,\n      f = void 0 === s ? Nr : s,\n      p = e.hitComponent,\n      m = void 0 === p ? pr : p,\n      d = e.resultsFooterComponent,\n      v =\n        void 0 === d\n          ? function () {\n              return null;\n            }\n          : d,\n      h = e.navigator,\n      y = e.initialScrollY,\n      _ = void 0 === y ? 0 : y,\n      b = e.transformSearchClient,\n      g = void 0 === b ? Nr : b,\n      S = e.disableUserPersonalization,\n      O = void 0 !== S && S,\n      w = e.initialQuery,\n      E = void 0 === w ? \"\" : w,\n      j = e.translations,\n      P = void 0 === j ? {} : j,\n      I = e.getMissingResultsUrl,\n      D = e.insights,\n      k = void 0 !== D && D,\n      C = P.footer,\n      A = P.searchBox,\n      x = $e(P, wo),\n      N = Ze(\n        Be.useState({\n          query: \"\",\n          collections: [],\n          completion: null,\n          context: {},\n          isOpen: !1,\n          activeItemId: null,\n          status: \"idle\",\n        }),\n        2,\n      ),\n      T = N[0],\n      R = N[1],\n      q = Be.useRef(null),\n      L = Be.useRef(null),\n      M = Be.useRef(null),\n      H = Be.useRef(null),\n      U = Be.useRef(null),\n      F = Be.useRef(10),\n      B = Be.useRef(\n        \"undefined\" != typeof window\n          ? window.getSelection().toString().slice(0, 64)\n          : \"\",\n      ).current,\n      V = Be.useRef(E || B).current,\n      K = (function (e, t, n) {\n        return Be.useMemo(\n          function () {\n            var r = Oo(e, t);\n            return (\n              r.addAlgoliaAgent(\"docsearch\", \"3.6.1\"),\n              !1 ===\n                /docsearch.js \\(.*\\)/.test(r.transporter.userAgent.value) &&\n                r.addAlgoliaAgent(\"docsearch-react\", \"3.6.1\"),\n              n(r)\n            );\n          },\n          [e, t, n],\n        );\n      })(t, n, g),\n      W = Be.useRef(\n        Jr({ key: \"__DOCSEARCH_FAVORITE_SEARCHES__\".concat(r), limit: 10 }),\n      ).current,\n      z = Be.useRef(\n        Jr({\n          key: \"__DOCSEARCH_RECENT_SEARCHES__\".concat(r),\n          limit: 0 === W.getAll().length ? 7 : 4,\n        }),\n      ).current,\n      J = Be.useCallback(\n        function (e) {\n          if (!O) {\n            var t = \"content\" === e.type ? e.__docsearch_parent : e;\n            t &&\n              -1 ===\n                W.getAll().findIndex(function (e) {\n                  return e.objectID === t.objectID;\n                }) &&\n              z.add(t);\n          }\n        },\n        [W, z, O],\n      ),\n      $ = Be.useCallback(\n        function (e) {\n          if (T.context.algoliaInsightsPlugin && e.__autocomplete_id) {\n            var t = e,\n              n = {\n                eventName: \"Item Selected\",\n                index: t.__autocomplete_indexName,\n                items: [t],\n                positions: [e.__autocomplete_id],\n                queryID: t.__autocomplete_queryID,\n              };\n            T.context.algoliaInsightsPlugin.insights.clickedObjectIDsAfterSearch(\n              n,\n            );\n          }\n        },\n        [T.context.algoliaInsightsPlugin],\n      ),\n      Z = Be.useMemo(\n        function () {\n          return ur({\n            id: \"docsearch\",\n            defaultActiveItemId: 0,\n            placeholder: i,\n            openOnFocus: !0,\n            initialState: { query: V, context: { searchSuggestions: [] } },\n            insights: k,\n            navigator: h,\n            onStateChange: function (e) {\n              R(e.state);\n            },\n            getSources: function (e) {\n              var o = e.query,\n                i = e.state,\n                u = e.setContext,\n                s = e.setStatus;\n              if (!o)\n                return O\n                  ? []\n                  : [\n                      {\n                        sourceId: \"recentSearches\",\n                        onSelect: function (e) {\n                          var t = e.item,\n                            n = e.event;\n                          J(t), Tr(n) || l();\n                        },\n                        getItemUrl: function (e) {\n                          return e.item.url;\n                        },\n                        getItems: function () {\n                          return z.getAll();\n                        },\n                      },\n                      {\n                        sourceId: \"favoriteSearches\",\n                        onSelect: function (e) {\n                          var t = e.item,\n                            n = e.event;\n                          J(t), Tr(n) || l();\n                        },\n                        getItemUrl: function (e) {\n                          return e.item.url;\n                        },\n                        getItems: function () {\n                          return W.getAll();\n                        },\n                      },\n                    ];\n              var p = Boolean(k);\n              return K.search([\n                {\n                  query: o,\n                  indexName: r,\n                  params: We(\n                    {\n                      attributesToRetrieve: [\n                        \"hierarchy.lvl0\",\n                        \"hierarchy.lvl1\",\n                        \"hierarchy.lvl2\",\n                        \"hierarchy.lvl3\",\n                        \"hierarchy.lvl4\",\n                        \"hierarchy.lvl5\",\n                        \"hierarchy.lvl6\",\n                        \"content\",\n                        \"type\",\n                        \"url\",\n                      ],\n                      attributesToSnippet: [\n                        \"hierarchy.lvl1:\".concat(F.current),\n                        \"hierarchy.lvl2:\".concat(F.current),\n                        \"hierarchy.lvl3:\".concat(F.current),\n                        \"hierarchy.lvl4:\".concat(F.current),\n                        \"hierarchy.lvl5:\".concat(F.current),\n                        \"hierarchy.lvl6:\".concat(F.current),\n                        \"content:\".concat(F.current),\n                      ],\n                      snippetEllipsisText: \"…\",\n                      highlightPreTag: \"<mark>\",\n                      highlightPostTag: \"</mark>\",\n                      hitsPerPage: 20,\n                      clickAnalytics: p,\n                    },\n                    c,\n                  ),\n                },\n              ])\n                .catch(function (e) {\n                  throw (\"RetryError\" === e.name && s(\"error\"), e);\n                })\n                .then(function (e) {\n                  var o = e.results[0],\n                    c = o.hits,\n                    s = o.nbHits,\n                    m = xr(\n                      c,\n                      function (e) {\n                        return Mr(e);\n                      },\n                      a,\n                    );\n                  i.context.searchSuggestions.length < Object.keys(m).length &&\n                    u({ searchSuggestions: Object.keys(m) }),\n                    u({ nbHits: s });\n                  var d = {};\n                  return (\n                    p &&\n                      (d = {\n                        __autocomplete_indexName: r,\n                        __autocomplete_queryID: o.queryID,\n                        __autocomplete_algoliaCredentials: {\n                          appId: t,\n                          apiKey: n,\n                        },\n                      }),\n                    Object.values(m).map(function (e, t) {\n                      return {\n                        sourceId: \"hits\".concat(t),\n                        onSelect: function (e) {\n                          var t = e.item,\n                            n = e.event;\n                          J(t), Tr(n) || l();\n                        },\n                        getItemUrl: function (e) {\n                          return e.item.url;\n                        },\n                        getItems: function () {\n                          return Object.values(\n                            xr(\n                              e,\n                              function (e) {\n                                return e.hierarchy.lvl1;\n                              },\n                              a,\n                            ),\n                          )\n                            .map(f)\n                            .map(function (e) {\n                              return e.map(function (t) {\n                                var n = null,\n                                  r = e.find(function (e) {\n                                    return (\n                                      \"lvl1\" === e.type &&\n                                      e.hierarchy.lvl1 === t.hierarchy.lvl1\n                                    );\n                                  });\n                                return (\n                                  \"lvl1\" !== t.type && r && (n = r),\n                                  We(\n                                    We({}, t),\n                                    {},\n                                    { __docsearch_parent: n },\n                                    d,\n                                  )\n                                );\n                              });\n                            })\n                            .flat();\n                        },\n                      };\n                    })\n                  );\n                });\n            },\n          });\n        },\n        [r, c, a, K, l, z, W, J, V, i, h, f, O, k, t, n],\n      ),\n      Q = Z.getEnvironmentProps,\n      Y = Z.getRootProps,\n      G = Z.refresh;\n    return (\n      (function (e) {\n        var t = e.getEnvironmentProps,\n          n = e.panelElement,\n          r = e.formElement,\n          o = e.inputElement;\n        Be.useEffect(\n          function () {\n            if (n && r && o) {\n              var e = t({ panelElement: n, formElement: r, inputElement: o }),\n                i = e.onTouchStart,\n                c = e.onTouchMove;\n              return (\n                window.addEventListener(\"touchstart\", i),\n                window.addEventListener(\"touchmove\", c),\n                function () {\n                  window.removeEventListener(\"touchstart\", i),\n                    window.removeEventListener(\"touchmove\", c);\n                }\n              );\n            }\n          },\n          [t, n, r, o],\n        );\n      })({\n        getEnvironmentProps: Q,\n        panelElement: H.current,\n        formElement: M.current,\n        inputElement: U.current,\n      }),\n      (function (e) {\n        var t = e.container;\n        Be.useEffect(\n          function () {\n            if (t) {\n              var e = t.querySelectorAll(\n                  \"a[href]:not([disabled]), button:not([disabled]), input:not([disabled])\",\n                ),\n                n = e[0],\n                r = e[e.length - 1];\n              return (\n                t.addEventListener(\"keydown\", o),\n                function () {\n                  t.removeEventListener(\"keydown\", o);\n                }\n              );\n            }\n            function o(e) {\n              \"Tab\" === e.key &&\n                (e.shiftKey\n                  ? document.activeElement === n &&\n                    (e.preventDefault(), r.focus())\n                  : document.activeElement === r &&\n                    (e.preventDefault(), n.focus()));\n            }\n          },\n          [t],\n        );\n      })({ container: q.current }),\n      Be.useEffect(function () {\n        return (\n          document.body.classList.add(\"DocSearch--active\"),\n          function () {\n            var e, t;\n            document.body.classList.remove(\"DocSearch--active\"),\n              null === (e = (t = window).scrollTo) ||\n                void 0 === e ||\n                e.call(t, 0, _);\n          }\n        );\n      }, []),\n      Be.useEffect(function () {\n        window.matchMedia(\"(max-width: 768px)\").matches && (F.current = 5);\n      }, []),\n      Be.useEffect(\n        function () {\n          H.current && (H.current.scrollTop = 0);\n        },\n        [T.query],\n      ),\n      Be.useEffect(\n        function () {\n          V.length > 0 && (G(), U.current && U.current.focus());\n        },\n        [V, G],\n      ),\n      Be.useEffect(function () {\n        function e() {\n          if (L.current) {\n            var e = 0.01 * window.innerHeight;\n            L.current.style.setProperty(\"--docsearch-vh\", \"\".concat(e, \"px\"));\n          }\n        }\n        return (\n          e(),\n          window.addEventListener(\"resize\", e),\n          function () {\n            window.removeEventListener(\"resize\", e);\n          }\n        );\n      }, []),\n      Be.createElement(\n        \"div\",\n        Je({ ref: q }, Y({ \"aria-expanded\": !0 }), {\n          className: [\n            \"DocSearch\",\n            \"DocSearch-Container\",\n            \"stalled\" === T.status && \"DocSearch-Container--Stalled\",\n            \"error\" === T.status && \"DocSearch-Container--Errored\",\n          ]\n            .filter(Boolean)\n            .join(\" \"),\n          role: \"button\",\n          tabIndex: 0,\n          onMouseDown: function (e) {\n            e.target === e.currentTarget && l();\n          },\n        }),\n        Be.createElement(\n          \"div\",\n          { className: \"DocSearch-Modal\", ref: L },\n          Be.createElement(\n            \"header\",\n            { className: \"DocSearch-SearchBar\", ref: M },\n            Be.createElement(\n              Wr,\n              Je({}, Z, {\n                state: T,\n                autoFocus: 0 === V.length,\n                inputRef: U,\n                isFromSelection: Boolean(V) && V === B,\n                translations: A,\n                onClose: l,\n              }),\n            ),\n          ),\n          Be.createElement(\n            \"div\",\n            { className: \"DocSearch-Dropdown\", ref: H },\n            Be.createElement(\n              Vr,\n              Je({}, Z, {\n                indexName: r,\n                state: T,\n                hitComponent: m,\n                resultsFooterComponent: v,\n                disableUserPersonalization: O,\n                recentSearches: z,\n                favoriteSearches: W,\n                inputRef: U,\n                translations: x,\n                getMissingResultsUrl: I,\n                onItemClick: function (e, t) {\n                  $(e), J(e), Tr(t) || l();\n                },\n              }),\n            ),\n          ),\n          Be.createElement(\n            \"footer\",\n            { className: \"DocSearch-Footer\" },\n            Be.createElement(fr, { translations: C }),\n          ),\n        ),\n      )\n    );\n  }\n  function jo(e) {\n    var t,\n      n,\n      r = Be.useRef(null),\n      o = Ze(Be.useState(!1), 2),\n      i = o[0],\n      c = o[1],\n      a = Ze(Be.useState((null == e ? void 0 : e.initialQuery) || void 0), 2),\n      u = a[0],\n      l = a[1],\n      s = Be.useCallback(\n        function () {\n          c(!0);\n        },\n        [c],\n      ),\n      f = Be.useCallback(\n        function () {\n          c(!1);\n        },\n        [c],\n      );\n    return (\n      (function (e) {\n        var t = e.isOpen,\n          n = e.onOpen,\n          r = e.onClose,\n          o = e.onInput,\n          i = e.searchButtonRef;\n        Be.useEffect(\n          function () {\n            function e(e) {\n              var c;\n              ((27 === e.keyCode && t) ||\n                (\"k\" ===\n                  (null === (c = e.key) || void 0 === c\n                    ? void 0\n                    : c.toLowerCase()) &&\n                  (e.metaKey || e.ctrlKey)) ||\n                (!(function (e) {\n                  var t = e.target,\n                    n = t.tagName;\n                  return (\n                    t.isContentEditable ||\n                    \"INPUT\" === n ||\n                    \"SELECT\" === n ||\n                    \"TEXTAREA\" === n\n                  );\n                })(e) &&\n                  \"/\" === e.key &&\n                  !t)) &&\n                (e.preventDefault(),\n                t\n                  ? r()\n                  : document.body.classList.contains(\"DocSearch--active\") ||\n                    document.body.classList.contains(\"DocSearch--active\") ||\n                    n()),\n                i &&\n                  i.current === document.activeElement &&\n                  o &&\n                  /[a-zA-Z0-9]/.test(String.fromCharCode(e.keyCode)) &&\n                  o(e);\n            }\n            return (\n              window.addEventListener(\"keydown\", e),\n              function () {\n                window.removeEventListener(\"keydown\", e);\n              }\n            );\n          },\n          [t, n, r, o, i],\n        );\n      })({\n        isOpen: i,\n        onOpen: s,\n        onClose: f,\n        onInput: Be.useCallback(\n          function (e) {\n            c(!0), l(e.key);\n          },\n          [c, l],\n        ),\n        searchButtonRef: r,\n      }),\n      Be.createElement(\n        Be.Fragment,\n        null,\n        Be.createElement(tt, {\n          ref: r,\n          translations:\n            null == e || null === (t = e.translations) || void 0 === t\n              ? void 0\n              : t.button,\n          onClick: s,\n        }),\n        i &&\n          Ie(\n            Be.createElement(\n              Eo,\n              Je({}, e, {\n                initialScrollY: window.scrollY,\n                initialQuery: u,\n                translations:\n                  null == e || null === (n = e.translations) || void 0 === n\n                    ? void 0\n                    : n.modal,\n                onClose: f,\n              }),\n            ),\n            document.body,\n          ),\n      )\n    );\n  }\n  return function (e) {\n    Ae(\n      Be.createElement(\n        jo,\n        o({}, e, {\n          transformSearchClient: function (t) {\n            return (\n              t.addAlgoliaAgent(\"docsearch.js\", \"3.6.1\"),\n              e.transformSearchClient ? e.transformSearchClient(t) : t\n            );\n          },\n        }),\n      ),\n      (function (e) {\n        var t =\n          arguments.length > 1 && void 0 !== arguments[1]\n            ? arguments[1]\n            : window;\n        return \"string\" == typeof e ? t.document.querySelector(e) : e;\n      })(e.container, e.environment),\n    );\n  };\n});\n//# sourceMappingURL=index.js.map\n\ndocsearch({\n  container: \"#docsearch\",\n  appId: \"74VN1YECLR\",\n  indexName: \"gpt-index\",\n  apiKey: \"c4b0e099fa9004f69855e474b3e7d3bb\",\n});\n"
  },
  {
    "path": "docs/api_docs/mkdocs.yml",
    "content": "docs_dir: docs/api_reference\nsite_url: https://developers.llamaindex.ai/python/workflows-api-reference/\nextra:\n  homepage: /\nextra_css:\n  - _static/css/custom.css\n  - _static/css/algolia.css\nextra_javascript:\n  - _static/js/algolia.js\nmarkdown_extensions:\n  - attr_list\n  - admonition\n  - pymdownx.details\n  - pymdownx.superfences\n  - md_in_html\n  - mkdocs-click\n  - toc:\n      permalink: \"#\"\nplugins:\n  - search\n  - include_dir_to_nav\n  - render_swagger\n  - gh-admonitions\n  - mkdocstrings:\n      handlers:\n        python:\n          load_external_modules: false\n          options:\n            allow_inspection: false\n            docstring_options:\n              ignore_init_summary: true\n            docstring_style: google\n            filters:\n              - \"!^_\"\n              - \"!^__init__\"\n            members_order: source\n            merge_init_into_class: false\n            show_symbol_type_heading: true\n            show_symbol_type_toc: true\n            separate_signature: true\n            show_root_full_path: true\n            show_root_heading: false\n            show_root_toc_entry: false\n            show_signature_annotations: true\n            signature_crossrefs: true\n            extensions:\n              - griffe_fieldz\n          paths:\n            - ../../workflows\n            - ../../packages/llama-agents-client/src\n            - ../../packages/llama-agents-server/src\nsite_name: LlamaIndex Workflows\ntheme:\n  custom_dir: overrides\n  favicon: _static/assets/LlamaLogoBrowserTab.png\n  features:\n    - navigation.instant\n    - navigation.indexes\n    - navigation.expand\n    - navigation.top\n    - navigation.footer\n    - toc.follow\n    - content.code.copy\n    - search.suggest\n    - search.highlight\n  logo: _static/assets/LlamaSquareBlack.svg\n  name: material\n  palette:\n    - media: (prefers-color-scheme)\n      toggle:\n        icon: material/brightness-auto\n        name: Switch to light mode\n    - accent: purple\n      media: \"(prefers-color-scheme: light)\"\n      primary: white\n      scheme: default\n      toggle:\n        icon: material/brightness-7\n        name: Switch to dark mode\n    - accent: purple\n      media: \"(prefers-color-scheme: dark)\"\n      primary: black\n      scheme: slate\n      toggle:\n        icon: material/brightness-4\n        name: Switch to system preference\n"
  },
  {
    "path": "docs/api_docs/overrides/main.html",
    "content": "{% extends \"base.html\" %} {% block header %} {{ super() }} {% endblock %}\n"
  },
  {
    "path": "docs/api_docs/overrides/partials/copyright.html",
    "content": "<readthedocs-flyout position=\"bottom-left\"></readthedocs-flyout>\n"
  },
  {
    "path": "docs/api_docs/overrides/partials/search.html",
    "content": "{% import \"partials/language.html\" as lang with context %}\n\n<!-- Search interface -->\n<div id=\"docsearch\"></div>\n"
  },
  {
    "path": "docs/api_docs/pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"docs\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [{name = \"Your Name\", email = \"you@example.com\"}]\nrequires-python = \">=3.10\"\nreadme = \"README.md\"\ndependencies = [\n  \"llama-index-workflows[server,client]\",\n  \"mkdocs>=1.6.1,<2\",\n  \"mkdocstrings[python]>=0.26.1,<1\",\n  \"mkdocs-include-dir-to-nav>=1.2.0,<2\",\n  \"mkdocs-material>=9.5.39,<10\",\n  \"mkdocs-redirects>=1.2.1,<2\",\n  \"mkdocs-click>=0.8.1,<1\",\n  \"mkdocs-render-swagger-plugin>=0.1.2,<1\",\n  \"griffe-fieldz>=0.2.0,<1\",\n  \"mkdocs-github-admonitions-plugin>=0.0.3,<1\",\n  \"pymdown-extensions>=10.21.2\"\n]\n\n[tool.uv]\npackage = false\n\n[tool.uv.sources]\nllama-index-workflows = {workspace = true}\nllama-agents-client = {workspace = true}\n"
  },
  {
    "path": "docs/src/components/Header.astro",
    "content": "---\nimport config from 'virtual:starlight/user-config';\n\nimport LanguageSelect from 'virtual:starlight/components/LanguageSelect';\nimport Search from 'virtual:starlight/components/Search';\nimport SiteTitle from 'virtual:starlight/components/SiteTitle';\nimport SocialIcons from 'virtual:starlight/components/SocialIcons';\nimport ThemeSelect from 'virtual:starlight/components/ThemeSelect';\n\n/**\n * Render the `Search` component if Pagefind is enabled or the default search component has been overridden.\n */\nconst shouldRenderSearch =\n\tconfig.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';\n---\n\n<div class=\"header\">\n\t<div class=\"title-wrapper sl-flex\">\n\t\t<SiteTitle />\n\t</div>\n\t<div class=\"sl-flex print:hidden\">\n\t\t{shouldRenderSearch && <Search />}\n\t</div>\n\t<div class=\"sl-hidden md:sl-flex print:hidden right-group\">\n        <div class=\"sl-flex\">\n            <a href=\"/typescript/framework/\">TypeScript</a>\n        </div>\n\t\t<div class=\"sl-flex social-icons\">\n\t\t\t<SocialIcons />\n\t\t</div>\n\t\t<ThemeSelect />\n\t\t<LanguageSelect />\n\t</div>\n</div>\n\n<style>\n\t@layer starlight.core {\n\t\t.header {\n\t\t\tdisplay: flex;\n\t\t\tgap: var(--sl-nav-gap);\n\t\t\tjustify-content: space-between;\n\t\t\talign-items: center;\n\t\t\theight: 100%;\n\t\t}\n\n\t\t.title-wrapper {\n\t\t\t/* Prevent long titles overflowing and covering the search and menu buttons on narrow viewports. */\n\t\t\toverflow: clip;\n\t\t\t/* Avoid clipping focus ring around link inside title wrapper. */\n\t\t\tpadding: 0.25rem;\n\t\t\tmargin: -0.25rem;\n\t\t\tmin-width: 0;\n\t\t}\n\n\t\t.right-group,\n\t\t.social-icons {\n\t\t\tgap: 1rem;\n\t\t\talign-items: center;\n\t\t}\n\t\t.social-icons::after {\n\t\t\tcontent: '';\n\t\t\theight: 2rem;\n\t\t\tborder-inline-end: 1px solid var(--sl-color-gray-5);\n\t\t}\n\n\t\t@media (min-width: 50rem) {\n\t\t\t:global(:root[data-has-sidebar]) {\n\t\t\t\t--__sidebar-pad: calc(2 * var(--sl-nav-pad-x));\n\t\t\t}\n\t\t\t:global(:root:not([data-has-toc])) {\n\t\t\t\t--__toc-width: 0rem;\n\t\t\t}\n\t\t\t.header {\n\t\t\t\t--__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));\n\t\t\t\t--__main-column-fr: calc(\n\t\t\t\t\t(\n\t\t\t\t\t\t\t100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) -\n\t\t\t\t\t\t\t\t(2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) -\n\t\t\t\t\t\t\t\tvar(--sl-content-width)\n\t\t\t\t\t\t) / 2\n\t\t\t\t);\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns:\n        /* 1 (site title): runs up until the main content column’s left edge or the width of the title, whichever is the largest  */\n\t\t\t\t\tminmax(\n\t\t\t\t\t\tcalc(var(--__sidebar-width) + max(0rem, var(--__main-column-fr) - var(--sl-nav-gap))),\n\t\t\t\t\t\tauto\n\t\t\t\t\t)\n\t\t\t\t\t/* 2 (search box): all free space that is available. */\n\t\t\t\t\t1fr\n\t\t\t\t\t/* 3 (right items): use the space that these need. */\n\t\t\t\t\tauto;\n\t\t\t\talign-content: center;\n\t\t\t}\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "docs/src/components/HomepageFeatures/index.js",
    "content": "import clsx from 'clsx';\nimport Heading from '@theme/Heading';\nimport styles from './styles.module.css';\n\nconst FeatureList = [\n  {\n    title: 'Easy to Use',\n    Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,\n    description: (\n      <>\n        Docusaurus was designed from the ground up to be easily installed and\n        used to get your website up and running quickly.\n      </>\n    ),\n  },\n  {\n    title: 'Focus on What Matters',\n    Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,\n    description: (\n      <>\n        Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go\n        ahead and move your docs into the <code>docs</code> directory.\n      </>\n    ),\n  },\n  {\n    title: 'Powered by React',\n    Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,\n    description: (\n      <>\n        Extend or customize your website layout by reusing React. Docusaurus can\n        be extended while reusing the same header and footer.\n      </>\n    ),\n  },\n];\n\nfunction Feature({Svg, title, description}) {\n  return (\n    <div className={clsx('col col--4')}>\n      <div className=\"text--center\">\n        <Svg className={styles.featureSvg} role=\"img\" />\n      </div>\n      <div className=\"text--center padding-horiz--md\">\n        <Heading as=\"h3\">{title}</Heading>\n        <p>{description}</p>\n      </div>\n    </div>\n  );\n}\n\nexport default function HomepageFeatures() {\n  return (\n    <section className={styles.features}>\n      <div className=\"container\">\n        <div className=\"row\">\n          {FeatureList.map((props, idx) => (\n            <Feature key={idx} {...props} />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/HomepageFeatures/styles.module.css",
    "content": ".features {\n  display: flex;\n  align-items: center;\n  padding: 2rem 0;\n  width: 100%;\n}\n\n.featureSvg {\n  height: 200px;\n  width: 200px;\n}\n"
  },
  {
    "path": "docs/src/components/ProtectedContent.jsx",
    "content": "import React, { useState, useEffect } from 'react';\n\nconst SELF_HOSTING_PASSWORD = 'llamacloud-self-host-2025';\nconst STORAGE_KEY = 'llamacloud-self-hosting-auth';\n\nexport default function ProtectedContent({ children }) {\n  const [isAuthenticated, setIsAuthenticated] = useState(false);\n  const [password, setPassword] = useState('');\n  const [error, setError] = useState('');\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    // Check if user is already authenticated\n    const savedAuth = localStorage.getItem(STORAGE_KEY);\n    if (savedAuth === 'true') {\n      setIsAuthenticated(true);\n    }\n    setIsLoading(false);\n  }, []);\n\n  const handleSubmit = (e) => {\n    e.preventDefault();\n    if (password === SELF_HOSTING_PASSWORD) {\n      setIsAuthenticated(true);\n      localStorage.setItem(STORAGE_KEY, 'true');\n      setError('');\n    } else {\n      setError('Incorrect password. Please try again.');\n      setPassword('');\n    }\n  };\n\n  const handleLogout = () => {\n    setIsAuthenticated(false);\n    localStorage.removeItem(STORAGE_KEY);\n    setPassword('');\n  };\n\n  if (isLoading) {\n    return <div>Loading...</div>;\n  }\n\n  if (!isAuthenticated) {\n    return (\n      <div style={{\n        maxWidth: '400px',\n        margin: '2rem auto',\n        padding: '2rem',\n        border: '1px solid #e0e0e0',\n        borderRadius: '8px',\n        backgroundColor: '#f9f9f9'\n      }}>\n        <h2 style={{ textAlign: 'center', marginBottom: '1.5rem' }}>\n          Self-Hosting Documentation Access\n        </h2>\n        <p style={{ textAlign: 'center', marginBottom: '1.5rem', color: '#666' }}>\n          This section requires a password to access.\n          Interested in self-hosting? <a href=\"https://www.llamaindex.ai/contact\">Contact sales</a> to learn more.\n        </p>\n        <form onSubmit={handleSubmit}>\n          <div style={{ marginBottom: '1rem' }}>\n            <label htmlFor=\"password\" style={{ display: 'block', marginBottom: '0.5rem' }}>\n              Password:\n            </label>\n            <input\n              type=\"password\"\n              id=\"password\"\n              value={password}\n              onChange={(e) => setPassword(e.target.value)}\n              style={{\n                width: '100%',\n                padding: '0.5rem',\n                border: '1px solid #ccc',\n                borderRadius: '4px',\n                fontSize: '1rem'\n              }}\n              required\n            />\n          </div>\n          {error && (\n            <p style={{ color: 'red', fontSize: '0.9rem', marginBottom: '1rem' }}>\n              {error}\n            </p>\n          )}\n          <button\n            type=\"submit\"\n            style={{\n              width: '100%',\n              padding: '0.75rem',\n              backgroundColor: '#007cba',\n              color: 'white',\n              border: 'none',\n              borderRadius: '4px',\n              fontSize: '1rem',\n              cursor: 'pointer'\n            }}\n          >\n            Access Documentation\n          </button>\n        </form>\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <div style={{\n        textAlign: 'right',\n        marginBottom: '1rem',\n        padding: '0.5rem',\n        backgroundColor: '#f0f0f0',\n        borderRadius: '4px'\n      }}>\n        <small style={{ marginRight: '1rem', color: '#666' }}>\n          Self-Hosting Documentation Access Granted\n        </small>\n        <button\n          onClick={handleLogout}\n          style={{\n            padding: '0.25rem 0.5rem',\n            backgroundColor: '#dc3545',\n            color: 'white',\n            border: 'none',\n            borderRadius: '3px',\n            fontSize: '0.8rem',\n            cursor: 'pointer'\n          }}\n        >\n          Logout\n        </button>\n      </div>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/ProtectedTabs.jsx",
    "content": "const ProtectedTabs = ({ children }) => {\n  return (\n    <div className=\"tabs-container\">\n      <style jsx>{`\n        .tabs-container {\n          margin: 1rem 0;\n        }\n\n        .tabs-radio {\n          display: none;\n        }\n\n        .tabs-labels {\n          display: flex;\n          gap: 0.25rem;\n          border-bottom: 1px solid #374151;\n          margin-bottom: 1rem;\n        }\n\n        .tab-label {\n          padding: 0.5rem 0.5rem;\n          font-weight: 500;\n          font-size: 1rem;\n          position: relative;\n          background: none;\n          border: none;\n          cursor: pointer;\n          color: #9ca3af;\n          transition: color 0.2s;\n          text-align: center;\n          min-width: 0;\n          flex: 1;\n          white-space: normal;\n          word-break: break-word;\n        }\n\n        .tab-label:hover {\n          color: #d1d5db;\n        }\n\n        .tabs-radio:checked + .tab-label {\n          color: #1f2937;\n          font-weight: 600;\n        }\n\n        html[data-theme='dark'] .tabs-radio:checked + .tab-label {\n          color: white;\n        }\n\n        .tabs-radio:checked + .tab-label::after {\n          content: '';\n          position: absolute;\n          bottom: 0;\n          left: 0;\n          right: 0;\n          height: 2px;\n          background-color: #a855f7;\n        }\n\n        .tabs-content {\n          padding: 0.5rem 0;\n        }\n\n        .tab-item {\n          display: none;\n        }\n\n        .tabs-labels:has(#tab-0:checked) ~ .tabs-content .tab-item:nth-child(1),\n        .tabs-labels:has(#tab-1:checked) ~ .tabs-content .tab-item:nth-child(2),\n        .tabs-labels:has(#tab-2:checked) ~ .tabs-content .tab-item:nth-child(3),\n        .tabs-labels:has(#tab-3:checked) ~ .tabs-content .tab-item:nth-child(4) {\n          display: block;\n        }\n      `}</style>\n\n      <div className=\"tabs-labels\">\n        <input type=\"radio\" id=\"tab-0\" name=\"tabs\" className=\"tabs-radio\" defaultChecked />\n        <label htmlFor=\"tab-0\" className=\"tab-label\">OpenAI w/ OIDC Auth</label>\n\n        <input type=\"radio\" id=\"tab-1\" name=\"tabs\" className=\"tabs-radio\" />\n        <label htmlFor=\"tab-1\" className=\"tab-label\">Azure OpenAI w/ OIDC Auth</label>\n\n        <input type=\"radio\" id=\"tab-2\" name=\"tabs\" className=\"tabs-radio\" />\n        <label htmlFor=\"tab-2\" className=\"tab-label\">OpenAI w/ Basic Auth</label>\n\n        <input type=\"radio\" id=\"tab-3\" name=\"tabs\" className=\"tabs-radio\" />\n        <label htmlFor=\"tab-3\" className=\"tab-label\">Azure OpenAI w/ Basic Auth</label>\n      </div>\n\n      <div className=\"tabs-content\">\n        {children}\n      </div>\n    </div>\n  );\n};\n\nconst ProtectedTabItem = ({ label, value, children }) => {\n  return <div className=\"tab-item\">{children}</div>;\n};\n\nexport { ProtectedTabs, ProtectedTabItem };\n"
  },
  {
    "path": "docs/src/components/SiteTitle.astro",
    "content": "---\nimport { logos } from 'virtual:starlight/user-images';\nimport config from 'virtual:starlight/user-config';\nconst { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;\n---\n\n<a href=\"/\" class=\"site-title sl-flex\">\n\t{\n\t\tconfig.logo && logos.dark && (\n\t\t\t<>\n\t\t\t\t<img\n\t\t\t\t\tclass:list={{ 'light:sl-hidden print:hidden': !('src' in config.logo) }}\n\t\t\t\t\talt={config.logo.alt}\n\t\t\t\t\tsrc={logos.dark.src}\n\t\t\t\t\twidth={logos.dark.width}\n\t\t\t\t\theight={logos.dark.height}\n\t\t\t\t/>\n\t\t\t\t{/* Show light alternate if a user configure both light and dark logos. */}\n\t\t\t\t{!('src' in config.logo) && (\n\t\t\t\t\t<img\n\t\t\t\t\t\tclass=\"dark:sl-hidden print:block\"\n\t\t\t\t\t\talt={config.logo.alt}\n\t\t\t\t\t\tsrc={logos.light?.src}\n\t\t\t\t\t\twidth={logos.light?.width}\n\t\t\t\t\t\theight={logos.light?.height}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</>\n\t\t)\n\t}\n\t<span class:list={{ 'sr-only': config.logo?.replacesTitle }} translate=\"no\">\n\t\t{siteTitle}\n\t</span>\n</a>\n\n<style>\n\t@layer starlight.core {\n\t\t.site-title {\n\t\t\talign-items: center;\n\t\t\tgap: var(--sl-nav-gap);\n\t\t\tfont-size: var(--sl-text-h4);\n\t\t\tfont-weight: 600;\n\t\t\tcolor: var(--sl-color-text-accent);\n\t\t\ttext-decoration: none;\n\t\t\twhite-space: nowrap;\n\t\t\tmin-width: 0;\n\t\t}\n\t\tspan {\n\t\t\toverflow: hidden;\n\t\t}\n\t\timg {\n\t\t\theight: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));\n\t\t\twidth: auto;\n\t\t\tmax-width: 100%;\n\t\t\tobject-fit: contain;\n\t\t\tobject-position: 0 50%;\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "docs/src/components/llamaExtract.js",
    "content": "import React from 'react';\n\nexport const LEPython = ({children}) => (\n  <>\n    <span><small>In Python:</small></span>\n    <pre>extractor = LlamaExtract(<br/>\n    &nbsp;&nbsp;{children}<br/>\n    )</pre>\n  </>\n)\n\nexport const LEAPI = ({children, endpoint = \"\", isUpload=false, outputFile=false}) => {\n  if (!endpoint) endpoint = \"parsing/upload\"\n  let outputLine = <></>\n  if (outputFile) outputLine = <>\n    &nbsp;\\<br/>&nbsp;&nbsp;--output \"file.png\"\n  </>\n  let uploadLine = <></>\n  if (isUpload) uploadLine = <>\n    &nbsp;\\<br/>&nbsp;&nbsp;-F 'file=@/path/to/your/file.pdf;type=application/pdf'\n  </>\n  let paramList = <></>\n  if(typeof children == \"string\") {\n    let entries = children.split(\"|\")\n    paramList = entries.map((line) => {\n      return <>&nbsp;\\<br/>&nbsp;&nbsp;--form '{line}'</>\n    })\n  }\n  return (\n    <>\n      <span><small>Using the API:</small></span>\n      <pre>curl -X 'POST' \\<br/>\n      &nbsp;&nbsp;'https://api.cloud.llamaindex.ai/api/{endpoint}' &nbsp;\\<br/>\n      &nbsp;&nbsp;-H 'accept: application/json' \\<br/>\n      &nbsp;&nbsp;-H 'Content-Type: multipart/form-data' \\<br/>\n      &nbsp;&nbsp;-H \"Authorization: Bearer $LLAMA_CLOUD_API_KEY\"\n      {paramList}\n      {uploadLine}\n      {outputLine}</pre>\n    </>\n  )\n}\n"
  },
  {
    "path": "docs/src/components/llamaParse.js",
    "content": "import React from 'react';\n\nexport const LPPython = ({children}) => (\n  <>\n    <span><small>In Python:</small></span>\n    <pre>parser = LlamaParse(<br/>\n    &nbsp;&nbsp;{children}<br/>\n    )</pre>\n  </>\n)\n\nexport const LPAPI = ({children, endpoint = \"\", isUpload=false, outputFile=false}) => {\n  if (!endpoint) endpoint = \"upload\"\n  let outputLine = <></>\n  if (outputFile) outputLine = <>\n    &nbsp;\\<br/>&nbsp;&nbsp;--output \"file.png\"\n  </>\n  let uploadLine = <></>\n  if (isUpload) uploadLine = <>\n    &nbsp;\\<br/>&nbsp;&nbsp;-F 'file=@/path/to/your/file.pdf;type=application/pdf'\n  </>\n  let paramList = <></>\n  if(typeof children !== \"undefined\") {\n    // Get the raw text content of children\n    let rawContent = \"\";\n    if (typeof children === \"string\") {\n      rawContent = children;\n    } else {\n      // For React elements, get their text content\n      // this is because React interprets key-value pairs with URLs as prop values\n      rawContent = React.Children.toArray(children)\n        .map(child => {\n          if (typeof child === \"string\") return child;\n          if (React.isValidElement(child)) return child.props.children || \"\";\n          return \"\";\n        })\n        .join(\"\");\n    }\n\n    // Split by | if multiple parameters\n    const entries = rawContent.split(\"|\");\n\n    paramList = entries.map((line) => {\n      return <>&nbsp;\\<br/>&nbsp;&nbsp;--form '{line.trim()}'</>\n    });\n  }\n\n  return (\n    <>\n      <span><small>Using the API:</small></span>\n      <pre>curl -X 'POST' \\<br/>\n      &nbsp;&nbsp;'https://api.cloud.llamaindex.ai/api/v1/parsing/{endpoint}' &nbsp;\\<br/>\n      &nbsp;&nbsp;-H 'accept: application/json' \\<br/>\n      &nbsp;&nbsp;-H 'Content-Type: multipart/form-data' \\<br/>\n      &nbsp;&nbsp;-H \"Authorization: Bearer $LLAMA_CLOUD_API_KEY\"\n      {paramList}\n      {uploadLine}\n      {outputLine}</pre>\n    </>\n  )\n}\n"
  },
  {
    "path": "docs/src/components/mdx_components.jsx",
    "content": "import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\nimport { Children, cloneElement } from 'react';\n\n\nexport const StyledTitle = ({ title, subtitle }) => {\n    return (\n      <div style={{ marginBottom: '1rem' }}>\n        <h3 style={{ marginBottom: '0.25rem' }}>{title}</h3>\n        {subtitle && <span style={{ fontSize: '0.875rem', opacity: 0.6 }}>{subtitle}</span>}\n      </div>\n    );\n    }\nexport const StyledTab = ({ children }) => {\n  return (\n    <Tabs>\n      {Children.map(children, (child) => {\n        if (child.type !== TabItem) return child;\n\n        return cloneElement(child, {\n          children: (\n            <div style={{ fontSize: '0.875rem' }}>\n              {child.props.children}\n            </div>\n          ),\n        });\n      })}\n    </Tabs>\n  );\n};\n\nexport const ImageSizer = ({ children, width = '350px' }) => {\n  return (\n    <div style={{ width, margin: '0' }}>\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/_meta.yml",
    "content": "label: Agents\ncollapsed: true\nhidden: false\norder: 5.5\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/cloud/_meta.yml",
    "content": "label: Cloud\ncollapsed: true\nhidden: false\norder: 2\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/cloud/agent-data-overview.md",
    "content": "---\ntitle: Agent Data\nsidebar:\n  order: 30\n---\n\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\n\n### What is Agent Data?\n\nSkip the database setup. LlamaAgents workflows and JavaScript UIs share a persistent Agent Data store built into the LlamaCloud API. It uses the same authentication as the rest of the API.\n\nAgent Data is a queryable store for JSON records produced by your agents. Each record is linked to a `deployment_name` (the deployed agent) and an optional `collection` (a logical bucket; defaults to `default`). Use it to persist extractions, events, metrics, and other structured output, then search and aggregate across records.\n\nKey concepts:\n- **deployment_name**: the identifier of the agent deployment the data belongs to. Access is authorized against that agent's project.\n- **collection**: a logical namespace within an agent for organizing different data types or apps. Storage is JSON. We recommend storing homogeneous data types within a single collection.\n- **data**: the JSON payload shaped by your app. SDKs provide typed wrappers.\n\nImportant behavior and constraints:\n- **Deployment required**: The `deployment_name` must correspond to an existing deployment. Data is associated with that deployment and its project.\n- **Local development**: When running locally, omit `deployment_name` to use the shared `_public` Agent Data store. Use distinct `collection` names to separate apps during local development.\n- **Access control**: You can only read/write data for agents in projects you can access. `_public` data is visible across agents within the same project.\n\n### SDK Reference\n\nFor CRUD operations, search, filtering, sorting, aggregation, and deletion, see the generated SDK reference:\n\n**[Agent Data API Reference](https://developers.llamaindex.ai/reference/resources/beta/subresources/agent_data/)**\n\nThe reference covers all available operations:\n- **Create / Get / Update / Delete** individual records\n- **Search** with filtering, sorting, and pagination\n- **Aggregate** by grouping fields with counts and first-item retrieval\n- **Delete by query** for bulk deletion using the filter DSL\n\nSDK packages:\n- **Python**: [`llama-cloud`](https://pypi.org/project/llama-cloud/) (`pip install 'llama-cloud>=1'`)\n- **JavaScript**: [`@llamaindex/llama-cloud`](https://www.npmjs.com/package/@llamaindex/llama-cloud) (`npm install @llamaindex/llama-cloud`)\n\n### ExtractedData wrapper\n\n`ExtractedData` is a specialized wrapper type available in the Python SDK (`llama-cloud`) and the JavaScript UI library (`@llamaindex/ui`). It is not part of the generated API reference, so it is documented here.\n\n`ExtractedData[T]` is designed for extraction workflows where data goes through review and approval stages. Use it as the type parameter when storing extraction results in Agent Data.\n\n**Fields:**\n\n| Field | Description |\n|-------|-------------|\n| `original_data` | The data as originally extracted (preserved for change tracking) |\n| `data` | The current state of the data (updated by human review) |\n| `status` | Workflow status: `pending_review`, `accepted`, `rejected`, `error`, or custom string |\n| `overall_confidence` | Aggregated confidence score (auto-calculated from field_metadata) |\n| `field_metadata` | Dict mapping field paths to metadata including confidence scores and citations |\n| `file_id` | LlamaCloud file ID of the source document |\n| `file_name` | Name of the source file |\n| `file_hash` | Content hash for deduplication |\n| `metadata` | Additional application-specific metadata |\n\n**Python usage:**\n\n```python\nfrom pydantic import BaseModel\nfrom llama_cloud.types.beta.extracted_data import ExtractedData\n\nclass Invoice(BaseModel):\n    vendor: str | None = None\n    total: float | None = None\n    date: str | None = None\n```\n\nUse the `client.beta.agent_data` resource to store `ExtractedData` records. The data is serialized as a JSON dict matching the `ExtractedData` shape.\n\n**Creating from an extraction job:**\n\nThe `from_extract_job` factory method creates an `ExtractedData` instance directly from a completed `ExtractV2Job`, automatically capturing field metadata (confidence scores, citations):\n\n```python\nfrom llama_cloud.types.beta.extracted_data import ExtractedData\n\nextracted = ExtractedData.from_extract_job(\n    job=extract_job,\n    schema=Invoice,\n)\n\nawait client.beta.agent_data.create(\n    data=extracted.model_dump(),\n    deployment_name=deployment_name,\n    collection=\"invoices\",\n)\n```\n\n**Creating manually:**\n\nUse `ExtractedData.create` when constructing extracted data from other sources or transforming to a different schema:\n\n```python\nfrom llama_cloud.types.beta.extracted_data import ExtractedData\n\ninvoice = Invoice(vendor=\"Acme Corp\", total=1500.00, date=\"2024-01-15\")\n\nextracted = ExtractedData.create(\n    data=invoice,\n    status=\"pending_review\",\n    file_id=\"file-abc123\",\n    file_name=\"invoice.pdf\",\n    file_hash=\"sha256:...\",\n    field_metadata={\n        \"vendor\": {\"confidence\": 0.95, \"citation\": [{\"page\": 1, \"matching_text\": \"Acme Corp\"}]},\n        \"total\": {\"confidence\": 0.92},\n    },\n)\n```\n\n**JavaScript / TypeScript usage:**\n\nIn `@llamaindex/ui`, `ExtractedData` is available as a TypeScript type for use in UI components that display extraction results with review workflows.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/cloud/builder.md",
    "content": "---\ntitle: Agent Builder\nsidebar:\n  order: 10\n---\n\nAgent Builder is a natural language interface for creating document workflows in LlamaCloud. Describe what you want to extract from your documents in plain English, and an AI coding agent generates a complete workflow you can deploy with a few clicks. The generated code is yours. It's a real Python project in your GitHub repository that you can customize, extend, or deploy on your own infrastructure.\n\n![Agent Builder UI](./assets/agent-builder.jpg)\n\n## How It Works\n\nAgent Builder transforms your descriptions into working document pipelines:\n\n1. **Describe** your extraction needs in plain English\n2. **Review** the generated workflow code and visual graph\n3. **Deploy** to LlamaCloud, or take the code and run it on your own infrastructure\n\nThe agent understands LlamaCloud services and can configure extraction schemas, classification rules, and multi-step pipelines through conversation.\n\n<div style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; margin-bottom: 1.5rem;\">\n  <iframe\n    style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%;\" src=\"https://www.youtube.com/embed/0Zhf5z2Onjs\" title=\"LlamaAgents overview\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen></>\n</div>\n\n## Building Your First Workflow\n\n### Start a New Session\n\nFrom the LlamaCloud dashboard, navigate to **Agents** and click **Start Building**. This opens the chat interface where you'll describe your workflow.\n\n### Describe What You Want\n\nTell the agent what documents you have and what data you want to extract. Be specific about your needs:\n\n> \"I have SEC 10-K filings and want to extract revenue, net income, and risk factors\"\n\n> \"Extract line items, totals, and vendor information from invoices\"\n\n> \"Given court documents, classify them as complaints, motions, or orders, then extract different fields based on type\"\n\nThe agent will ask clarifying questions if it needs more detail about your requirements.\n\n### Watch Your Workflow Take Shape\n\nAs the agent works, you'll see real-time activity showing files being created and modified, along with a visual graph of the pipeline steps. Once generation completes, you can continue chatting to refine it:\n\n- Adjust the extraction schema to capture additional fields\n- Add validation rules to check extracted data\n- Change how documents are classified\n- Modify any aspect of the workflow\n\nFor details on extraction schemas and configuration, see [LlamaExtract documentation](/python/cloud/llamaextract/getting_started).\n\n:::tip[Experiment Freely]\nChanges you make in the agent session don't automatically update an existing deployment. Your deployed workflow remains stable until you explicitly push the changes to GitHub and redeploy. This means you can experiment with different approaches, test new extraction fields, or iterate on your workflow without affecting production.\n:::\n\n## Deploying Your Workflow\n\nWhen you're satisfied with your workflow, click **Deploy** to make it live. The deployment process connects your workflow to GitHub and deploys it to LlamaCloud.\n\n:::note\nDeployment requires a GitHub account. Your workflow code will be stored in a GitHub repository, enabling version control and future customization.\n:::\n\n### Step 1: Connect GitHub\n\nClick **Connect GitHub** to authorize LlamaCloud to access your GitHub account. This uses standard GitHub OAuth and allows LlamaCloud to create repositories on your behalf.\n\n### Step 2: Create or Select a Repository\n\nChoose where to store your workflow code:\n\n- **Create new repository**: LlamaCloud creates a new repo in your account with your workflow code\n- **Select existing**: Choose an existing repository (useful for updates or adding to an existing project)\n\n### Step 3: Install the LlamaCloud GitHub App\n\n:::info\nThis is a separate step from GitHub user authentication. The GitHub App grants LlamaCloud's deployment infrastructure access to your organization or repository.\n:::\n\nYou'll be prompted to install the **LlamaCloud GitHub App** on your repository. This grants LlamaCloud permission to:\n\n- Read your repository contents\n- Deploy updates when you push changes\n\n**Why both OAuth and the GitHub App?** OAuth lets you authorize actions as yourself (like creating repositories). The GitHub App lets LlamaCloud's deployment infrastructure autonomously access your repo independently to build and deploy your workflow.\n\n### Step 4: Configure and Deploy\n\nReview the deployment configuration:\n\n- **Environment variables**: Add any required API keys (e.g., `OPENAI_API_KEY` for LLM-powered steps)\n- **Deployment name**: A unique identifier for this deployment\n\nClick **Deploy**. LlamaCloud will build and deploy your workflow.\n\n### Your Workflow is Live\n\nOnce deployment status shows **Running**, your workflow is ready to use:\n\n- Click **Visit** to open the workflow's web interface\n- Upload documents to process them through your pipeline\n- View extracted data and results\n\n## After Deployment\n\n### Customizing Your Workflow Code\n\nYour workflow is a real Python project using the open-source [Workflows](/python/llamaagents/workflows/index) framework. You can customize it beyond what the chat interface provides:\n\n1. Clone the repository locally\n2. Edit the workflow code directly\n3. Push changes to GitHub\n4. Update the deployment to pull your changes\n\nFor details on project structure, see [Configuration Reference](/python/llamaagents/llamactl/configuration-reference).\n\n### Updating a Deployment\n\nDeployments are intentionally decoupled from Agent Builder sessions. When you continue chatting with the agent and make changes, those changes are saved to your repository but **won't affect your running deployment** until you explicitly update it.\n\nTo deploy your latest changes:\n\n1. Push your code changes to GitHub (either from Agent Builder or your local clone)\n2. Go to your deployment in LlamaCloud\n3. Click the **...** menu and select **Update Version**\n4. LlamaCloud pulls and deploys your latest code\n\nThis separation lets you iterate and experiment in Agent Builder while keeping your production deployment stable.\n\n### Managing Deployments\n\nFrom the LlamaCloud dashboard, you can:\n\n- **Rollback** to a previous version if something breaks\n- **Edit settings** to change the repository or branch\n- **Delete** deployments you no longer need\n\nFor full deployment management details, see [Click-to-Deploy](/python/llamaagents/llamactl/click-to-deploy).\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/cloud/click-to-deploy.md",
    "content": "---\ntitle: Click-to-Deploy from LlamaCloud\nsidebar:\n  order: 20\n---\n\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\n\nLlamaAgents allows you to deploy document workflow agents directly from the [LlamaCloud UI](https://cloud.llamaindex.ai/) with a single click. Choose a pre-built starter template, configure secrets, and deploy—no command line required.\n\n## Get started with starter templates\n\nFrom the LlamaCloud dashboard, navigate to **Agents** in your project. If you have no deployments, you'll see a \"Jumpstart your Agent\" section showing available starter templates.\n\nEach starter template is a complete, working document workflow application that demonstrates how to combine LlamaCloud's document primitives—[Parse](/python/cloud/llamaparse/getting_started), [Extract](/python/cloud/llamaextract/getting_started), and [Classify](/python/cloud/llamaclassify/getting_started)—into a multi-step pipeline.\n\n### Available starters\n\n| Template | Description | Key Features |\n|----------|-------------|--------------|\n| **SEC Insights** | Classify financial PDFs and extract structured insights | Parse → Classify → Extract pipeline for SEC filings |\n| **Invoice + Contract Matching** | Parse invoices and match with contracts, identifying discrepancies | Multi-document reconciliation workflow |\n\n### Deploy a starter\n\n1. Click on a starter template card to open the deployment dialog\n2. Enter a **name** for your deployment (letters, numbers, and dashes only)\n3. If the starter requires API keys (e.g., `OPENAI_API_KEY`), enter them in the **Required secrets** section\n4. Click **Deploy**\n\nLlamaCloud will clone the template repository, build your application, and deploy it. This typically takes 1–3 minutes. Once deployed, your agent will appear in the deployments list with its status.\n\n### View your agent\n\nOnce deployment status shows **Running**, click **Visit** to open your agent's UI. Most starters include a web interface where you can:\n\n- Upload documents for processing\n- View extracted data and classifications\n- Review and correct results (for extraction-review workflows)\n\nMany starters also include sample data files. Click **Example Data** on the deployment card to download test documents.\n\n## Customize your deployment\n\nStarter templates are fully customizable. To modify the workflow logic, UI, or configuration:\n\n### Fork and edit\n\n1. Click **Customize** on the deployment card, or select **Edit** from the dropdown menu\n2. Follow the link to **fork the repository on GitHub**\n3. Make your changes in your forked repository\n4. Update the deployment's **Repository URL** to point to your fork\n5. Click **Update** to redeploy with your changes\n\n### What you can customize\n\nEvery LlamaAgents deployment is a standard Python project with:\n\n- **Workflows** (`src/`): LlamaIndex Workflow definitions using Parse, Extract, Classify, and other LlamaCloud services\n- **UI** (`ui/`): React frontend using `@llamaindex/ui` hooks\n- **Configuration** (`pyproject.toml`): Workflow registration, environment settings, and build configuration\n\nFor details on the project structure, see [Configuration Reference](/python/llamaagents/llamactl/configuration-reference).\n\n## Manage deployments\n\nAfter deploying, you can manage your agent from the LlamaCloud UI:\n\n### Update to latest version\n\nIf you've pushed changes to your repository:\n1. Click the **⋮** menu on the deployment card\n2. Select **Update Version**\n3. Confirm to pull and deploy the latest commit from your configured branch\n\n### Rollback\n\nIf a deployment update causes issues:\n1. Click the **⋮** menu and select **Rollback**\n2. Choose a previous release from the history list\n3. Click **Rollback** to restore that version\n\n### Edit settings\n\nTo change the source repository or branch:\n1. Select **Edit** from the deployment menu\n2. Update the **Repository URL** or **Branch**\n3. Click **Update**\n\n:::note\nPrivate repositories require installing the [LlamaAgents GitHub App](https://github.com/apps/llama-deploy). You'll be prompted to install it when configuring a private repository.\n:::\n\n### Delete\n\nTo remove a deployment:\n1. Select **Delete** from the deployment menu\n2. Confirm deletion\n\n:::warning\nDeleting a deployment is permanent. All associated resources and data will be removed.\n:::\n\n## Next steps\n\nReady to dive into the code? Learn how to author and configure workflows in [Serving your Workflows](/python/llamaagents/llamactl/workflow-api).\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/_meta.yml",
    "content": "label: llamactl\ncollapsed: true\nhidden: false\norder: 3\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/agent-templates.md",
    "content": "---\ntitle: Agent Templates\nsidebar:\n  order: 2\n---\n\nWe provide a set of document agent templates, both as full-stack apps with a UI, and as headless servers. These templates aim to cover a number of most common use-cases, and are intended as a starting point for your own custom agents.\n\nYou can pull these templates via our [llamactl CLI](/python/llamaagents/llamactl/getting-started/), which will create a local repository for your agent project, equipped with all the source-code and UI elements.\n\n> This list of agents is not final, expect changes and updates along the way.\n\n### Available LlamaAgent Templates\n\nThere are two groups of templates, with and without UI elements. You can see the most up-to-date list of templates by [initializing an agent project with `llamactl`](/python/llamaagents/llamactl/getting-started/#initialize-a-project).\n\nBelow is a full list of templates available via the `llamactl` CLI. We also provide [Click to Deploy templates from within LlamaCloud](python/llamaagents/llamactl/click-to-deploy/).\n\n| **Template With UI**  | |\n| --- | --- |\n| **Template Name** | **Description** |\n| Basic UI  | A minimal starting point for building an event-driven, async-first agent with UI using LlamaAgent workflows and LlamaDeploy. Includes a Vite app that calls the local agent workflow server via API requests. |\n| Showcase | A showcase application demonstrating different agent workflow patterns and capabilities with a full-stack setup. Includes agent workflow sources and a Vite UI app that communicates with the workflow server via API requests.  |\n| Document Q&A | A document question-answering application built with LlamaAgent workflows and LlamaCloud services like the LlamaCloud Index. Allows users to upload documents and ask questions about their content.  |\n| Extraction Agent with Review UI | A data extraction and ingestion application with a review UI for validating and editing extracted data. Uses LlamaExtract agents to extract structured data from documents, stores results in Agent Data, and provides a dynamic UI that generates editing interfaces based on the extraction schema. |\n| Invoice Extraction & Reconciliation | Extracts structured data from invoices and reconciles it against contract documents using LlamaExtract and LlamaCloud Index. Helps finance and operations teams validate that incoming invoices comply with agreed contract terms by automatically detecting mismatches in payment terms, totals, and other key fields. |\n| **Template with Headless Workflows  (No UI)**  | |\n| **Template Name** | **Description** |\n| Basic Workflow | A minimal starting point for building an event-driven, async-first agent using LlamaAgent workflows. Provides a simple hello-world example that can be extended with custom steps and logic.  |\n| Document Parser | A document parsing agent template for processing and extracting information from complex documents using LlamaAgent workflows and LlamaParse.  |\n| Human in the Loop | A LlamaAgent workflow template that incorporates human-in-the-loop interactions, allowing agents to pause and wait for human input or approval at specific steps. |\n| Invoice Extraction | An agent template for extracting structured data from invoices using LlamaIndex workflows and extraction agents. |\n| RAG | A Retrieval-Augmented Generation (RAG) agent template for building question-answering systems that retrieve relevant context from indexed documents before generating responses.  |\n| Web Scraping | A web scraping agent workflow template for extracting data from web pages using LlamaIndex workflows and web scraping capabilities.  |\n\n### Coding Agent Support Files\n\nEach template downloaded/cloned with `llamactl` will also come with coding agent files like `CLAUDE.md`, `GEMINI.md` and `AGENTS.md`, designed to help developing and customizing LlamaAgents with the assistance of coding agents simpler. These files contain all the relevant context and information that coding agents such as Cursor, Claude Code etc might need.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/cd-with-github-actions.md",
    "content": "---\ntitle: Continuous Deployment with GitHub Actions\nsidebar:\n  order: 19\n---\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\n\nUse `llamactl deployments apply -f` in GitHub Actions when you want a deployment spec in source control and an automated update on every push.\n\n## Required Secrets and Variables\n\n`llamactl` can authenticate from environment variables, so CI does not need a stored profile:\n\n- `LLAMA_CLOUD_API_KEY`: store as a GitHub Actions secret.\n- `LLAMA_AGENTS_PROJECT_ID`: store as a GitHub Actions variable or secret.\n\nIf you deploy to a non-default LlamaCloud environment, also set `LLAMA_CLOUD_BASE_URL`.\n\n## Deployment Spec\n\nKeep a deployment spec in your repository, for example `deployment.yaml`:\n\n```yaml\nname: my-agent\nspec:\n  repo_url: https://github.com/${GITHUB_REPOSITORY}\n  deployment_file_path: \".\"\n  git_ref: ${GITHUB_SHA}\n  secrets:\n    OPENAI_API_KEY: ${OPENAI_API_KEY}\n```\n\n`deployments apply` resolves `${VAR}` references from the process environment before sending the deployment to LlamaCloud. That lets the same file use GitHub-provided values like `GITHUB_SHA` and secrets injected by the workflow.\n\nUse a stable `name` if the workflow should update the same deployment every run. If `name` is omitted and the spec uses `generate_name`, each apply can create a new deployment.\n\n## Workflow Example\n\n```yaml\nname: Deploy LlamaAgents app\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    env:\n      LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}\n      LLAMA_AGENTS_PROJECT_ID: ${{ vars.LLAMA_AGENTS_PROJECT_ID }}\n      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Apply deployment\n        run: uvx llamactl deployments apply -f deployment.yaml --annotate-on-error\n```\n\n`--annotate-on-error` writes validation and API errors back into the YAML input so the Actions log points at the field that failed. The command exits non-zero on failure.\n\n## Capturing Status Output\n\n`llamactl` keeps machine-readable output on stdout and status messages on stderr. If a workflow parses status text from `deployments apply`, capture stderr explicitly:\n\n```bash\nstatus=\"$(uvx llamactl deployments apply -f deployment.yaml --annotate-on-error 2>&1 >/dev/null)\"\nprintf '%s\\n' \"$status\"\n```\n\nPrefer a stable `name` in `deployment.yaml` over parsing the status message when downstream steps need the deployment name.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/configuration-reference.md",
    "content": "---\ntitle: Deployment Config Reference\nsidebar:\n  order: 18\n---\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\n\nLlamaAgents reads configuration from your repository to run your app. The configuration is defined in your project's `pyproject.toml`.\n\n### pyproject.toml\n\n```toml\n[tool.llamaagents]\nname = \"my-app\"\nenv_files = [\".env\"]\n\n[tool.llamaagents.workflows]\nworkflow-one = \"my_app.workflows:some_workflow\"\nworkflow-two = \"my_app.workflows:another_workflow\"\n\n[tool.llamaagents.ui]\ndirectory = \"ui\"\nbuild_output_dir = \"ui/static\"\n```\n\n### Authentication\n\nDeployments can be configured to automatically inject authentication for LlamaCloud.\n\n```toml\n[tool.llamaagents]\nllama_cloud = true\n```\n\nWhen this is set:\n- During development, `llamactl` uses environment variable auth or the active profile, then injects `LLAMA_CLOUD_API_KEY`, `LLAMA_AGENTS_PROJECT_ID`, and `LLAMA_CLOUD_BASE_URL` into your Python server process and JavaScript build.\n- When deployed, LlamaCloud automatically injects a dedicated API key into the Python process. The frontend process receives a short-lived session cookie specific to each user visiting the application. Therefore, configure the project ID on the frontend API client so that LlamaCloud API requests from the frontend and backend are scoped to the same project ID.\n\n### `.env` files\n\nMost apps need API keys (e.g., OpenAI). You can specify them via a `.env` file and reference it in your config:\n\n```toml\n[tool.llamaagents]\nenv_files = [\".env\"]\n```\n\nThen set your secrets:\n\n```bash\n# .env\nOPENAI_API_KEY=sk-xxxx\n```\n\n### Alternative file formats (YAML/TOML)\n\nIf you prefer to keep your `pyproject.toml` simple, you can write the same configuration in a `llama_agents.yaml` or `llama_agents.toml` file. All fields use the same structure and types; omit the `tool.llamaagents` prefix.\n\n## Schema\n\n### DeploymentConfig fields\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `name` | string | `\"default\"` | URL-safe deployment name. In `pyproject.toml`, if omitted it falls back to `project.name`. |\n| `workflows` | map&lt;string,string&gt; | — | Map of `workflowName -> \"module.path:workflow\"`. |\n| `env_files` | list&lt;string&gt; | `[\".env\"]` | Paths to env files to load. Relative to the config file. Duplicate entries are removed. |\n| `env` | map&lt;string,string&gt; | `{}` | Environment variables injected at runtime. |\n| `required_env_vars` | list&lt;string&gt; | `[]` | Environment variable names that must be set at runtime. If any are missing or empty, `llamactl serve` and deployments will fail fast with an error. |\n| `llama_cloud` | boolean | false | Indicates that a deployment connects to LlamaCloud. Set to true to automatically inject a LlamaCloud API key. |\n| `ui` | `UIConfig` | `null` | Optional UI configuration. `directory` is required if `ui` is present. |\n\n#### Required environment variables\n\nUse `required_env_vars` to ensure critical secrets are present before the app starts:\n\n```toml\n[tool.llamaagents]\n# Force the app to start only when these are set\nrequired_env_vars = [\"OPENAI_API_KEY\", \"ANTHROPIC_API_KEY\"]\n```\n\nTip: Combine with `env_files` for local development or supply values via your deployment's secret manager.\n\n### UIConfig fields\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `directory` | string | — | Path to UI source, relative to the config directory. Required when `ui` is set. |\n| `build_output_dir` | string | ``${directory}/dist`` | Built UI output directory. If set in TOML/`pyproject.toml`, the path is relative to the config file. If set via `package.json` (`llamaagents.build_output_dir`), it is resolved as `${directory}/${build_output_dir}`. |\n| `package_manager` | string | `\"npm\"` (or inferred) | Package manager used to build the UI. If not set, inferred from `package.json` `packageManager` (e.g., `pnpm@9.0.0` → `pnpm`). |\n| `build_command` | string | `\"build\"` | NPM script name used to build. |\n| `serve_command` | string | `\"dev\"` | NPM script name used to serve in development. |\n| `proxy_port` | integer | `4502` | Port the app server proxies to in development. |\n\n## UI Integration via package.json\n\nNote: after setting `ui.directory` so that `package.json` can be found, you can configure the UI within it instead.\n\nFor example:\n\n```json\n{\n  \"name\": \"my-ui\",\n  \"packageManager\": \"pnpm@9.7.0\",\n  \"scripts\": { \"build\": \"vite build\", \"dev\": \"vite\" },\n  \"llamaagents\": {\n    \"build_output_dir\": \"dist\",\n    \"package_manager\": \"pnpm\",\n    \"build_command\": \"build\",\n    \"serve_command\": \"dev\",\n    \"proxy_port\": 5173\n  }\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/getting-started.md",
    "content": "---\ntitle: Getting Started\nsidebar:\n  order: 1\n---\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\n\n## Getting Started with `llamactl`\n\n`llamactl` is the local development and deployment CLI for LlamaAgents. It can scaffold an app, run the app server locally, and manage cloud deployments from your terminal.\n\n:::tip[Prefer a UI?]\nYou can also deploy starter templates directly from the LlamaCloud dashboard. See [Click-to-Deploy from LlamaCloud](/python/llamaagents/llamactl/click-to-deploy).\n:::\n\nBefore you start:\n\n- Install [`uv`](https://docs.astral.sh/uv/getting-started/installation/). `llamactl` uses it to manage Python environments and project dependencies.\n- Install `git`. Cloud deployments are built from source repositories.\n- Install Node.js if you are using a template with a frontend. For macOS and Linux, we recommend [`nvm`](https://github.com/nvm-sh/nvm). For Windows, we recommend [Chocolatey](https://community.chocolatey.org/packages/nodejs).\n- Windows support is experimental. For the best experience, use WSL2. If you run directly on Windows, see [the Windows guide](https://github.com/run-llama/llamactl-windows).\n\n## Install\n\nInstall `llamactl` globally with `uv`:\n\n```bash\nuv tool install -U llamactl\n```\n\nOr pin it to a project:\n\n```bash\nuv add --dev llamactl\n```\n\n## Authenticate\n\nLog in with your browser:\n\n```bash\nllamactl auth login\n```\n\nIf browser login is not available, use an API key and project ID:\n\n```bash\nllamactl auth token --api-key \"$LLAMA_CLOUD_API_KEY\" --project \"$LLAMA_AGENTS_PROJECT_ID\"\n```\n\nFor CI or other non-interactive environments, you can skip the stored profile and set environment variables instead:\n\n```bash\nexport LLAMA_CLOUD_API_KEY=\"llx-...\"\nexport LLAMA_AGENTS_PROJECT_ID=\"project-id\"\n```\n\nSee [`llamactl auth`](/python/llamaagents/llamactl-reference/commands-auth), [`llamactl environments`](/python/llamaagents/llamactl-reference/commands-environments), and [`llamactl projects`](/python/llamaagents/llamactl-reference/commands-projects) for profile, environment, and project commands.\n\n## Initialize a Project\n\nCreate a new LlamaAgents project:\n\n```bash\nllamactl init\n```\n\n`llamactl init` opens a template picker and writes a project scaffold. Templates may include Python workflows only, or a Python app plus a frontend UI.\n\n:::warning\n`llamactl init` uses symlinks. On Windows, enable Developer Mode with `start ms-settings:developers` before running the command.\n:::\n\n:::info\nThe scaffold may include assistant-facing files such as `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md`. They are optional and do not affect builds, runtime, or deployments.\n:::\n\nApplication configuration lives in your project's `pyproject.toml`, or in `llama_agents.yaml` / `llama_agents.toml`. See the [Deployment Config Reference](/python/llamaagents/llamactl/configuration-reference) for the schema.\n\n## Run Locally\n\nFrom the project directory, start the local development server:\n\n```bash\nllamactl serve\n```\n\n`llamactl serve` installs dependencies, reads the workflows configured for the app, serves them as an API, and proxies the frontend development server when the app has a UI.\n\nFor example, this configuration serves `my-workflow` under the local deployment named `my-package`:\n\n```toml\n[project]\nname = \"my-package\"\n\n[tool.llamaagents.workflows]\nmy-workflow = \"my_package.my_workflow:workflow\"\n\n[tool.llamaagents.ui]\ndirectory = \"ui\"\n```\n\n```py\n# src/my_package/my_workflow.py\nworkflow = MyWorkflow()\n```\n\nThe local API is available at `http://localhost:4501/deployments/my-package`. To run the workflow, make a `POST` request to `/deployments/my-package/workflows/my-workflow/run`.\n\nFor flags, see [`llamactl serve`](/python/llamaagents/llamactl-reference/commands-serve). For workflow API details, see [Workflows & App Server API](/python/llamaagents/llamactl/workflow-api).\n\n## Create a Cloud Deployment\n\nCloud deployments are built from a Git repository. Commit and push your project first:\n\n```bash\ngit remote add origin https://github.com/org/repo\ngit add -A\ngit commit -m \"Set up LlamaAgents app\"\ngit push -u origin main\n```\n\nThen create the deployment:\n\n```bash\nllamactl deployments create\n```\n\n`deployments create` opens a YAML deployment spec in your `$EDITOR`. Review the detected repository, deployment config path, Git ref, and secrets. Save and close the file to create the deployment.\n\nFor non-interactive creation, pass a file:\n\n```bash\nllamactl deployments create -f deployment.yaml\n```\n\nPrivate GitHub repositories require LlamaCloud to have repository access. If access is missing, `llamactl` will return an error with the next step.\n\n## Declarative Deployments\n\nFor repeatable deploys, generate an apply-shaped deployment spec:\n\n```bash\nllamactl deployments template > deployment.yaml\n```\n\nEdit the file, then apply it:\n\n```bash\nllamactl deployments apply -f deployment.yaml\n```\n\n`apply` creates the deployment when it does not exist and updates it when it does. Secret values can reference local environment variables:\n\n```yaml\nname: my-agent\nspec:\n  repo_url: https://github.com/org/repo\n  deployment_file_path: \".\"\n  git_ref: main\n  secrets:\n    OPENAI_API_KEY: ${OPENAI_API_KEY}\n```\n\nRun a dry run before changing cloud state:\n\n```bash\nllamactl deployments apply -f deployment.yaml --dry-run\n```\n\n## Inspect and Stream Logs\n\nList deployments in the active project:\n\n```bash\nllamactl deployments get\n```\n\nFetch one deployment:\n\n```bash\nllamactl deployments get NAME\n```\n\nStream logs:\n\n```bash\nllamactl deployments logs NAME --follow\n```\n\nUse `-o json` or `-o yaml` with `deployments get` when another tool needs structured output.\n\n## Update a Deployment\n\nIf the deployment tracks a branch, re-resolve that branch and start a new release from the latest commit:\n\n```bash\nllamactl deployments update NAME\n```\n\nTo point at a specific branch, tag, or commit:\n\n```bash\nllamactl deployments update NAME --git-ref main\n```\n\nFor push-mode deployments, `update` pushes local code only when the current repo already has the deployment remote configured. Use `--push` to link and push the current repo, or `--no-push` to redeploy code already available to the server:\n\n```bash\nllamactl deployments update NAME --push\nllamactl deployments update NAME --no-push\n```\n\nTo edit the deployment spec in your editor:\n\n```bash\nllamactl deployments edit NAME\n```\n\nOr keep the spec in version control and re-apply it:\n\n```bash\nllamactl deployments apply -f deployment.yaml\n```\n\n## Package for Self-Hosted Deployments\n\nIf you prefer to build and deploy containers yourself, use `llamactl pkg` to generate container files for your app. See the [`pkg` command reference](/python/llamaagents/llamactl-reference/commands-pkg/).\n\n---\n\nNext: Read about defining and exposing workflows in [Workflows & App Server API](/python/llamaagents/llamactl/workflow-api).\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/ui-build.md",
    "content": "---\ntitle: Configuring a UI\nsidebar:\n  order: 10\n---\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\n\nThis page explains how to configure a custom frontend that builds and communicates with your LlamaAgents workflow server. If you've started from a template, you're good to go. Read on to learn more.\n\nThe LlamaAgents toolchain is unopinionated about your UI stack — bring your own UI. Most templates use Vite with React, but any framework will work that can:\n\n- build static assets for production, and\n- read a few environment variables during build and development\n\n## How the integration works\n\n`llamactl` starts and proxies your frontend during development by calling your `npm run dev` command. When you deploy, it builds your UI statically with `npm run build`. These commands are configurable; see [UIConfig](/python/llamaagents/llamactl/configuration-reference#uiconfig-fields) in the configuration reference. You can also use other package managers if you have [corepack](https://nodejs.org/download/release/v19.9.0/docs/api/corepack.html) enabled.\n\nDuring development, `llamactl` starts its workflow server (port `4501` by default) and starts the UI, passing a `PORT` environment variable (set to `4502` by default) and a `LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH` (for example, `/deployments/<name>/ui`) where the UI will be served. It then proxies requests from the server to the client app from that base path.\n\nOnce deployed, the Kubernetes operator builds your application with the configured npm script (`build` by default) and serves your static assets at the same base path.\n\n## Required configuration\n\n1. Serve the dev UI on the configured `PORT`. This environment variable tells your dev server which port to use during development. Many frameworks, such as Next.js, read this automatically.\n2. Set your app's base path to the value of `LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH`. LlamaAgents applications rely on this path to route to multiple workflow deployments. The proxy leaves this path intact so your application can link internally using absolute paths. Your development server and router need to be aware of this base path. Most frameworks provide a way to configure it. For example, Vite uses [`base`](https://vite.dev/config/shared-options.html#base).\n3. Re-export the `LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH` env var to your application. Read this value (for example, in React Router) to configure a base path. This is also often necessary to link static assets correctly.\n4. If you're integrating with LlamaCloud, re-export the `LLAMA_DEPLOY_PROJECT_ID` env var to your application and use it to scope your LlamaCloud requests to the same project. Read more in the [Configuration Reference](/python/llamaagents/llamactl/configuration-reference#authentication).\n5. We also recommend re-exporting `LLAMA_DEPLOY_DEPLOYMENT_NAME`, which can be helpful for routing requests to your workflow server correctly.\n\n## Examples\n\n### Vite (React)\n\nConfigure `vite.config.ts` to read the injected environment and set the base path and port:\n\n```ts\n// vite.config.ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\nexport default defineConfig(() => {\n  const basePath = process.env.LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH;\n  const port = process.env.PORT ? parseInt(process.env.PORT) : undefined;\n  return {\n    plugins: [react()],\n    server: { port, host: true, hmr: { port } },\n    base: basePath,\n    // Pass-through env for client usage\n    define: {\n      ...(basePath && {\n        \"import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH\": JSON.stringify(basePath),\n      }),\n      ...(process.env.LLAMA_DEPLOY_DEPLOYMENT_NAME && {\n        \"import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME\": JSON.stringify(\n          process.env.LLAMA_DEPLOY_DEPLOYMENT_NAME,\n        ),\n      }),\n      ...(process.env.LLAMA_DEPLOY_PROJECT_ID && {\n        \"import.meta.env.VITE_LLAMA_DEPLOY_PROJECT_ID\": JSON.stringify(\n          process.env.LLAMA_DEPLOY_PROJECT_ID,\n        ),\n      }),\n    },\n  };\n});\n```\n\nScripts in `package.json` typically look like:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\"\n  }\n}\n```\n\n### Next.js (static export)\n\nNext.js supports static export. Configure `next.config.mjs` to use the provided base path and enable static export:\n\n```js\n// next.config.mjs\nconst basePath = process.env.LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH || \"\";\nconst deploymentName = process.env.LLAMA_DEPLOY_DEPLOYMENT_NAME;\nconst projectId = process.env.LLAMA_DEPLOY_PROJECT_ID;\n\nexport default {\n  // Mount app under /deployments/<name>/ui\n  basePath,\n  // For assets when hosted behind a path prefix\n  assetPrefix: basePath || undefined,\n  // Enable static export for production\n  output: \"export\",\n  // Expose base path to browser for runtime URL construction\n  env: {\n    NEXT_PUBLIC_LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH: basePath,\n    NEXT_PUBLIC_LLAMA_DEPLOY_DEPLOYMENT_NAME: deploymentName,\n    NEXT_PUBLIC_LLAMA_DEPLOY_PROJECT_ID: projectId,\n  },\n};\n```\n\nEnsure your scripts export to a directory (default: `out/`):\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build && next export\"\n  }\n}\n```\n\nThe dev server binds to the `PORT` the app server sets; no additional configuration is needed. For dynamic routes or server features not compatible with static export, you can omit the export and rely on proxying to the Python app server. However, production static hosting requires a build output directory.\n\n#### Runtime URL construction (images/assets)\n\n- Vite: use the configured `base` or `import.meta.env.BASE_URL` (or the pass-through variable) to prefix asset URLs you build at runtime:\n\n```tsx\nconst base = import.meta.env.VITE_LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH || import.meta.env.BASE_URL || \"/\";\n<img src={`${base.replace(/\\/$/, \"\")}/images/logo.png`} />\n```\n\n- Next.js static export: use the exposed `NEXT_PUBLIC_LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH` so routes resolve absolute asset paths correctly:\n\n```tsx\nconst base = process.env.NEXT_PUBLIC_LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH || \"\";\nexport default function Logo() {\n  return <img src={`${base}/images/logo.png`} alt=\"logo\" />;\n}\n```\n\n## `PUBLIC_*` env var overrides\n\nSet `PUBLIC_X` to override `X` in the UI build env only. The backend keeps the original value. `PUBLIC_*` keys are stripped from the build environment.\n\n```yaml\nenv:\n  API_URL: \"http://internal.svc:8000\"\n  PUBLIC_API_URL: \"https://api.example.com\"  # UI build sees API_URL=https://api.example.com\n```\n\nYour vite/next config can then map the overridden value into framework-specific vars (e.g. `VITE_API_URL` via a `define` block) as usual.\n\n## Configure the UI output directory\n\nYour UI must output static assets that the platform can locate. Configure `ui.directory` and `ui.build_output_dir` as described in the [Deployment Config Reference](/python/llamaagents/llamactl/configuration-reference#uiconfig-fields). Default: `${ui.directory}/dist`.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/ui-hooks.md",
    "content": "---\ntitle: Workflow React Hooks\nsidebar:\n  order: 15\n---\n\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\n\nOur React library, `@llamaindex/ui`, is the recommended way to integrate your UI with a LlamaAgents workflow server and LlamaCloud. It comes pre-installed in any of our templates containing a UI. The library provides both React hooks for custom integrations and standard components.\n\n### Workflows Hooks\n\nOur React hooks provide an idiomatic way to observe and interact with your LlamaAgents workflows remotely from a frontend client.\n\nThere are 4 hooks you can use:\n1. **useWorkflow**: Get actions for a specific workflow (create handlers, run to completion).\n2. **useHandler**: Get state and actions for a single handler (stream events, send events).\n3. **useHandlers**: List and monitor handlers with optional filtering.\n4. **useWorkflows**: List all available workflows.\n\n### Client setup\n\nConfigure the hooks with a workflow client. Wrap your app with an `ApiProvider` that points to your deployment:\n\n```tsx\nimport { ApiProvider, type ApiClients, createWorkflowsClient } from \"@llamaindex/ui\";\n\nconst deploymentName =\n  (import.meta as any).env?.VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME || \"default\";\n\nconst clients: ApiClients = {\n  workflowsClient: createWorkflowsClient({\n    baseUrl: `/deployments/${deploymentName}`,\n  }),\n};\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return <ApiProvider clients={clients}>{children}</ApiProvider>;\n}\n```\n\n### List available workflows\n\nUse `useWorkflows` to list all workflows available in the deployment:\n\n```tsx\nimport { useWorkflows } from \"@llamaindex/ui\";\n\nexport function WorkflowList() {\n  const { state, sync } = useWorkflows();\n\n  if (state.loading) return <div>Loading…</div>;\n\n  return (\n    <div>\n      <button onClick={() => sync()}>Refresh</button>\n      <ul>\n        {Object.values(state.workflows).map((w) => (\n          <li key={w.name}>{w.name}</li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\n### Start a run\n\nStart a workflow by name with `useWorkflow`. Call `createHandler` with a JSON input payload to get back a handler state immediately.\n\n```tsx\nimport { useState } from \"react\";\nimport { useWorkflow } from \"@llamaindex/ui\";\n\nexport function CreateHandler() {\n  const workflow = useWorkflow(\"stream\");\n  const [handlerId, setHandlerId] = useState<string | null>(null);\n\n  async function handleClick() {\n    const handlerState = await workflow.createHandler({});\n    setHandlerId(handlerState.handler_id);\n  }\n\n  return (\n    <div>\n      <button onClick={handleClick}>Create Handler</button>\n      {handlerId && <div>Created: {handlerId}</div>}\n    </div>\n  );\n}\n```\n\n### Watch a run and stream events\n\nSubscribe to a handler's live event stream using `subscribeToEvents`:\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport { useWorkflow, useHandler, WorkflowEvent, isStopEvent } from \"@llamaindex/ui\";\n\nexport function StreamEvents() {\n  const workflow = useWorkflow(\"stream\");\n  const [handlerId, setHandlerId] = useState<string | null>(null);\n  const handler = useHandler(handlerId);\n  const [events, setEvents] = useState<WorkflowEvent[]>([]);\n\n  async function start() {\n    setEvents([]);\n    const h = await workflow.createHandler({});\n    setHandlerId(h.handler_id);\n  }\n\n  useEffect(() => {\n    if (!handlerId) return;\n    const sub = handler.subscribeToEvents({\n      onData: (event) => setEvents((prev) => [...prev, event]),\n    });\n    return () => sub.unsubscribe();\n  }, [handlerId]);\n\n  const stop = events.find(isStopEvent);\n\n  return (\n    <div>\n      <button onClick={start}>Start & Stream</button>\n      {handlerId && <div>Status: {handler.state.status}</div>}\n      {stop && <pre>{JSON.stringify(stop.data, null, 2)}</pre>}\n      {!stop && events.length > 0 && <div>{events.length} events received</div>}\n    </div>\n  );\n}\n```\n\n### Monitor multiple workflow runs\n\nUse `useHandlers` to query and monitor a filtered list of workflow handlers. This is useful for progress indicators or \"Recent runs\" views.\n\n```tsx\nimport { useHandlers } from \"@llamaindex/ui\";\n\nexport function RecentRuns() {\n  const { state, sync } = useHandlers({\n    query: { status: [\"running\", \"completed\"] },\n  });\n\n  if (state.loading) return <div>Loading…</div>;\n\n  const handlers = Object.values(state.handlers);\n\n  return (\n    <div>\n      <button onClick={() => sync()}>Refresh</button>\n      <ul>\n        {handlers.map((h) => (\n          <li key={h.handler_id}>\n            {h.handler_id.slice(0, 8)}… — {h.status}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\nThe `sync` option controls whether to fetch handlers on mount. Call `sync()` manually to refresh the list from the server at any time.\n\n### Hook Reference\n\n| Hook | Purpose | Key Methods/Properties |\n|------|---------|----------------------|\n| `useWorkflow(name)` | Work with a specific workflow | `createHandler(input)`, `runToCompletion(input)`, `state.graph` |\n| `useHandler(handlerId)` | Work with a specific handler | `sendEvent(event)`, `subscribeToEvents(callbacks)`, `sync()`, `state.status`, `state.result` |\n| `useHandlers({ query, sync })` | List/filter handlers | `sync()`, `setHandler(h)`, `actions(id)`, `state.handlers` |\n| `useWorkflows({ sync })` | List all workflows | `sync()`, `state.workflows` |\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl/workflow-api.md",
    "content": "---\ntitle: Serving your Workflows\nsidebar:\n  order: 5\n---\n:::caution\nCloud deployments of LlamaAgents are now in beta preview and broadly available for feedback. You can try them out locally or deploy to LlamaCloud and send us feedback with the in-app button.\n:::\nLlamaAgents runs your LlamaIndex workflows locally and in the cloud. Author your workflows, add minimal configuration, and `llamactl` wraps them in an application server that exposes them as HTTP APIs.\n\n## Learn the basics (LlamaIndex Workflows)\n\nLlamaAgents is built on top of LlamaIndex workflows. If you're new to workflows, start here: [LlamaIndex Workflows](/python/llamaagents/workflows).\n\n## Author a workflow (quick example)\n\n```python\n# src/app/workflows.py\nfrom llama_index.core.workflow import Workflow, step, StartEvent, StopEvent\n\nclass QuestionFlow(Workflow):\n    @step\n    async def generate(self, ev: StartEvent) -> StopEvent:\n        question = ev.question\n        return StopEvent(result=f\"Answer to {question}\")\n\nqa_workflow = QuestionFlow(timeout=120)\n```\n\n## Configure workflows for LlamaAgents to serve\n\nThe app server reads workflows configured in your `pyproject.toml` and makes them available under their configured names.\n\nDefine workflow instances in your code, then reference them in your config.\n\n```toml\n# pyproject.toml\n[project]\nname = \"app\"\n# ...\n[tool.llamaagents.workflows]\nanswer-question = \"app.workflows:qa_workflow\"\n```\n\n## How serving works (local and cloud)\n\n- `llamactl serve` discovers your config. See [`llamactl serve`](/python/llamaagents/llamactl-reference/commands-serve/).\n- The app server loads your workflows.\n- HTTP routes are exposed under `/deployments/{name}`. In development, `{name}` defaults to your Python project name and is configurable. On deploy, you can set a new name; a short random suffix may be appended to ensure uniqueness.\n- Workflow instances are registered under the specified name. For example, `POST /deployments/app/workflows/answer-question/run` runs the workflow above.\n- If you configure a UI, it runs alongside your API (proxied in dev, static in preview). For details, see [UI build and dev integration](/python/llamaagents/llamactl/ui-build).\n\nDuring development, the API is available at `http://localhost:4501`. After you deploy to LlamaCloud, it is available at `https://api.cloud.llamaindex.ai`.\n\n### Authorization\n\nDuring local development, the API is unprotected. After deployment, your API uses the same authorization as LlamaCloud. Create an API token in the same project as the agent to make requests. For example:\n\n```bash\ncurl 'https://api.cloud.llamaindex.ai/deployments/app-xyz123/workflows/answer-question/run' \\\n  -H 'Authorization: Bearer llx-xxx' \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"start_event\": {\"question\": \"What is the capital of France?\"}}'\n```\n\n## Workflow HTTP API\n\nWhen using a `WorkflowServer`, the app server exposes your workflows as an API. View the OpenAPI reference at `/deployments/<name>/docs`.\n\nThis API allows you to:\n- Retrieve details about registered workflows\n- Trigger runs of your workflows\n- Stream published events from your workflows, and retrieve final results from them\n- Send events to in-progress workflows (for example, HITL scenarios).\n\nDuring development, visit `http://localhost:4501/debugger` to test and observe your workflows in a UI.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/_meta.yml",
    "content": "label: 'llamactl Reference'\norder: 100\ncollapsed: true\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-auth.md",
    "content": "---\ntitle: auth\nsidebar:\n  order: 104\n---\nAuthenticate and manage profiles for the current environment. Profiles store your control plane API URL, project, and optional API key.\n\n## Usage\n\n```bash\nllamactl auth [COMMAND] [options]\n```\n\nCommands:\n\n- `token [--project PROJECT] [--api-key KEY]`: Create a profile from an API key; validates token and selects a project\n- `login`: Log in via web browser (OIDC device flow) and create a profile\n- `get [NAME] [-o text|json|yaml]`: List profiles or show one profile in the current environment\n- `use [NAME]`: Set the current profile\n- `logout [NAME]`: Delete a profile and its local credentials\n- `inject [--env-file PATH]`: Write profile credentials to a `.env` file\n\nNotes:\n\n- Profiles are filtered by the current environment (`llamactl environments use`).\n- `auth token` creates a profile without prompts when both `--api-key` and `--project` are supplied.\n- Project selection moved to `llamactl projects use`.\n- Organization listing moved to `llamactl organizations get`.\n\n## Commands\n\n### Token\n\n```bash\nllamactl auth token [--project PROJECT] [--api-key KEY]\n```\n\nWithout flags, prompts for an API key, validates it by listing projects, then lets you choose a project. Creates an auto-named profile and sets it current.\n\nWith both `--api-key` and `--project`, creates the profile without prompts.\n\nExample:\n\n```bash\nllamactl auth token --api-key llx-... --project your-project-id\n```\n\n### Login\n\n```bash\nllamactl auth login\n```\n\nLog in via your browser using the OIDC device flow, select a project, and create a login profile set as current.\n\n### Get\n\n```bash\nllamactl auth get [NAME] [-o text|json|yaml]\n```\n\nShows profiles for the current environment. Pass `NAME` to show one profile.\n\n### Use\n\n```bash\nllamactl auth use [NAME]\n```\n\nSet the current profile. If `NAME` is omitted in a TTY, choose from the available profiles. Scripts should pass `NAME`.\n\n### Logout\n\n```bash\nllamactl auth logout [NAME]\n```\n\nDelete a profile. If the deleted profile is current, the current selection is cleared.\n\n### Inject\n\n```bash\nllamactl auth inject [--env-file PATH]\n```\n\nWrite `LLAMA_CLOUD_API_KEY`, `LLAMA_CLOUD_BASE_URL`, and `LLAMA_AGENTS_PROJECT_ID` from the current profile into a `.env` file. Creates the file if it doesn't exist; overwrites existing values.\n\n## Environment Variables\n\n`llamactl` can authenticate via environment variables instead of a stored profile. This is useful for CI, automated scripts, and environments where `llamactl auth login` isn't practical.\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `LLAMA_CLOUD_API_KEY` | Yes | — | API key for authentication |\n| `LLAMA_AGENTS_PROJECT_ID` | Yes (for project-scoped commands) | — | Project to operate on |\n| `LLAMA_CLOUD_BASE_URL` | No | `https://api.cloud.llamaindex.ai` | Control plane API URL |\n| `LLAMA_CLOUD_USE_PROFILE` | No | `false` | Set to `1` to ignore env vars and use a stored profile |\n\nWhen `LLAMA_CLOUD_API_KEY` and `LLAMA_AGENTS_PROJECT_ID` are both set, `llamactl` uses them directly and skips profile lookup. If a stored profile also exists, a warning is printed to stderr; set `LLAMA_CLOUD_USE_PROFILE=1` to opt back into profile auth.\n\nThe `--project` flag always takes precedence over `LLAMA_AGENTS_PROJECT_ID`.\n\nExample:\n\n```bash\nexport LLAMA_CLOUD_API_KEY=\"llx-...\"\nexport LLAMA_AGENTS_PROJECT_ID=\"your-project-id\"\nllamactl deployments get\n```\n\nOr generate a `.env` from your current profile with [`llamactl auth inject`](#inject).\n\n## See also\n\n- Environments: [`llamactl environments`](/python/llamaagents/llamactl-reference/commands-environments/)\n- Projects: [`llamactl projects`](/python/llamaagents/llamactl-reference/commands-projects/)\n- Organizations: [`llamactl organizations`](/python/llamaagents/llamactl-reference/commands-organizations/)\n- Getting started: [Introduction](/python/llamaagents/llamactl/getting-started/)\n- Deployments: [`llamactl deployments`](/python/llamaagents/llamactl-reference/commands-deployments/)\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-config.md",
    "content": "---\ntitle: config\nsidebar:\n  order: 108\n---\nShow local `llamactl` configuration.\n\n## Usage\n\n```bash\nllamactl config [-o text|json|yaml]\n```\n\nShows the current environment, profile, and active project. Structured output is useful for scripts that need to inspect local context.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-deployments.md",
    "content": "---\ntitle: deployments\nsidebar:\n  order: 103\n---\nDeploy your app to LlamaCloud and manage existing deployments.\n\nDeployment names are the stable ids shown in the `NAME` column. Commands use the active profile's project by default. Pass `--project PROJECT` on API-backed commands to override it for one command.\n\n## Usage\n\n```bash\nllamactl deployments [COMMAND] [options]\n```\n\nCommands:\n\n- `get [NAME]`: List deployments, or show one deployment\n- `create`: Create a deployment from an editor or YAML file\n- `edit [NAME]`: Edit a deployment in your editor, or update from a YAML file\n- `apply -f FILE`: Create or update a deployment from YAML\n- `template`: Print a YAML scaffold for a new deployment\n- `delete NAME`: Delete a deployment\n- `delete -f FILE`: Delete the deployment named in a YAML file\n- `update NAME`: Resolve a git ref again and start a new release\n- `history NAME`: Show release history\n- `rollback NAME`: Roll back to an earlier git SHA\n- `logs NAME`: Fetch or stream deployment logs\n- `configure-git-remote NAME`: Configure the push-mode git remote\n\nNotes:\n\n- `-o json` and `-o yaml` are for scripts. Status messages go to stderr; structured data goes to stdout.\n- `-f -` reads YAML from stdin on commands that accept `-f`.\n- `repo_url: \"\"` in apply YAML means push-mode. The deployment stores code in LlamaCloud's internal git repo instead of pulling from an external Git URL.\n\n### Push-mode auto-push\n\nPush-mode commands can mirror local code to the deployment's internal git repo. The local git remote is named `llamaagents-NAME`.\n\n- `create` configures that remote and pushes by default.\n- `edit`, `apply`, and `update` auto-push only when the current repo already has the `llamaagents-NAME` remote.\n- `--push` configures the remote and pushes from the current repo.\n- `--no-push` skips the push and uses code already available to the server.\n\nUse `llamactl deployments configure-git-remote NAME` to link a repo before relying on auto-push.\n\n## Commands\n\n### Get\n\n```bash\nllamactl deployments get [NAME] [-o text|json|yaml|wide|template] [--project PROJECT]\n```\n\nWith no `NAME`, lists deployments in the active project. With `NAME`, prints one deployment.\n\nOutput modes:\n\n- `text`: Default table output\n- `wide`: Table output with less common columns\n- `json`: Machine-readable JSON\n- `yaml`: Machine-readable YAML\n- `template`: Apply-shaped YAML for one deployment only\n\nExamples:\n\n```bash\nllamactl deployments get\nllamactl deployments get invoice-agent -o yaml\nllamactl deployments get invoice-agent -o template > deployment.yaml\n```\n\n### Create\n\n```bash\nllamactl deployments create [-f FILE] [--no-push] [--project PROJECT]\n```\n\nWithout `-f`, opens `$EDITOR` with a commented YAML scaffold. Save and close the file to create the deployment.\n\nWith `-f FILE`, creates from YAML without opening an editor. Use `-f -` to read from stdin.\n\nFlags:\n\n- `-f, --filename FILE`: YAML file, or `-` for stdin\n- `--no-push`: Skip pushing local code for push-mode deployments\n- `--project PROJECT`: Override the active project\n\nExamples:\n\n```bash\nllamactl deployments create\nllamactl deployments create -f deployment.yaml\nllamactl deployments create -f - < deployment.yaml\nllamactl deployments create --no-push\n```\n\n### Edit\n\n```bash\nllamactl deployments edit [NAME] [-f FILE] [--push] [--no-push] [--project PROJECT]\n```\n\nWithout `-f`, fetches the deployment, renders editable YAML, and opens `$EDITOR`. If `NAME` is omitted in a TTY, choose from existing deployments. Scripts should pass `NAME`.\n\nWith `-f FILE`, updates from YAML without opening an editor. If `NAME` is omitted, the YAML must include top-level `name`.\n\nFor push-mode deployments, edit auto-pushes only from a repo that already has the `llamaagents-NAME` remote. Pass `--push` to link and push the current repo.\n\nFlags:\n\n- `-f, --filename FILE`: YAML file, or `-` for stdin\n- `--push`: Link and push the current repo even if the deployment remote is not configured\n- `--no-push`: Skip pushing local code for push-mode deployments\n- `--project PROJECT`: Override the active project\n\nExamples:\n\n```bash\nllamactl deployments edit invoice-agent\nllamactl deployments edit invoice-agent -f deployment.yaml\nllamactl deployments edit -f deployment.yaml\n```\n\n### Apply\n\n```bash\nllamactl deployments apply -f FILE [--dry-run] [--push] [--no-push] [--annotate-on-error] [--project PROJECT]\n```\n\nApplies deployment YAML declaratively. If the top-level `name` exists, `apply` updates that deployment. If it does not exist, `apply` creates it. YAML produced by `deployments template` or `deployments get NAME -o template` is ready for this command.\n\n`${VAR}` references are resolved from the process environment at apply time. Masked secret values from read output are ignored so round-tripping a deployment does not overwrite existing secrets with placeholders.\n\nFor push-mode updates, apply auto-pushes only from a repo that already has the `llamaagents-NAME` remote. Push-mode creates still configure the remote and push by default.\n\nFlags:\n\n- `-f, --filename FILE`: Required YAML file, or `-` for stdin\n- `--dry-run`: Validate and print the resolved payload without changing the deployment\n- `--push`: Link and push the current repo even if the deployment remote is not configured\n- `--no-push`: Skip pushing local code for push-mode deployments\n- `--annotate-on-error`: Write validation errors back into the YAML as comments\n- `--project PROJECT`: Override the active project\n\nExamples:\n\n```bash\nllamactl deployments template > deployment.yaml\nllamactl deployments apply -f deployment.yaml\nllamactl deployments apply -f deployment.yaml --dry-run\nllamactl deployments apply -f deployment.yaml --annotate-on-error\nllamactl deployments get invoice-agent -o template | llamactl deployments apply -f -\n```\n\n### Template\n\n```bash\nllamactl deployments template\n```\n\nPrints a commented YAML scaffold for a new deployment. This command is offline and does not require auth. It uses local context when available, such as the current git remote, current branch, deployment config path, and secret names.\n\nExample:\n\n```bash\nllamactl deployments template > deployment.yaml\n```\n\n### Delete\n\n```bash\nllamactl deployments delete NAME [--project PROJECT]\nllamactl deployments delete -f FILE [--project PROJECT]\n```\n\nDeletes a deployment immediately. Pass `NAME` directly, or pass `-f FILE` to read the deployment name from YAML.\n\nFlags:\n\n- `-f, --filename FILE`: YAML file containing top-level `name`\n- `--project PROJECT`: Override the active project\n\nExamples:\n\n```bash\nllamactl deployments delete invoice-agent\nllamactl deployments delete -f deployment.yaml\n```\n\n### Update\n\n```bash\nllamactl deployments update NAME [--git-ref REF] [--push] [--no-push] [--project PROJECT]\n```\n\nResolves the deployment's configured git ref again and starts a new release from the resulting commit. Use `--git-ref` to switch to a branch, tag, or commit before resolving.\n\nFor push-mode deployments, `update` mirrors local code only when the current repo already has the deployment remote configured. Use `--push` to link and push the current repo, or `--no-push` if you want to redeploy the revision already available to the server.\n\nFlags:\n\n- `--git-ref REF`: Branch, tag, or commit SHA to deploy\n- `--push`: Link and push the current repo even if the deployment remote is not configured\n- `--no-push`: Skip mirroring local code for push-mode deployments\n- `--project PROJECT`: Override the active project\n\nExamples:\n\n```bash\nllamactl deployments update invoice-agent\nllamactl deployments update invoice-agent --git-ref release-2026-05\nllamactl deployments update invoice-agent --push\nllamactl deployments update invoice-agent --no-push\n```\n\n### History\n\n```bash\nllamactl deployments history NAME [-o text|json|yaml|wide] [--project PROJECT]\n```\n\nShows release history for a deployment, newest first.\n\nExamples:\n\n```bash\nllamactl deployments history invoice-agent\nllamactl deployments history invoice-agent -o yaml\n```\n\n### Rollback\n\n```bash\nllamactl deployments rollback NAME [--git-sha SHA] [--project PROJECT]\n```\n\nRolls a deployment back to a previous git SHA. In a TTY, omit `--git-sha` to choose from release history. Scripts should pass `--git-sha`.\n\nFlags:\n\n- `--git-sha SHA`: Git SHA to roll back to\n- `--project PROJECT`: Override the active project\n\nExamples:\n\n```bash\nllamactl deployments history invoice-agent\nllamactl deployments rollback invoice-agent --git-sha 3f2a1c9\nllamactl deployments rollback invoice-agent\n```\n\n### Logs\n\n```bash\nllamactl deployments logs NAME [--follow] [--json] [--tail N] [--since-seconds N] [--include-init-containers] [--project PROJECT]\n```\n\nFetches recent deployment logs and exits. Use `--follow` to keep streaming.\n\nFlags:\n\n- `--follow, -f`: Stream until interrupted\n- `--json`: Emit one JSON log event per line\n- `--tail N`: Number of log lines to fetch initially\n- `--since-seconds N`: Only return logs newer than this many seconds\n- `--include-init-containers`: Include init container logs\n- `--project PROJECT`: Override the active project\n\nExamples:\n\n```bash\nllamactl deployments logs invoice-agent\nllamactl deployments logs invoice-agent --follow\nllamactl deployments logs invoice-agent --tail 50 --since-seconds 3600\nllamactl deployments logs invoice-agent --json\n```\n\n### Configure Git Remote\n\n```bash\nllamactl deployments configure-git-remote NAME [--project PROJECT]\n```\n\nConfigures an authenticated git remote for a push-mode deployment. The remote is named `llamaagents-NAME`.\n\nAfter this runs, `deployments edit`, `deployments apply -f`, and `deployments update` can auto-push from the current repo for that deployment.\n\nExamples:\n\n```bash\nllamactl deployments configure-git-remote invoice-agent\ngit push llamaagents-invoice-agent\n```\n\n## See also\n\n- Getting started: [Introduction](/python/llamaagents/llamactl/getting-started/)\n- Configure names, env, and UI: [Deployment Config Reference](/python/llamaagents/llamactl/configuration-reference/)\n- Local dev server: [`llamactl serve`](/python/llamaagents/llamactl-reference/commands-serve/)\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-environments.md",
    "content": "---\ntitle: environments\nsidebar:\n  order: 105\n---\nManage environments (distinct control plane API URLs). Environments determine which profiles are shown and where auth/project actions apply.\n\n## Usage\n\n```bash\nllamactl environments [COMMAND] [options]\n```\n\nCommands:\n\n- `get [API_URL] [-o text|json|yaml]`: List environments or show one environment\n- `add <API_URL>`: Probe the server and upsert the environment\n- `use [API_URL]`: Select the current environment; prompts if omitted in interactive mode\n- `delete [API_URL]`: Remove an environment and its associated profiles\n\nNotes:\n\n- Probing reads `requires_auth` and `min_llamactl_version` from the server version endpoint.\n- Switching environment filters profiles shown by `llamactl auth get` and used by other commands.\n\n## Commands\n\n### Get\n\n```bash\nllamactl environments get [API_URL]\n```\n\nShows a table of environments with API URL, whether auth is required, and the active environment. Pass `API_URL` to show one environment.\n\n### Add\n\n```bash\nllamactl environments add <API_URL>\n```\n\nProbes the server at `<API_URL>` and stores discovered settings. Interactive mode can prompt for the URL.\n\n### Use\n\n```bash\nllamactl environments use [API_URL]\n```\n\nSets the current environment. If omitted in interactive mode, you’ll be prompted to select one.\n\n### Delete\n\n```bash\nllamactl environments delete [API_URL]\n```\n\nDeletes an environment and all associated profiles. If the deleted environment was current, the current environment is reset to the default.\n\n## See also\n\n- Profiles and tokens: [`llamactl auth`](/python/llamaagents/llamactl-reference/commands-auth/)\n- Projects: [`llamactl projects`](/python/llamaagents/llamactl-reference/commands-projects/)\n- Getting started: [Introduction](/python/llamaagents/llamactl/getting-started/)\n- Deployments: [`llamactl deployments`](/python/llamaagents/llamactl-reference/commands-deployments/)\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-init.md",
    "content": "---\ntitle: init\nsidebar:\n  order: 101\n---\nCreate a new app from a starter template, or update an existing app to the latest template version.\n\n## Usage\n\n```bash\nllamactl init [--template <id>] [--dir <path>] [--force]\nllamactl init --update\n```\n\n## Templates\n\nRun `llamactl init` without `--template` to open the template picker. The picker shows both UI templates and headless workflow templates.\n\nUse `--template <id>` when you already know which template you want. Template IDs can change as templates are added or renamed, so use the picker or the [Agent Templates](/python/llamaagents/llamactl/agent-templates/) page for the current list.\n\n## Options\n\n- `--update`: Update the current app to the latest template version. Ignores other options.\n- `--template <id>`: Template to use.\n- `--dir <path>`: Directory to create the new app in. Defaults to the template name.\n- `--force`: Overwrite the directory if it already exists.\n\n## What it does\n\n- Copies the selected template into the target directory using [`copier`](https://copier.readthedocs.io/en/stable/)\n- Adds assistant docs: `AGENTS.md` and symlinks `CLAUDE.md`/`GEMINI.md`\n- Initializes a Git repository if `git` is available\n- Prints next steps to run locally and deploy\n\n## Examples\n\nOpen the template picker:\n\n```bash\nllamactl init\n```\n\nCreate from a known template:\n\n```bash\nllamactl init --template <template-id> --dir my-app\n```\n\nOverwrite an existing directory:\n\n```bash\nllamactl init --template <template-id> --dir ./my-app --force\n```\n\nUpdate an existing app to the latest template:\n\n```bash\nllamactl init --update\n```\n\nSee also: [Getting Started guide](/python/llamaagents/llamactl/getting-started/).\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-organizations.md",
    "content": "---\ntitle: organizations\nsidebar:\n  order: 107\n---\nList organizations available to the current profile.\n\n## Usage\n\n```bash\nllamactl organizations get [-o text|json|yaml]\n```\n\nThe current/default organization is marked in text output. On servers without organization support, text output prints a warning and structured output returns an empty list.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-pkg.md",
    "content": "---\ntitle: pkg\nsidebar:\n  order: 106\n---\n\n:::caution\nThis command is currently limited to python **agent workflows**.\nFrontend packaging is **not yet supported**.\n:::\n\nThe `pkg` command group lets you package and export your application for custom deployments.\nCurrently it supports exporting a Dockerfile that can be built into an image with any OCI compliant image builder, such as `docker` or `podman`.\n\n\n## Usage\n\n```bash\nllamactl pkg [COMMAND] [options]\n```\n\n### Available Commands\n\n- `container` – Generate a minimal, build-ready container file (e.g., `Dockerfile`) for your workflows.\n\n\n## Command: `container`\n\n```bash\nllamactl pkg container [DEPLOYMENT_FILE] [options]\n```\n\nGenerates a container build file from a given deployment file and user-specified options.\n\n### Options\n\n\n- `deployment_file` - Path to the deployment file. Defaults to the current directory (`.`)\n- `--python-version` - Python version for the base image. If not specified, it's inferred from `.python-version` or `pyproject.toml`; defaults to **3.12** if none found.\n- `--port <int>` - Port to expose for the API server. Defaults to 4501.\n- `--dockerignore-path` - Path for the generated `.dockerignore` file. Defaults to `.dockerignore`.\n- `--overwrite` - Overwrite any existing output files. No default value.\n- `--exclude <path>` - Path(s) to exclude from the build (appended to `.dockerignore`). Can be used multiple times. No default value.\n- `--output-file` - Path and filename for the generated container build file. Defaults to `Dockerfile`.\n- `--help` - Show help for this command and exit. No default value.\n\n### Notes\n\n- The generated `.dockerignore` file automatically excludes common Python-related directories such as:\n  - Local virtual environments\n  - Caches\n  - Files that may contain sensitive data (e.g., `.env`)\n- The produced container file is **minimal by design**. It should work for most cases, but you may need to customize it for specific use cases.\n\n### Examples\n\n**1. Create default `Dockerfile` and `.dockerignore`:**\n```bash\nllamactl pkg container\n```\n\n**2. Generate a custom container file with specific name, Python version, and port:**\n```bash\nllamactl pkg container --output-file Containerfile --python-version 3.14 --port 4502\n```\n\n**3. Exclude certain files or directories from the build:**\n```bash\nllamactl pkg container --exclude .env.local --exclude .github/workflows/ --exclude \"*.pdf\"\n```\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-projects.md",
    "content": "---\ntitle: projects\nsidebar:\n  order: 106\n---\nList and select projects for the current profile.\n\n## Usage\n\n```bash\nllamactl projects [COMMAND] [options]\n```\n\nCommands:\n\n- `get [PROJECT_ID] [--org ORG_ID] [-o text|json|yaml]`: List projects or show one project\n- `use [PROJECT_ID] [--org ORG_ID]`: Set the active project for the current profile\n\n`--project` on deployment commands still overrides the active profile project for one call.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/llamactl-reference/commands-serve.md",
    "content": "---\ntitle: serve\nsidebar:\n  order: 102\n---\nServe your app locally for development and testing. Reads configuration from your project (e.g., `pyproject.toml` or `llama_agents.yaml`) and starts the Python API server, optionally proxying your UI in dev.\n\nSee also: [Deployment Config Reference](/python/llamaagents/llamactl/configuration-reference/) and [UI build and dev integration](/python/llamaagents/llamactl/ui-build/).\n\n## Usage\n\n```bash\nllamactl serve [DEPLOYMENT_FILE] [options]\n```\n\n- `DEPLOYMENT_FILE` defaults to `.` (current directory). Provide a path to a specific deployment file or directory if needed.\n\n## Options\n\n- `--no-install`: Skip installing Python and JS dependencies\n- `--no-reload`: Disable API server auto‑reload on code changes\n- `--no-open-browser`: Do not open the browser automatically\n- `--preview`: Build the UI to static files and serve them (production‑like)\n- `--port <int>`: Port for the API server\n- `--ui-port <int>`: Port for the UI proxy in dev\n- `--log-level <DEBUG|INFO|WARNING|ERROR|CRITICAL>`: Log level for the API server\n- `--log-format <console|json>`: Log format for the API server\n- `--persistence <memory|local|cloud>`: Persistence mode for the workflow server. Defaults to local persistence.\n- `--local-persistence-path <path>`: SQLite database path for local persistence\n- `--host <host>`: Host for the API server. Defaults to `127.0.0.1`; use `0.0.0.0` to accept remote connections.\n\n## Behavior\n\n- Prepares the server environment (installs dependencies unless `--no-install`)\n- In dev mode (default), proxies your UI dev server and reloads on change\n- In preview mode, builds the UI to static files and serves them without a proxy\n- Uses local workflow persistence by default; `--persistence cloud` stores workflow state in LlamaCloud\n\n### Credential injection\n\nIf your app uses LlamaCloud (e.g., for LlamaParse or cloud persistence), `llamactl serve` automatically injects credentials into the child process environment. It checks for `LLAMA_CLOUD_API_KEY` in the environment first, then falls back to the active profile's API key. Credentials are also forwarded with `PUBLIC_`, `VITE_`, and `NEXT_PUBLIC_` prefixes so frontend frameworks can access them during local development.\n\nSee [`llamactl auth`](/python/llamaagents/llamactl-reference/commands-auth/) for details on environment variable and profile-based authentication.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/overview.md",
    "content": "---\ntitle: Overview\nsidebar:\n  order: 1\n---\n\n## LlamaAgents at a Glance\n\nLlamaAgents is the most advanced way to build **agent workflows**. Author and run **multi-step document agents** from scratch locally using our open-source [Agent Workflows](/python/llamaagents/workflows/), or build and deploy them in the cloud with our vibe-coding [**Agent Builder**](/python/llamaagents/cloud/builder/) in [LlamaCloud](https://cloud.llamaindex.ai/)—without wiring up infrastructure, persistence, or deployment yourself.\n\nStitch together Parse, Extract, Split, Classify, and custom operations into [Workflows](/python/llamaagents/workflows/) that perform knowledge tasks on your documents. When you need full control, it's real Python underneath: fork and extend without a rewrite. Agent Workflows give you event-driven orchestration with branching, parallelism, [human-in-the-loop](/python/llamaagents/workflows/human-in-the-loop/) review, durability, and [observability](/python/llamaagents/workflows/observability/).\n\n<div style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; margin-bottom: 1.5rem;\">\n  <iframe\n    style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%;\" src=\"https://www.youtube.com/embed/0Zhf5z2Onjs\" title=\"LlamaAgents overview\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen></>\n</div>\n\n### Get Started\n\n- **Build locally**: Use the [`llamactl` CLI](/python/llamaagents/llamactl/getting-started/) to create projects from [starter templates](/python/llamaagents/llamactl-reference/commands-init/), develop and serve workflows on your machine, then deploy to LlamaCloud or self-host. You can also use [Agent Workflows](/python/llamaagents/workflows/) directly in your own Python applications—run them as async processes or [mount them as endpoints](/python/llamaagents/workflows/deployment/) in your existing server.\n\n- **Build in the cloud**: Use [**Agent Builder**](/python/llamaagents/cloud/builder/) in [LlamaCloud](https://cloud.llamaindex.ai/) (Agents → Builder) to describe your workflow in plain language; an AI coding agent generates a complete, deployable workflow. The code is yours—customize it in GitHub or run it on your own infrastructure. For a one-click path, [click-to-deploy a starter template](/python/llamaagents/llamactl/click-to-deploy/) like SEC Insights or Invoice Matching.\n\n- **Go deeper**: Combine local development with cloud services. Use [Agent Workflows](/python/llamaagents/workflows/) for orchestration and [WorkflowClient](/python/llamaagents/workflows/deployment/#using-workflowclient-to-interact-with-servers) to call deployed workflows via REST or the typed Python client.\n\n### Components\n\n**[`llamactl` CLI](/python/llamaagents/llamactl/getting-started/)**: Development and deployment for local workflow apps. Initialize from [starter templates](/python/llamaagents/llamactl-reference/commands-init/), serve locally, and deploy to LlamaCloud or export for self-hosting.\n\n**[Agent Workflows](/python/llamaagents/workflows/)**: The event-driven orchestration framework at the core. Use it as an async library in your own code, or let `llamactl` serve it. Built-in durability and [observability](/python/llamaagents/workflows/observability/).\n\n**[Agent Builder](/python/llamaagents/cloud/builder/)**: In [LlamaCloud](https://cloud.llamaindex.ai/) → **Agents** → **Builder**. Natural-language, vibe-coding interface to create document workflows; the agent generates real Python you can deploy or take to GitHub.\n\n**[`llama-cloud-services`](/python/cloud/)**: LlamaCloud document primitives (Parse, Extract, Classify), [Agent Data](/python/llamaagents/cloud/agent-data-overview/) for structured storage, and vector indexes. `llamactl` handles authentication when deploying to the cloud.\n\n**[@llamaindex/ui](/python/llamaagents/llamactl/ui-hooks/)**: React hooks for workflow-powered frontends. Deploy alongside your backend with `llamactl`.\n\n**[Workflows Client](/python/llamaagents/workflows/deployment/#using-workflowclient-to-interact-with-servers)**: Call deployed workflows via REST API or typed Python client.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/_meta.yml",
    "content": "label: Agent Workflows\ncollapsed: true\nhidden: false\norder: 1\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/async_workflows.md",
    "content": "---\nsidebar:\n  order: 15\ntitle: Writing async workflows\n---\n\nWorkflows run on Python's `asyncio` event loop. The runtime uses cooperative multitasking: while one step is `await`-ing (for example, waiting for an LLM response or a network call), other steps and workflows are free to make progress.\n\nIf you're new to async programming in Python, read [Introduction to async Python](/python/framework/getting_started/async_python/) first for a general overview of `asyncio`, event loops, and `await`.\n\nSteps can be defined as either `async def` or plain `def`. This page covers how each behaves and how to handle blocking or CPU-intensive work without stalling the event loop.\n\n## Sync steps (`def` instead of `async def`)\n\nWorkflow steps can be defined as plain `def` functions instead of `async def`. When the runtime encounters a sync step, it automatically offloads the entire function to the default thread pool using `asyncio.get_event_loop().run_in_executor()`, so the event loop is never blocked:\n\n```python\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nimport requests\n\n\nclass SyncStepWorkflow(Workflow):\n    @step\n    def fetch_data(self, ev: StartEvent) -> StopEvent:\n        # This runs in a thread automatically — the event loop stays free\n        response = requests.get(\"https://api.example.com/data\")\n        return StopEvent(result=response.json())\n```\n\nThis is the simplest option when your step body is entirely synchronous. The framework handles the thread-offloading for you, including preserving `contextvars` across the thread boundary.\n\nHowever, there are cases where you still need finer-grained control inside an `async def` step — for example, when only part of the step is blocking, or when you want to use a dedicated executor for CPU-heavy work. The sections below cover those scenarios.\n\n## Blocking I/O in async steps\n\nMany Python libraries only offer synchronous APIs: database drivers, HTTP clients, file system operations, SDK calls, and more. When you need to use one of these inside an `async def` workflow step, offload the call to a thread pool using `asyncio.to_thread`:\n\n```python\nimport asyncio\nimport requests\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\n\nclass BlockingIOWorkflow(Workflow):\n    @step\n    async def fetch_data(self, ev: StartEvent) -> StopEvent:\n        # Bad: this blocks the event loop until the request completes\n        # response = requests.get(\"https://api.example.com/data\")\n\n        # Good: run the blocking call in a thread so the event loop stays free\n        response = await asyncio.to_thread(\n            requests.get, \"https://api.example.com/data\"\n        )\n        return StopEvent(result=response.json())\n```\n\n`asyncio.to_thread` schedules the function on the default `ThreadPoolExecutor` and returns an awaitable. While the blocking call runs in a separate thread, the event loop continues processing other steps and workflows.\n\nThis applies to any synchronous library call that performs I/O — reading files, querying databases, calling external APIs, and so on:\n\n```python\nimport asyncio\nimport json\nfrom pathlib import Path\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\n\ndef read_large_file(path: str) -> dict:\n    \"\"\"A synchronous function that reads and parses a large JSON file.\"\"\"\n    return json.loads(Path(path).read_text())\n\n\nclass FileReaderWorkflow(Workflow):\n    @step\n    async def process_file(self, ev: StartEvent) -> StopEvent:\n        data = await asyncio.to_thread(read_large_file, ev.file_path)\n        return StopEvent(result=data)\n```\n\n## CPU-intensive operations\n\nCPU-bound work — data transformation, image processing, numerical computation — presents a different challenge. Even when run on a thread, CPU-intensive Python code can contend with the event loop due to the GIL (Global Interpreter Lock).\n\nFor CPU-heavy work, use a dedicated, smaller thread pool (or process pool) so that these tasks are queued and do not saturate the default executor:\n\n```python\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nfrom workflows import Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\n# A small, dedicated pool for CPU-bound work.\n# Keeping this small ensures CPU tasks are queued rather than\n# overwhelming the system with parallel CPU-bound threads.\ncpu_pool = ThreadPoolExecutor(max_workers=2)\n\n\ndef expensive_computation(data: str) -> str:\n    \"\"\"A CPU-intensive operation, e.g. data parsing or transformation.\"\"\"\n    # Simulate heavy work\n    result = data\n    for _ in range(1_000_000):\n        result = result.strip()\n    return result\n\n\nclass ComputeEvent(Event):\n    data: str\n\n\nclass CPUWorkflow(Workflow):\n    @step\n    async def start(self, ev: StartEvent) -> ComputeEvent:\n        return ComputeEvent(data=ev.input_data)\n\n    @step\n    async def compute(self, ev: ComputeEvent) -> StopEvent:\n        loop = asyncio.get_running_loop()\n        result = await loop.run_in_executor(cpu_pool, expensive_computation, ev.data)\n        return StopEvent(result=result)\n```\n\nKey differences from the I/O case:\n\n- **Use `loop.run_in_executor`** with an explicit executor instead of `asyncio.to_thread` so you can control the pool size and type.\n- **Keep the pool small.** A pool of 1–2 workers means CPU tasks queue up rather than competing for CPU time. Adjust based on your workload and available cores.\n- **Consider a `ProcessPoolExecutor`** for truly CPU-bound work that you want to run outside the GIL. The API is the same — just swap the executor type:\n\n```python\nfrom concurrent.futures import ProcessPoolExecutor\n\ncpu_pool = ProcessPoolExecutor(max_workers=2)\n```\n\nNote that functions submitted to a `ProcessPoolExecutor` must be picklable (top-level functions, not lambdas or closures).\n\n## Summary\n\n| Scenario | Solution | Why |\n|---|---|---|\n| Entire step is synchronous | Define the step as `def` instead of `async def` | The runtime automatically runs it in a thread pool |\n| Blocking call inside an `async def` step | `await asyncio.to_thread(fn, ...)` | Frees the event loop while I/O completes in a thread |\n| CPU-intensive work | `await loop.run_in_executor(pool, fn, ...)` with a small dedicated pool | Queues heavy computation so it doesn't starve the event loop or other tasks |\n\nThe core principle is straightforward: **never block the asyncio event loop.** For fully synchronous steps, use a plain `def` and let the framework handle threading. For blocking calls within an `async def` step, offload them to a thread or process and `await` the result.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/branches_and_loops.md",
    "content": "---\nsidebar:\n  order: 2\ntitle: Branches and loops\n---\n\nA key feature of Workflows is their enablement of branching and looping logic, more simply and flexibly than graph-based approaches.\n\n## Loops in workflows\n\nTo create a loop, we'll take a `LoopingWorkflow` that randomly loops. It will have a single event that we'll call `LoopEvent` (but it can have any arbitrary name).\n\n```python\nfrom workflows.events import Event\n\nclass LoopEvent(Event):\n    num_loops: int\n```\n\nNow we'll `import random` and modify our `step_one` function to randomly decide either to loop or to continue:\n\n```python\nimport random\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nclass LoopingWorkflow(Workflow):\n    @step\n    async def prepare_input(self, ev: StartEvent) -> LoopEvent:\n        num_loops = random.randint(0, 10)\n        return LoopEvent(num_loops=num_loops)\n\n    @step\n    async def loop_step(self, ev: LoopEvent) -> LoopEvent | StopEvent:\n        if ev.num_loops <= 0:\n            return StopEvent(result=\"Done looping!\")\n\n        return LoopEvent(num_loops=ev.num_loops-1)\n```\n\nLet's visualize this:\n\n![A simple loop](./assets/loop.png)\n\nYou can create a loop from any step to any other step by defining the appropriate event types and return types.\n\n## Branches in workflows\n\nClosely related to looping is branching. As you've already seen, you can conditionally return different events. Let's see a workflow that branches into two different paths:\n\n```python\nimport random\nfrom workflows import Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nclass BranchA1Event(Event):\n    payload: str\n\n\nclass BranchA2Event(Event):\n    payload: str\n\n\nclass BranchB1Event(Event):\n    payload: str\n\n\nclass BranchB2Event(Event):\n    payload: str\n\n\nclass BranchWorkflow(Workflow):\n    @step\n    async def start(self, ev: StartEvent) -> BranchA1Event | BranchB1Event:\n        if random.randint(0, 1) == 0:\n            print(\"Go to branch A\")\n            return BranchA1Event(payload=\"Branch A\")\n        else:\n            print(\"Go to branch B\")\n            return BranchB1Event(payload=\"Branch B\")\n\n    @step\n    async def step_a1(self, ev: BranchA1Event) -> BranchA2Event:\n        print(ev.payload)\n        return BranchA2Event(payload=ev.payload)\n\n    @step\n    async def step_b1(self, ev: BranchB1Event) -> BranchB2Event:\n        print(ev.payload)\n        return BranchB2Event(payload=ev.payload)\n\n    @step\n    async def step_a2(self, ev: BranchA2Event) -> StopEvent:\n        print(ev.payload)\n        return StopEvent(result=\"Branch A complete.\")\n\n    @step\n    async def step_b2(self, ev: BranchB2Event) -> StopEvent:\n        print(ev.payload)\n        return StopEvent(result=\"Branch B complete.\")\n```\n\nOur imports are the same as before, but we've created 4 new event types. `start` randomly decides to take one branch or another, and then multiple steps in each branch complete the workflow. Let's visualize this:\n\n![A simple branch](./assets/branching.png)\n\nYou can of course combine branches and loops in any order to fulfill the needs of your application. Later in this tutorial you'll learn how to run multiple branches in parallel using `send_event` and synchronize them using `collect_events`.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/client.md",
    "content": "---\nsidebar:\n  order: 13\ntitle: Python Client\n---\n\nThe `WorkflowClient` class provides a Python interface for interacting with a running `WorkflowServer`. It supports listing workflows, running them synchronously or asynchronously, streaming events, and sending events for human-in-the-loop workflows.\n\n## Installation\n\nThe client is a separate package from the core `llama-index-workflows` library:\n\n```bash\npip install llama-agents-client\n```\n\n## Setup\n\n```python\nfrom llama_agents.client import WorkflowClient\n\nclient = WorkflowClient(base_url=\"http://0.0.0.0:8080\")\n```\n\nYou can also pass a pre-configured `httpx.AsyncClient` instead of a `base_url`:\n\n```python\nimport httpx\n\nhttpx_client = httpx.AsyncClient(base_url=\"http://0.0.0.0:8080\", headers={\"Authorization\": \"Bearer ...\"})\nclient = WorkflowClient(httpx_client=httpx_client)\n```\n\n## Basic Usage\n\n```python\nfrom llama_agents.client import WorkflowClient\nfrom workflows.events import StartEvent\n\nasync def main():\n    client = WorkflowClient(base_url=\"http://0.0.0.0:8080\")\n\n    # Check server health\n    await client.is_healthy()\n\n    # List available workflows\n    workflows = await client.list_workflows()\n    print(workflows)\n\n    # Run a workflow synchronously (blocks until completion)\n    result = await client.run_workflow(\"greet\", start_event=StartEvent(name=\"John\"))\n    print(result.result)\n```\n\n## Async Runs and Event Streaming\n\nFor long-running workflows, start the workflow asynchronously and stream events as they're produced:\n\n```python\nhandler = await client.run_workflow_nowait(\"greet\", start_event=StartEvent(name=\"John\"))\nhandler_id = handler.handler_id\n\nasync for event in client.get_workflow_events(handler_id):\n    print(\"Received:\", event.type, event.value)\n\nresult = await client.get_handler(handler_id)\nprint(f\"Final result: {result.result}\")\n```\n\n### Cursor Behavior (`after_sequence`)\n\n`get_workflow_events` accepts an `after_sequence` parameter that controls where in the event history the stream begins:\n\n| Value | Behavior |\n|---|---|\n| `-1` (default) | Stream **all** events from the beginning, including any that were produced before you started listening. |\n| `\"now\"` | Skip all existing events and only stream events produced **after** the request arrives. |\n| Any integer `N` | Stream events with sequence number greater than `N`. |\n\nThis is useful for different use cases:\n\n- **Full replay** (`-1`): You want to see every event from a run, even if it's already in progress or completed.\n- **Live tail** (`\"now\"`): You started the workflow yourself and only care about new events going forward. If the workflow has already completed and all events have been produced, the server responds with HTTP 204 and the stream ends immediately.\n- **Resume from checkpoint** (integer): You previously disconnected and want to resume from the last event you saw.\n\nTo access the stream position, use `get_workflow_events` which returns an `EventStream` object with a `last_sequence` property:\n\n```python\nstream = client.get_workflow_events(handler_id)\nasync for event in stream:\n    print(event.type, \"at sequence\", stream.last_sequence)\n\n# Save the position for later\nsaved_sequence = stream.last_sequence\n```\n\nYou can then resume from a saved position:\n\n```python\nstream = client.get_workflow_events(handler_id, after_sequence=saved_sequence)\nasync for event in stream:\n    print(event)\n```\n\nOr skip existing events and only receive new ones:\n\n```python\nstream = client.get_workflow_events(handler_id, after_sequence=\"now\")\nasync for event in stream:\n    print(event)\n```\n\n`get_workflow_events` automatically reconnects from the last received sequence on connection drops (up to `max_reconnect_attempts`, default 3).\n\n## Human-in-the-Loop\n\nFor workflows that require external input, use event streaming combined with `send_event`:\n\n```python\nfrom workflows import Workflow, step\nfrom workflows.context import Context\nfrom workflows.events import (\n    StartEvent,\n    StopEvent,\n    InputRequiredEvent,\n    HumanResponseEvent,\n)\nfrom llama_agents.server import WorkflowServer\n\nclass RequestEvent(InputRequiredEvent):\n    prompt: str\n\nclass ResponseEvent(HumanResponseEvent):\n    response: str\n\nclass OutEvent(StopEvent):\n    output: str\n\nclass HumanInTheLoopWorkflow(Workflow):\n    @step\n    async def prompt_human(self, ev: StartEvent, ctx: Context) -> RequestEvent:\n        return RequestEvent(prompt=\"What is your name?\")\n\n    @step\n    async def greet_human(self, ev: ResponseEvent) -> OutEvent:\n        return OutEvent(output=f\"Hello, {ev.response}\")\n\nserver = WorkflowServer()\nserver.add_workflow(\"human\", HumanInTheLoopWorkflow(timeout=1000))\nawait server.serve(\"0.0.0.0\", \"8080\")\n```\n\nThen on the client side:\n\n```python\nfrom llama_agents.client import WorkflowClient\n\nclient = WorkflowClient(base_url=\"http://0.0.0.0:8080\")\nhandler = await client.run_workflow_nowait(\"human\")\nhandler_id = handler.handler_id\n\nasync for event in client.get_workflow_events(handler_id):\n    # load_event() reconstructs the typed Event class by qualified name\n    loaded = event.load_event()\n    if isinstance(loaded, RequestEvent):\n        print(\"Workflow is requiring human input:\", loaded.prompt)\n        name = input(\"Reply here: \")\n        await client.send_event(\n            handler_id=handler_id,\n            event=ResponseEvent(response=name.capitalize().strip()),\n        )\n\nresult = await client.get_handler(handler_id)\nres = OutEvent.model_validate(result.result)\nprint(\"Received final message:\", res.output)\n```\n\n`load_event()` works automatically when the event class is importable by its qualified name. You can also pass a `registry` list to resolve against: `event.load_event(registry=[RequestEvent, ResponseEvent])`. If you don't need typed events, the raw `event.type` and `event.value` dict are always available.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/concurrent_execution.md",
    "content": "---\nsidebar:\n  order: 5\ntitle: Concurrent execution of workflows\n---\n\nIn addition to looping, branching, and streaming, workflows can run steps concurrently. This is useful when you have multiple steps that can be run independently of each other and they have time-consuming operations that they `await`, allowing other steps to run in parallel.\n\n## Emitting multiple events\n\nTo emit multiple events to trigger multiple steps, you can use `ctx.send_event()`:\n\n```python\nimport asyncio\nimport random\nfrom workflows import Workflow, Context, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nclass StepTwoEvent(Event):\n    query: str\n\nclass ParallelFlow(Workflow):\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> StepTwoEvent | None:\n        ctx.send_event(StepTwoEvent(query=\"Query 1\"))\n        ctx.send_event(StepTwoEvent(query=\"Query 2\"))\n        ctx.send_event(StepTwoEvent(query=\"Query 3\"))\n\n    @step(num_workers=4)\n    async def step_two(self, ev: StepTwoEvent) -> StopEvent:\n        print(\"Running slow query \", ev.query)\n        await asyncio.sleep(random.randint(0, 5))\n\n        return StopEvent(result=ev.query)\n```\n\nIn this example, our `start` step emits 3 `StepTwoEvent`s. The `step_two` step is decorated with `num_workers=4`, which tells the workflow to run up to 4 instances of this step concurrently (this is the default).\n\n## Collecting events\n\nIf you execute the previous example, you'll note that the workflow stops after whichever query is first to complete. Sometimes that's useful, but other times you'll want to wait for all your slow operations to complete before moving on to another step. You can do this using `collect_events`:\n\n```python\nimport asyncio\nimport random\nfrom workflows import Workflow, Context, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nclass StepTwoEvent(Event):\n    query: str\n\nclass StepThreeEvent(Event):\n    result: str\n\nclass ConcurrentFlow(Workflow):\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> StepTwoEvent | None:\n        ctx.send_event(StepTwoEvent(query=\"Query 1\"))\n        ctx.send_event(StepTwoEvent(query=\"Query 2\"))\n        ctx.send_event(StepTwoEvent(query=\"Query 3\"))\n\n    @step(num_workers=4)\n    async def step_two(self, ctx: Context, ev: StepTwoEvent) -> StepThreeEvent:\n        print(\"Running query \", ev.query)\n        await asyncio.sleep(random.randint(1, 5))\n        return StepThreeEvent(result=ev.query)\n\n    @step\n    async def step_three(\n        self, ctx: Context, ev: StepThreeEvent\n    ) -> StopEvent | None:\n        # wait until we receive 3 events\n        result = ctx.collect_events(ev, [StepThreeEvent] * 3)\n        if result is None:\n            return None\n\n        # do something with all 3 results together\n        print(result)\n        return StopEvent(result=\"Done\")\n```\n\nThe `collect_events` method lives on the `Context` and takes the event that triggered the step and an array of event types to wait for. In this case, we are awaiting 3 events of the same `StepThreeEvent` type.\n\nThe `step_three` step is fired every time a `StepThreeEvent` is received, but `collect_events` will return `None` until all 3 events have been received. At that point, the step will continue and you can do something with all 3 results together.\n\nThe `result` returned from `collect_events` is an array of the events that were collected, in the order that they were received.\n\n## Multiple event types\n\nOf course, you do not need to wait for the same type of event. You can wait for any combination of events you like, such as in this example:\n\n```python\nimport asyncio\nfrom workflows import Workflow, Context, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nclass StepAEvent(Event):\n    query: str\n\nclass StepBEvent(Event):\n    query: str\n\nclass StepCEvent(Event):\n    query: str\n\nclass StepACompleteEvent(Event):\n    result: str\n\nclass StepBCompleteEvent(Event):\n    result: str\n\nclass StepCCompleteEvent(Event):\n    result: str\n\n\nclass ConcurrentFlow(Workflow):\n    @step\n    async def start(\n        self, ctx: Context, ev: StartEvent\n    ) -> StepAEvent | StepBEvent | StepCEvent | None:\n        ctx.send_event(StepAEvent(query=\"Query 1\"))\n        ctx.send_event(StepBEvent(query=\"Query 2\"))\n        ctx.send_event(StepCEvent(query=\"Query 3\"))\n\n    @step\n    async def step_a(self, ctx: Context, ev: StepAEvent) -> StepACompleteEvent:\n        print(\"Doing something A-ish\")\n        return StepACompleteEvent(result=ev.query)\n\n    @step\n    async def step_b(self, ctx: Context, ev: StepBEvent) -> StepBCompleteEvent:\n        print(\"Doing something B-ish\")\n        return StepBCompleteEvent(result=ev.query)\n\n    @step\n    async def step_c(self, ctx: Context, ev: StepCEvent) -> StepCCompleteEvent:\n        print(\"Doing something C-ish\")\n        return StepCCompleteEvent(result=ev.query)\n\n    @step\n    async def step_three(\n        self,\n        ctx: Context,\n        ev: StepACompleteEvent | StepBCompleteEvent | StepCCompleteEvent,\n    ) -> StopEvent:\n        print(\"Received event \", ev.result)\n\n        # wait until we receive 3 events\n        if (\n            ctx.collect_events(\n                ev,\n                [StepCCompleteEvent, StepACompleteEvent, StepBCompleteEvent],\n            )\n            is None\n        ):\n            return None\n\n        # do something with all 3 results together\n        return StopEvent(result=\"Done\")\n```\n\nThere are several changes we've made to handle multiple event types:\n\n- `start` is now declared as emitting 3 different event types\n- `step_three` is now declared as accepting 3 different event types\n- `collect_events` now takes an array of the event types to wait for\n\nNote that the order of the event types in the array passed to `collect_events` is important. The events will be returned in the order they are passed to `collect_events`, regardless of when they were received.\n\nThe visualization of this workflow is quite pleasing:\n\n![A concurrent workflow](./assets/different_events.png)\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/customizing_entry_exit_points.md",
    "content": "---\nsidebar:\n  order: 7\ntitle: Customizing entry and exit points\n---\n\nMost of the times, relying on the default entry and exit points we have seen in the [Getting Started](/python/llamaagents/workflows/) section is enough.\nHowever, workflows support custom events where you normally would expect `StartEvent` and `StopEvent`, let's see how.\n\n## Using a custom `StartEvent`\n\nWhen we call the `run()` method on a workflow instance, the keyword arguments passed become fields of a `StartEvent`\ninstance that's automatically created under the hood. In case we want to pass complex data to start a workflow, this\napproach might become cumbersome, and it's when we can introduce a custom start event.\n\nTo be able to use a custom start event, the first step is creating a custom class that inherits from `StartEvent`:\n\n```python\nfrom pathlib import Path\n\nfrom workflows.events import StartEvent\nfrom llama_index.indices.managed.llama_cloud import LlamaCloudIndex\nfrom llama_index.llms.openai import OpenAI\n\n\nclass MyCustomStartEvent(StartEvent):\n    a_string_field: str\n    a_path_to_somewhere: Path\n    an_index: LlamaCloudIndex\n    an_llm: OpenAI\n```\n\nAll we have to do now is using `MyCustomStartEvent` as event type in the steps that act as entry points.\nTake this artificially complex step for example:\n\n```python\nclass JokeFlow(Workflow):\n    ...\n\n    @step\n    async def generate_joke_from_index(\n        self, ev: MyCustomStartEvent\n    ) -> JokeEvent:\n        # Build a query engine using the index and the llm from the start event\n        query_engine = ev.an_index.as_query_engine(llm=ev.an_llm)\n        topic = await query_engine.aquery(\n            f\"What is the closest topic to {ev.a_string_field}\"\n        )\n        # Use the llm attached to the start event to instruct the model\n        prompt = f\"Write your best joke about {topic}.\"\n        response = await ev.an_llm.acomplete(prompt)\n        # Dump the response on disk using the Path object from the event\n        ev.a_path_to_somewhere.write_text(str(response))\n        # Finally, pass the JokeEvent along\n        return JokeEvent(joke=str(response))\n```\n\nWe could still pass the fields of `MyCustomStartEvent` as keyword arguments to the `run` method of our workflow, but\nthat would be, again, cumbersome. A better approach is to use pass the event instance through the `start_event`\nkeyword argument like this:\n\n```python\ncustom_start_event = MyCustomStartEvent(...)\nw = JokeFlow(timeout=60, verbose=False)\nresult = await w.run(start_event=custom_start_event)\nprint(str(result))\n```\n\nThis approach makes the code cleaner and more explicit and allows autocompletion in IDEs to work properly.\n\n## Using a custom `StopEvent`\n\nSimilarly to `StartEvent`, relying on the built-in `StopEvent` works most of the times but not always. In fact, when we\nuse `StopEvent`, the result of a workflow must be set to the `result` field of the event instance. Since a result can\nbe any Python object, the `result` field of `StopEvent` is typed as `Any`, losing any advantage from the typing system.\nAdditionally, returning more than one object is cumbersome: we usually stuff a bunch of unrelated objects into a\ndictionary that we then assign to `StopEvent.result`.\n\nFirst step to support custom stop events, we need to create a subclass of `StopEvent`:\n\n```python\nfrom workflows.events import StopEvent\n\n\nclass MyStopEvent(StopEvent):\n    critique: CompletionResponse\n```\n\nWe can now replace `StopEvent` with `MyStopEvent` in our workflow:\n\n```python\nclass JokeFlow(Workflow):\n    ...\n\n    @step\n    async def critique_joke(self, ev: JokeEvent) -> MyStopEvent:\n        joke = ev.joke\n\n        prompt = f\"Give a thorough analysis and critique of the following joke: {joke}\"\n        response = await self.llm.acomplete(prompt)\n        return MyStopEvent(critique=response)\n\n    ...\n```\n\nThe one important thing we need to remember when using a custom stop events, is that the result of a workflow run\nwill be the instance of the event:\n\n```python\nw = JokeFlow(timeout=60, verbose=False)\n# Warning! `result` now contains an instance of MyStopEvent!\nresult = await w.run(topic=\"pirates\")\n# We can now access the event fields as any normal Event\nprint(result.critique.text)\n```\n\nThis approach takes advantage of the Python typing system, is friendly to autocompletion in IDEs and allows\nintrospection from outer applications that now know exactly what a workflow run will return.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/dbos.md",
    "content": "---\nsidebar:\n  order: 14\ntitle: DBOS Durable Execution\n---\n\nThe [durable workflows](/python/llamaagents/workflows/durable_workflows) page shows how to make workflows survive restarts and errors using manual context snapshots. The `llama-agents-dbos` package removes that manual work by plugging a [DBOS](https://www.dbos.dev/)-backed runtime into your workflows. Every step transition is persisted automatically, so a crashed workflow resumes exactly where it left off — no snapshot code required.\n\n## Installation\n\n```bash\npip install llama-agents-dbos\n```\n\n## Quick Start — Standalone Durable Workflow\n\nThe simplest way to use DBOS is with SQLite (zero external dependencies). Define a workflow as usual, pass a `DBOSRuntime`, and your state is persisted automatically.\n\n```python\nimport asyncio\n\nfrom dbos import DBOS\nfrom llama_agents.dbos import DBOSRuntime\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\n# 1. Configure DBOS — SQLite by default\nDBOS(config={\"name\": \"counter-example\", \"run_admin_server\": False})\n\n\n# 2. Define events and workflow (nothing DBOS-specific here)\nclass Tick(Event):\n    count: int = Field(description=\"Current count\")\n\n\nclass CounterResult(StopEvent):\n    final_count: int = Field(description=\"Final counter value\")\n\n\nclass CounterWorkflow(Workflow):\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> Tick:\n        await ctx.store.set(\"count\", 0)\n        print(\"[Start] Initializing counter to 0\")\n        return Tick(count=0)\n\n    @step\n    async def increment(self, ctx: Context, ev: Tick) -> Tick | CounterResult:\n        count = ev.count + 1\n        await ctx.store.set(\"count\", count)\n        print(f\"[Tick {count:2d}] count = {count}\")\n\n        if count >= 20:\n            return CounterResult(final_count=count)\n\n        await asyncio.sleep(0.5)\n        return Tick(count=count)\n\n\n# 3. Create runtime, attach to workflow, and launch\nruntime = DBOSRuntime()\nworkflow = CounterWorkflow(runtime=runtime)\n\n\nasync def main() -> None:\n    await runtime.launch()\n    result = await workflow.run(run_id=\"counter-run-1\")\n    print(f\"Result: final_count = {result.final_count}\")\n\n\nasyncio.run(main())\n```\n\nIf you kill the process mid-run (e.g. Ctrl+C at tick 8), calling `workflow.run(run_id=\"counter-run-1\")` again will resume from tick 8 instead of restarting from zero.\n\n| Persists over `run` calls | ✅ |\n| --- | --- |\n| Persists over process restarts | ✅ |\n| Survives runtime errors | ✅ |\n\n## Durable Workflow Server\n\n`DBOSRuntime` integrates with `WorkflowServer` so every workflow you serve gets durable execution out of the box. The runtime provides both the persistence store and the server runtime:\n\n```python\nimport asyncio\n\nfrom dbos import DBOS\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import WorkflowServer\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nDBOS(config={\"name\": \"quickstart\", \"run_admin_server\": False})\n\n\nclass Tick(Event):\n    count: int = Field(description=\"Current count\")\n\n\nclass CounterResult(StopEvent):\n    final_count: int = Field(description=\"Final counter value\")\n\n\nclass CounterWorkflow(Workflow):\n    \"\"\"Counts to 5, emitting stream events along the way.\"\"\"\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> Tick:\n        return Tick(count=0)\n\n    @step\n    async def tick(self, ctx: Context, ev: Tick) -> Tick | CounterResult:\n        count = ev.count + 1\n        ctx.write_event_to_stream(Tick(count=count))\n        print(f\"  tick {count}\")\n        await asyncio.sleep(0.5)\n        if count >= 5:\n            return CounterResult(final_count=count)\n        return Tick(count=count)\n\n\nasync def main() -> None:\n    runtime = DBOSRuntime()\n\n    server = WorkflowServer(\n        workflow_store=runtime.create_workflow_store(),\n        runtime=runtime.build_server_runtime(),\n    )\n    server.add_workflow(\"counter\", CounterWorkflow(runtime=runtime))\n\n    print(\"Serving on http://localhost:8000\")\n    print(\"Try: curl -X POST http://localhost:8000/workflows/counter/run\")\n    await server.start()\n    try:\n        await server.serve(host=\"0.0.0.0\", port=8000)\n    finally:\n        await server.stop()\n\n\nasyncio.run(main())\n```\n\nThe workflow debugger UI at `http://localhost:8000/` works exactly the same as with the default runtime — DBOS is transparent to the server layer.\n\n## Idle Release\n\nLong-running workflows that wait for external input (human-in-the-loop, webhooks, etc.) can sit idle in memory for extended periods. The `idle_timeout` parameter tells the DBOS runtime to release idle workflows from memory and resume them automatically when new events arrive:\n\n```python\nimport asyncio\n\nfrom dbos import DBOS\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import WorkflowServer\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\n\nDBOS(config={\"name\": \"idle-release-demo\", \"run_admin_server\": False})\n\n\nclass AskName(InputRequiredEvent):\n    prompt: str = Field(default=\"What is your name?\")\n\n\nclass UserInput(HumanResponseEvent):\n    response: str = Field(default=\"\")\n\n\nclass GreeterWorkflow(Workflow):\n    @step\n    async def ask(self, ctx: Context, ev: StartEvent) -> AskName:\n        return AskName()\n\n    @step\n    async def greet(self, ctx: Context, ev: UserInput) -> StopEvent:\n        return StopEvent(result={\"greeting\": f\"Hello, {ev.response}!\"})\n\n\nasync def main() -> None:\n    runtime = DBOSRuntime()\n\n    server = WorkflowServer(\n        workflow_store=runtime.create_workflow_store(),\n        # Release workflows after 30 seconds of inactivity\n        runtime=runtime.build_server_runtime(idle_timeout=30.0),\n    )\n    server.add_workflow(\"greeter\", GreeterWorkflow(runtime=runtime))\n\n    await server.start()\n    try:\n        await server.serve(host=\"0.0.0.0\", port=8000)\n    finally:\n        await server.stop()\n\n\nasyncio.run(main())\n```\n\nWhen the greeter workflow emits `AskName` and no input arrives within 30 seconds, the runtime releases it from memory. Once a `UserInput` event is sent (via `POST /events/{handler_id}`), the runtime transparently restores the workflow from the database and delivers the event. The caller never knows the workflow was released.\n\n## Using Postgres for Multi-Replica Deployments\n\nSQLite works well for single-process setups. For production deployments that need multiple server replicas, switch to Postgres. Each replica must have a unique `executor_id`:\n\n```python\nfrom dbos import DBOS\n\nDBOS(config={\n    \"name\": \"my-app\",\n    \"system_database_url\": \"postgresql://user:pass@localhost:5432/mydb\",\n    \"run_admin_server\": False,\n    \"executor_id\": \"replica-1\",  # unique per replica\n})\n```\n\nSee the `examples/dbos/server_replicas.py` example for a complete multi-replica demo. For a deeper look at how replicas coordinate, see the [DBOS architecture overview](https://docs.dbos.dev/architecture).\n\nFor production multi-replica deployments, [DBOS Conductor](https://docs.dbos.dev/production/conductor) adds auto-scaling and monitoring dashboards on top of the core runtime.\n\n## Execution Model\n\nUnderstanding the DBOS execution model helps you write workflows that behave correctly across restarts and replicas.\n\n### Replica ownership\n\nEach replica is identified by its `executor_id` and **owns** every workflow it starts. A workflow and all of its steps run in the same process — there is no distribution of individual steps across replicas. This means your steps can safely rely on local state like in-memory caches, local files, or process-level singletons. The trade-off is that a single workflow's workload cannot be spread across multiple replicas.\n\n### Journaling and replay\n\nStep completions and stream events are journaled to the database. When a workflow resumes after a crash or an idle release, the runtime replays the journal to rebuild the workflow's `Context` and `store`, then continues from the last recorded step.\n\nBecause recovery is replay-based, **steps may execute more than once** if they were interrupted before the journal entry was committed. Design steps to be idempotent where possible, or use the context store to track progress within a step (as shown in the [durable workflows](/python/llamaagents/workflows/durable_workflows) page).\n\n### Scaling and draining\n\nReplica IDs and replica counts must be stable. If you scale down and remove a replica, any workflows that replica owned will be abandoned until that `executor_id` comes back. Before removing a replica, drain it by letting its in-flight workflows complete and not routing new work to it.\n\n[DBOS Conductor](https://docs.dbos.dev/production/conductor) handles this automatically — it detects drained or timed-out replicas via heartbeats and re-assigns their in-flight workflows to healthy replicas.\n\n### Code changes and versioning\n\nSince resumption is based on journal replay, changing a workflow's code while historical runs are still in progress can cause non-determinism — for example, a step that now accepts a different set of events than when the run was originally started. To avoid this:\n- **Drain in-flight workflows** before deploying code changes, or\n- **Register the updated workflow under a new name** so that old runs continue against the original code and new runs use the updated version\n\nA workflow's name defaults to its module-qualified class name (e.g. `my_app.CounterWorkflow`). You can set it explicitly with the `workflow_name` parameter:\n\n```python\nwf = CounterWorkflow(runtime=runtime, workflow_name=\"counter-v2\")\n```\n\nWhen using a server, the name passed to `add_workflow` is the HTTP route name, independent of the workflow's internal name:\n\n```python\nserver.add_workflow(\"counter-v2\", CounterWorkflow(runtime=runtime, workflow_name=\"counter-v2\"))\n```\n\n### Event streaming behavior\n\nWhen using `handler.stream_events()` in-process (outside of a server), DBOS streams are replayed from the beginning on each call. This means you will receive all events the workflow has ever emitted, not just new ones.\n\nThe [workflow server](/python/llamaagents/workflows/deployment) uses a cursor-based approach instead — its `GET /events/{handler_id}` endpoint tracks position so each consumer only receives events once.\n\n### Crash recovery\n\nWhen a replica restarts, DBOS automatically detects and relaunches any incomplete workflows belonging to its `executor_id`. No manual intervention is required — the replica picks up where it left off by replaying its journal.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/deployment.md",
    "content": "---\nsidebar:\n  order: 12\ntitle: Run Your Workflow as a Server\n---\n\nThe `workflows` library includes a `WorkflowServer` class that allows you to easily expose your workflows over an HTTP API. This provides a flexible way to run and manage workflows from any HTTP-capable client.\n\nAdditionally, the `WorkflowServer` is deployed with a static debugging application that allows you to visualize, run, and debug workflows. This is automatically mounted at the root `/` path of the running server.\n\n## Installation\n\nThe workflow server is a separate package from the core `llama-index-workflows` library:\n\n```bash\npip install llama-agents-server\n```\n\n## Programmatic Usage\n\nYou can create a server, add your workflows, and run it programmatically. This is useful when you want to embed the\nworkflow server in a larger application.\n\nFirst, create a Python file (e.g., `my_server.py`):\n\n```python\n# my_server.py\nimport asyncio\nfrom workflows import Workflow, step\nfrom workflows.context import Context\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_agents.server import WorkflowServer\n\n\nclass StreamEvent(Event):\n    sequence: int\n\n\n# Define a simple workflow\nclass GreetingWorkflow(Workflow):\n    @step\n    async def greet(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        for i in range(3):\n            ctx.write_event_to_stream(StreamEvent(sequence=i))\n            await asyncio.sleep(0.3)\n\n        name = ev.get(\"name\", \"World\")\n        return StopEvent(result=f\"Hello, {name}!\")\n\ngreet_wf = GreetingWorkflow()\n\n\n# Create a server instance\nserver = WorkflowServer()\n\n# Add the workflow to the server\nserver.add_workflow(\"greet\", greet_wf)\n\n# To run the server programmatically (e.g., from your own script)\n# import asyncio\n#\n# async def main():\n#     await server.serve(host=\"0.0.0.0\", port=8080)\n#\n# if __name__ == \"__main__\":\n#     asyncio.run(main())\n```\n\n## Command-Line Interface (CLI)\n\nThe library also provides a convenient CLI to run a server from a file containing a `WorkflowServer` instance.\n\nGiven the `my_server.py` file from the example above, you can start the server with the following command:\n\n```bash\npython -m workflows.server my_server.py\n```\n\nThe server will start on `0.0.0.0:8080` by default. You can configure the host and port using the\n`WORKFLOWS_PY_SERVER_HOST` and `WORKFLOWS_PY_SERVER_PORT` environment variables.\n\n## Workflow Debugger UI\n\nThe `WorkflowServer` is deployed with a static debugging application that allows you to visualize, run, and debug workflows. This is automatically mounted at the root `/` path of the running server.\n\n![Workflow Debugger UI](./assets/ui_sample.png)\n\nThe Workflow Debugging UI offers a few key features:\n- **Workflow Visualization**: The UI provides a visual representation of the workflow's structure both statically and while it is running. You can re-arrange the nodes as needed.\n- **Automatic schema detection**: If you customize the schemas of your start/stop events, or internal events, the UI will automatically detect and display UI appropriate for the schema.\n- **Human-in-the-loop**: While a workflow is running, you can send any event into the workflow. This is useful for workflows that rely on human input to continue execution. See the `Send Event` button on the top of the events log.\n- **Events Log**: All streamed events are logged in the UI, allowing you to inspect the workflow's execution in real-time in the right side-panel.\n- **Multiple Runs**: Debug and compare multiple runs. Each time you run a workflow, the left-side panel tracks that run.\n- **Multiple Workflows**: The UI will let you run any workflow that is mounted within the `WorkflowServer`.\n\n### Handling \"Hidden\" Events\n\nSometimes, workflows will send/accept events that are annotated in the workflow (like using `ctx.wait_for_event()`). In these cases, you can still inform the UI about these events using the `server.add_workflow(..., additional_events=[...])` API to inject those events. Then, UI elements like the `Send Event` functionality will be aware of these events.\n\n## API Endpoints\n\nThe `WorkflowServer` exposes the following RESTful endpoints:\n\n| Method | Path                           | Description                                                                                             |\n|--------|--------------------------------|---------------------------------------------------------------------------------------------------------|\n| `GET`  | `/health`                      | Returns a health check response (`{\"status\": \"healthy\"}`).                                               |\n| `GET`  | `/workflows`                   | Lists the names of all registered workflows.                                                            |\n| `POST` | `/workflows/{name}/run`        | Runs the specified workflow synchronously and returns the final result.                                 |\n| `POST` | `/workflows/{name}/run-nowait` | Starts the specified workflow asynchronously and returns a `handler_id`.                                |\n| `GET`  | `/handlers/{handler_id}`        | Retrieves the result of an asynchronously run workflow. Returns `202 Accepted` if still running, `500` if the workflow failed, `200` if the workflow completed.       |\n| `GET`  | `/events/{handler_id}`         | Streams all events from a running workflow as newline-delimited JSON (`application/x-ndjson` and `text/event-stream` if SSE are enabled).          |\n| `POST`  | `/events/{handler_id}`         | Sends an event to a workflow during its execution (useful for human-in-the-loop)         |\n| `GET`  | `/handlers`         |  Get all the workflow handlers (running and completed)        |\n| `POST`  | `/handlers/{handler_id}/cancel`         | Stop and cancel the execution of a workflow.        |\n\n\n### Running a Workflow (`/run`)\n\nTo run a workflow and wait for its completion, send a `POST` request to `/workflows/{name}/run`.\n\n**Request Body:**\n\n```json\n{\n  \"start_event\": {},\n  \"context\": {},\n  \"handler_id\": \"\",\n}\n```\n\n- `start_event`: serialized representation of a StartEvent or a subclass of it. Using this as a workflow input is recommended.\n- `context`: serialized representation of the workflow context\n- `handler_id`: workflow handler identifier to continue from a previous completed run.\n\n**Successful Response (`200 OK`):**\n\n```json\n{\n  \"result\": \"The workflow has been successfully run\"\n}\n```\n\n### Running a Workflow Asynchronously (`/run-nowait`)\n\nTo start a workflow without waiting for it to finish, use the `/run-nowait` endpoint.\n\n**Request Body:**\n\n```json\n{\n  \"start_event\": {},\n  \"context\": {},\n  \"handler_id\": \"\"\n}\n```\n\nThe request body has the same arguments as the `/run` endpoint.\n\n**Successful Response (`200 OK`):**\n\n```json\n{\n  \"handler_id\": \"someUniqueId123\",\n  \"status\": \"started\"\n}\n```\n\nYou can then use the `handler_id` to check for the result or stream events.\n\n## Streaming events (`GET /events/{handler_id}`)\n\n> _This endpoint only works if you previously started a workflow asynchronously with `/run-nowait`_\n\nTo stream events either as Server-Sent Events (SSE) or as multi-line JSON payloads, you can send a request to the `/events/{handler_id}` endpoint with the handler ID of an asynchronous workflow run you previously started.\n\n**Query parameters**\n\n- `sse` (set to either \"true\" or \"false\", not required): stream the events as Server Sent Events (`text/event-stream`) if true, else stream them as a multi-line JSON payload (`application/x-ndjson`). Defaults to true.\n- `acquire_timeout` (a float-convertible string, not required): timeout for acquiring the lock to iterate over events\n- `include_internal` (set to either \"true\" or \"false\", not required): stream internal workfloe events if set to true. Defaults to false.\n- `include_qualified_name` (set to either \"true\" or \"false\", not required): include the qualified name of the event in the response body. Defaults to true.\n\n**Example request**\n\n```bash\ncurl http://localhost:80/events/someUniqueId123?sse=false&acquire_timeout=1&include_internal=false&include_qualified_name=true\n```\n\n**Successful response (`200 OK`)**\n\nSingle event payload:\n\n```json\n{\n  \"value\": {\"result\": 12},\n  \"qualified_name\": \"__main__.MathEvent\",\n  \"type\": \"__main__.MathEvent\",\n  \"types\": [\"workflows.events.Event\", \"__main__.MathEvent\"],\n}\n```\n\n**Important considerations**\n\n- Only one reader is allowed to stream the events per workflow run\n- Once the events have been streamed, they cannot be recovered (unless you implemented some persistence logic on the client side)\n\n> _We are working to improve both these aspects, so changes in the server behavior might be expected_\n\n## Getting the result from a workflow execution (`/results/{handler_id}`)\n\n> _This endpoint only works if you previously started a workflow asynchronously with `/run-nowait`_\n\nTo get the result of a previously started asynchronous workflow run, you can use the `/results/{handler_id}` endpoint passing the handler ID of the run.\n\n**Example request**\n\n```bash\ncurl http://localhost:80/results/someUniqueId123\n```\n\n**Successful response (`200 OK`)**\n\n```json\n{\n  \"handler_id\": \"someUniqueId123\",\n  \"workflow_name\": \"math_workflow\",\n  \"run_id\": \"uniqueRunId456\",\n  \"error\": null,\n  \"result\": {\n    \"sum\": 15,\n    \"subtraction\": 9,\n    \"multiplication\": 36,\n    \"division\": 4,\n  },\n  \"status\": \"completed\",\n  \"started_at\": \"2024-10-21T14:32:15.123Z\",\n  \"updated_at\": \"2024-10-21T14:45:30.456Z\",\n  \"completed_at\": \"2024-10-21T14:45:30.456Z\"\n}\n```\n\n**Accepted response (`202 ACCEPTED`)**\n\nStatus code `202` is returned when the workflow is still running, and thus has not produce a result yet.\n\n## Sending an event (`POST /events/{handler_id}`)\n\nIn cases where external input is needed for the workflow to run (human in the loop, e.g.), you can send a POST request to the `events/{handler_id}` endpoint with the event data to send (and, optionally, the step of the workflow to send them to) in order to provide said external input.\n\n**Request body**\n\n```json\n{\n  \"event\": {\"__is_pydantic\": true, \"value\": {\"feedback\": \"This is great!\", \"approved\": true}, \"qualified_name\": \"__main__.HumanFeedbackEvent\"},\n  \"step\": \"process_human_feedback\"\n}\n```\n\n- `event`: serialized representation of a workflow Event.\n- `step` (optional): name of the step to send the event to.\n\n**Successful response (`200 OK`)**\n\n```json\n{\n  \"status\": \"sent\"\n}\n```\n\n## Canceling a workflow run (`/handlers/{handler_id}/cancel`)\n\nTo stop a running workflow handler by cancelling its tasks, and optionally removing the associated handler from the persistence store, you can use `/handlers/{handler_id}/cancel`.\n\n**Query parameters**\n\n- `purge` (can be set to either \"true\" or \"false\", not required): whether or not to remove the handler associated with the workflow from the persistence store. Defaults to false.\n\n**Example request**\n\n```bash\ncurl -X POST http://localhost:80/handlers/someUniqueId123/cancel?purge=true\n```\n\n**Successful response (`200 OK`)**\n\n```js\n{\n  \"status\": \"deleted\", // or canceled if purge is false\n}\n```\n\n## Persistence\n\nBy default, `WorkflowServer` uses an in-memory store (`MemoryWorkflowStore`), so all handler state and events are lost when the process restarts. For durable persistence, pass a `workflow_store` backed by a database.\n\n### SQLite\n\nThe simplest option for single-process deployments is `SqliteWorkflowStore`, which persists handler state, events, and results to a local file:\n\n```python\nfrom llama_agents.server import WorkflowServer, SqliteWorkflowStore\n\nstore = SqliteWorkflowStore(db_path=\"workflows.db\")\n\nserver = WorkflowServer(workflow_store=store)\nserver.add_workflow(\"greet\", greet_wf)\n```\n\n### DBOS (Postgres)\n\nFor production deployments that need Postgres-backed persistence, durable execution, and the ability to run distributed workers, use the `DBOSRuntime` from the `llama-agents-dbos` package. This replaces the default runtime with one backed by [DBOS](https://docs.dbos.dev/), providing transactional state management and recovery across process restarts:\n\n```python\nfrom dbos import DBOS\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import WorkflowServer\n\n# Configure DBOS — uses SQLite by default, or set system_database_url for Postgres\nDBOS(config={\"name\": \"my-app\", \"run_admin_server\": False})\n\nruntime = DBOSRuntime()\n\nserver = WorkflowServer(\n    workflow_store=runtime.create_workflow_store(),\n    runtime=runtime.build_server_runtime(),\n)\nserver.add_workflow(\"greet\", GreetingWorkflow())\n```\n\nBy default DBOS uses SQLite (zero setup). To use Postgres, pass a `system_database_url` in the DBOS config. For multi-replica setups, each replica must have a unique `executor_id`:\n\n```python\nDBOS(config={\n    \"name\": \"my-app\",\n    \"system_database_url\": \"postgresql://user:pass@localhost:5432/mydb\",\n    \"run_admin_server\": False,\n    \"executor_id\": \"replica-1\",  # unique per replica\n})\n```\n\nWith Postgres, multiple server replicas can share the same database for distributed execution and recovery. See the `examples/dbos/` directory for a full multi-replica demo.\n\n## Python Client\n\nFor programmatic interaction with a `WorkflowServer`, see the [Python Client](/python/llamaagents/workflows/client) documentation.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/drawing.md",
    "content": "---\nsidebar:\n  order: 8\ntitle: Drawing a Workflow\n---\n\nWorkflows can be visualized, using the power of type annotations in your step definitions.\n\nThere are two main ways to visualize your workflows.\n\n## 1. Converting a Workflow to HTML\n\nFirst install:\n\n```bash\npip install llama-index-utils-workflow\n```\n\nThen import and use:\n\n```python\nfrom llama_index.utils.workflow import (\n    draw_all_possible_flows,\n    draw_most_recent_execution,\n)\n\n# Draw all\ndraw_all_possible_flows(MyWorkflow, filename=\"all_paths.html\")\n\n# Draw an execution\nw = MyWorkflow()\nhandler = w.run(topic=\"Pirates\")\nawait handler\ndraw_most_recent_execution(handler, filename=\"most_recent.html\")\n```\n\n## 2. Using the `workflow-debugger`\n\nWorkflows ship with a [`WorkflowServer`](/python/llamaagents/workflows/deployment) that allows you to convert workflows to API's. As part of the `WorkflowServer`, a debugging UI is provided as the home `/` page.\n\nUsing this server app, you can visualize and run your workflows.\n\n![workflow debugger](./assets/ui_sample.png)\n\nSetting up the server is straightforward:\n\n```python\nimport asyncio\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.server import WorkflowServer\n\nclass MyWorkflow(Workflow):\n    @step\n    async def my_step(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"Done!\")\n\nasync def main():\n    server = WorkflowServer()\n    server.add_workflow(\"my_workflow\", MyWorkflow())\n    await server.serve(\"0.0.0.0\", \"8080\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/durable_workflows.md",
    "content": "---\nsidebar:\n  order: 13\ntitle: Writing durable workflows\n---\n\nWorkflows are ephemeral by default, meaning that once the `run()` method returns its result, the workflow state is lost. A subsequent call to `run()` on the same workflow instance will start from a fresh state.\n\nIf the use case requires to persist the workflow state  across multiple runs and possibly different processes, there are a few strategies that can be used to make workflows more durable.\n\n## Storing data in the workflow instance\n\nWorkflows are regular Python classes, and data can be stored in class or instance variables, so that subsequent `run()` invocations can access it.\n\n```python\nclass DbWorkflow(Workflow):\n    def __init__(self, db: Client, *args, **kwargs):\n        self.db = db\n        super().__init__(*args, **kwargs)\n\n    @step\n    def count(self, ev: StartEvent) -> StopEvent:\n        num_rows = self.db.exec(\"select COUNT(*) from t;\")\n        return StopEvent(result=num_rows)\n```\n\nIn this case, multiple calls to `run()` will reuse the same database client.\n\n| Persists over `run` calls | ✅ |\n| --- | --- |\n| Persists over process restarts | ❌ |\n| Survives runtime errors | ❌ |\n\n## Storing data in the context object\n\nEach workflow comes with a special object responsible for its runtime operations called `Context`. The context instance is available to any step of a workflow and comes with a `store` property that can be used to store and load state data. Using the state store has two major advantages compared to class and instance variables:\n\n- It’s async safe and supports concurrent access\n- It can be serialized\n\n```python\nw = MyWorkflow()\nhandler = w.run()\ncontext = handler.ctx\n# Save the context to a database\ndb.save(\"id\", context.to_dict())\n\n#\n# Restart the Python process...\n#\n\nw = MyWorkflow()\n# Load the context from the database\ncontext = Context.from_dict(w, db.load(\"id\"))\n# Pass the context containing the state to the workflow\nresult = await w.run(ctx=context)\n```\n\n| Persists over `run` calls | ✅ |\n| --- | --- |\n| Persists over process restarts | ✅ |\n| Survives runtime errors | ❌ |\n\n## Using external resources to checkpoint execution\n\nTo avoid any overhead, workflows don’t take snapshots of the current state automatically, so they can’t survive a fatal error on their own. However, any step can rely on some external database like Redis and snapshot the current context on sensitive parts of the code.\n\nFor example, given a long running workflow processing hundreds of documents, we could save the id of the last document successfully processed in the state store:\n\n```python\nclass DurableWorkflow(Workflow):\n    def __init__(self, r: Redis):\n        self.redis = r\n\n    @step\n    async def convert_documents(self, ev: StartEvent, ctx: Context) -> StopEvent:\n        # Get the workflow input\n        document_ids = ev.ids\n        # Get the list of processed documents from the state store\n        converted_ids = await ctx.store.get(\"converted_ids\", default=[])\n        for doc_id in document_ids:\n\t\t        # Ignore documents that were alredy processed\n\t\t        if doc_id in converted_ids:\n\t\t            continue\n            convert()\n            # Update the state store\n            converted_ids.append(doc_id)\n            await ctx.store.set(\"converted_ids\", converted_ids)\n            # Create a snapshot of the current context\n            self.redis.hset(\"ctx\", mapping=ctx.to_dict())\n```\n\nThe workflow will use a Redis collection to store a snapshot of the current context after every conversion. If the process running the workflow crashes, the process can be safely restarted with the same input. In fact, `ctx.store` will contain the list of documents already processed and the `for` loop will be able to skip them and continue to process the remaining work.\n\n### Bonus: inject dependencies into the workflow to reduce boilerplate\n\nUsing the Resource feature of workflows, the Redis client can be injected into the step directly:\n\n```python\ndef get_redis_client(*args, **kwargs):\n\t\t\"\"\"This can be reused across several workflows to reduce boilerplate\"\"\"\n    return Redis(host='localhost', port=6379, decode_responses=True)\n\n\nclass DurableWorkflow(Workflow):\n    @step\n    async def convert_documents(\n        self,\n        ev: StartEvent,\n        ctx: Context,\n        redis: Annotated[Redis, Resource(get_redis_client)]\n    ) -> StopEvent:\n        # Get the workflow input\n        document_ids = ev.ids\n        # Get the list of processed documents from the state store\n        converted_ids = await ctx.store.get(\"converted_ids\", default=[])\n        for doc_id in document_ids:\n\t\t        # Ignore documents that were alredy processed\n\t\t        if doc_id in converted_ids:\n\t\t            continue\n            convert()\n            # Update the state store\n            converted_ids.append(doc_id)\n            await ctx.store.set(\"converted_ids\", converted_ids)\n            # Create a snapshot of the current context\n            redis.hset(\"ctx\", mapping=ctx.to_dict())\n```\n\n| Persists over `run` calls | ✅ |\n| --- | --- |\n| Persists over process restarts | ✅ |\n| Survives runtime errors | ✅ |\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/human_in_the_loop.md",
    "content": "---\nsidebar:\n  order: 6\ntitle: Human in the Loop\n---\n\nSince workflows are so flexible, there are many possible ways to implement human-in-the-loop patterns.\n\nThe easiest way to implement a human-in-the-loop is to use the `InputRequiredEvent` and `HumanResponseEvent` events during event streaming.\n\n```python\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent, InputRequiredEvent, HumanResponseEvent\n\n\nclass HumanInTheLoopWorkflow(Workflow):\n    @step\n    async def step1(self, ev: StartEvent) -> InputRequiredEvent:\n        return InputRequiredEvent(prefix=\"Enter a number: \")\n\n    @step\n    async def step2(self, ev: HumanResponseEvent) -> StopEvent:\n        return StopEvent(result=ev.response)\n\n\n# workflow should work with streaming\nworkflow = HumanInTheLoopWorkflow()\n\nhandler = workflow.run()\nasync for event in handler.stream_events():\n    if isinstance(event, InputRequiredEvent):\n        # here, we can handle human input however you want\n        # this means using input(), websockets, accessing async state, etc.\n        # here, we just use input()\n        response = input(event.prefix)\n        handler.ctx.send_event(HumanResponseEvent(response=response))\n\nfinal_result = await handler\n```\n\nHere, the workflow will wait until the `HumanResponseEvent` is emitted.\n\nIf needed, you can also subclass these two events to add custom payloads.\n\n## Stopping/Resuming Between Human Responses\n\nYou can break out of the event loop and resume later. This is useful when you want to pause the workflow to wait for a human response asynchronously (e.g., from a web request).\n\n```python\nfrom workflows import Context\n\nhandler = workflow.run()\nasync for event in handler.stream_events():\n    if isinstance(event, InputRequiredEvent):\n        # Serialize the context, store it anywhere as a JSON blob\n        ctx_dict = handler.ctx.to_dict()\n        await handler.cancel_run()\n        break\n\n...\n\n# now we handle the human response once it comes in\nresponse = input(event.prefix)\n\nrestored_ctx = Context.from_dict(workflow, ctx_dict)\nhandler = workflow.run(ctx=restored_ctx)\n\n# Send the event to resume the workflow\nhandler.ctx.send_event(HumanResponseEvent(response=response))\n\n# now we resume the workflow streaming with our restored context\nasync for event in handler.stream_events():\n    continue\n\nfinal_result = await handler\n```\n\n## Using `wait_for_event`\n\nAn alternative approach is to use `ctx.wait_for_event()` to wait for input within a single step:\n\n```python\n@step\nasync def ask_user(self, ctx: Context, ev: StartEvent) -> StopEvent:\n    response = await ctx.wait_for_event(\n        HumanResponseEvent,\n        waiter_event=InputRequiredEvent(prefix=\"Enter a number: \"),\n        waiter_id=\"get_number\",\n    )\n    return StopEvent(result=response.response)\n```\n\n**Important**: `wait_for_event` replays all code preceding it whenever the step receives its triggering event _or_ a matching waiting event. The step always runs at least once up to the waiter, which then raises an internal exception to pause execution. Because of this, any code before the `wait_for_event` call must be idempotent (safe to repeat).\n\nDue to this complexity, the event-based approach with separate steps is generally recommended.\n\nSee the [API reference](/python/workflows-api-reference/context/#workflows.context.Context.wait_for_event) for full details.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/index.md",
    "content": "---\nsidebar:\n  order: 1\ntitle: Introduction\n---\n\n## What is a workflow?\n\nA workflow is an event-driven, step-based way to control the execution flow of an application.\n\nYour application is divided into sections called Steps which are triggered by Events, and themselves emit Events which trigger further steps. By combining steps and events, you can create arbitrarily complex flows that encapsulate logic and make your application more maintainable and easier to understand. A step can be anything from a single line of code to a complex agent. They can have arbitrary inputs and outputs, which are passed around by Events.\n\n## Why workflows?\n\nAs generative AI applications become more complex, it becomes harder to manage the flow of data and control the execution of the application. Workflows provide a way to manage this complexity by breaking the application into smaller, more manageable pieces.\n\nOther frameworks and LlamaIndex itself have attempted to solve this problem previously with directed acyclic graphs (DAGs) but these have a number of limitations that workflows do not:\n\n- Logic like loops and branches needed to be encoded into the edges of graphs, which made them hard to read and understand.\n- Passing data between nodes in a DAG created complexity around optional and default values and which parameters should be passed.\n- DAGs did not feel natural to developers trying to developing complex, looping, branching AI applications.\n\nThe event-based pattern and vanilla python approach of Workflows resolves these problems.\n\n\n:::note\nThe Workflows library can be installed standalone, via `pip install llama-index-workflows`. However,\n`llama-index-core` comes with an installation of Workflows included.\n\nIn order to maintain the `llama_index` API stable and avoid breaking changes, when installing `llama-index-core` or\nthe `llama-index` umbrella package, Workflows can be accessed with the import path `llama_index.core.workflow`.\n:::\n\n## Getting Started\n\n:::tip\nWorkflows make async a first-class citizen, and this page assumes you are running in an async environment. What this means for you is setting up your code for async properly. If you are already running in a server like FastAPI, or in a notebook, you can freely use await already!\n\nIf you are running your own python scripts, its best practice to have a single async entry point.\n\n```python\nasync def main():\n    w = MyWorkflow(...)\n    result = await w.run(...)\n    print(result)\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(main())\n```\n:::\n\nAs an illustrative example, let's consider a naive workflow where a joke is generated and then critiqued.\n\n```python\nfrom workflows import Workflow, step\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n)\n\n# `pip install llama-index-llms-openai` if you don't already have it\nfrom llama_index.llms.openai import OpenAI\n\n\nclass JokeEvent(Event):\n    joke: str\n\n\nclass JokeFlow(Workflow):\n    llm = OpenAI(model=\"gpt-4.1\")\n\n    @step\n    async def generate_joke(self, ev: StartEvent) -> JokeEvent:\n        topic = ev.topic\n\n        prompt = f\"Write your best joke about {topic}.\"\n        response = await self.llm.acomplete(prompt)\n        return JokeEvent(joke=str(response))\n\n    @step\n    async def critique_joke(self, ev: JokeEvent) -> StopEvent:\n        joke = ev.joke\n\n        prompt = f\"Give a thorough analysis and critique of the following joke: {joke}\"\n        response = await self.llm.acomplete(prompt)\n        return StopEvent(result=str(response))\n\n\nw = JokeFlow(timeout=60, verbose=False)\nresult = await w.run(topic=\"pirates\")\nprint(str(result))\n```\n\n![joke](./assets/joke.png)\n\nThere's a few moving pieces here, so let's go through this piece by piece.\n\n### Defining Workflow Events\n\n```python\nclass JokeEvent(Event):\n    joke: str\n```\n\nEvents are user-defined pydantic objects. You control the attributes and any other auxiliary methods. In this case, our workflow relies on a single user-defined event, the `JokeEvent`.\n\n### Setting up the Workflow Class\n\n```python\nclass JokeFlow(Workflow):\n    llm = OpenAI(model=\"gpt-4.1\")\n    ...\n```\n\nOur workflow is implemented by subclassing the `Workflow` class. For simplicity, we attached a static `OpenAI` llm instance.\n\n### Workflow Entry Points\n\n```python\nclass JokeFlow(Workflow):\n    ...\n\n    @step\n    async def generate_joke(self, ev: StartEvent) -> JokeEvent:\n        topic = ev.topic\n\n        prompt = f\"Write your best joke about {topic}.\"\n        response = await self.llm.acomplete(prompt)\n        return JokeEvent(joke=str(response))\n\n    ...\n```\n\nHere, we come to the entry-point of our workflow. While most events are use-defined, there are two special-case events,\nthe `StartEvent` and the `StopEvent` that the framework provides out of the box. Here, the `StartEvent` signifies where\nto send the initial workflow input.\n\nThe `StartEvent` is a bit of a special object since it can hold arbitrary attributes. Here, we accessed the topic with\n`ev.topic`, which would raise an error if it wasn't there. You could also do `ev.get(\"topic\")` to handle the case where\nthe attribute might not be there without raising an error.\n\nFor further type safety, you can also subclass the `StartEvent`.\n\nAt this point, you may have noticed that we haven't explicitly told the workflow what events are handled by which steps.\nInstead, the `@step` decorator is used to infer the input and output types of each step. Furthermore, these inferred\ninput and output types are also used to verify for you that the workflow is valid before running!\n\n### Workflow Exit Points\n\n```python\nclass JokeFlow(Workflow):\n    ...\n\n    @step\n    async def critique_joke(self, ev: JokeEvent) -> StopEvent:\n        joke = ev.joke\n\n        prompt = f\"Give a thorough analysis and critique of the following joke: {joke}\"\n        response = await self.llm.acomplete(prompt)\n        return StopEvent(result=str(response))\n\n    ...\n```\n\nHere, we have our second, and last step, in the workflow. We know its the last step because the special `StopEvent` is\nreturned. When the workflow encounters a returned `StopEvent`, it immediately stops the workflow and returns whatever\nwe passed in the `result` parameter.\n\nIn this case, the result is a string, but it could be a dictionary, list, or any other object.\n\nYou can also subclass the `StopEvent` class for further type safety.\n\n### Running the Workflow\n\n```python\nw = JokeFlow(timeout=60, verbose=False)\nresult = await w.run(topic=\"pirates\")\nprint(str(result))\n```\n\nLastly, we create and run the workflow. There are some settings like timeouts (in seconds) and verbosity to help with\ndebugging.\n\nThe `.run()` method is async, so we use await here to wait for the result. The keyword arguments passed to `run()` will\nbecome fields of the special `StartEvent` that will be automatically emitted and start the workflow. As we have seen,\nin this case `topic` will be accessed from the step with `ev.topic`.\n\n## Examples\n\nTo help you become more familiar with the workflow concept and its features, LlamaIndex documentation offers example notebooks that you can run for hands-on learning:\n\n- [Common Workflow Patterns](/python/examples/workflow/workflows_cookbook/) walks you through common usage patterns\nlike looping and state management using simple workflows. It's usually a great place to start.\n- [RAG + Reranking](/python/examples/workflow/rag/) shows how to implement a real-world use case with a fairly\nsimple workflow that performs both ingestion and querying.\n- [Citation Query Engine](/python/examples/workflow/citation_query_engine/) similar to RAG + Reranking, the\nnotebook focuses on how to implement intermediate steps in between retrieval and generation. A good example of how to\nuse the [`Context`](#working-with-global-context-state) object in a workflow.\n- [Corrective RAG](/python/examples/workflow/corrective_rag_pack/) adds some more complexity on top of a RAG\nworkflow, showcasing how to query a web search engine after an evaluation step.\n- [Utilizing Concurrency](/python/examples/workflow/parallel_execution/) explains how to manage the parallel\nexecution of steps in a workflow, something that's important to know as your workflows grow in complexity.\n\nRAG applications are easy to understand and offer a great opportunity to learn the basics of workflows. However, more complex agentic scenarios involving tool calling, memory, and routing are where workflows excel.\n\nThe examples below highlight some of these use-cases.\n\n- [ReAct Agent](/python/examples/workflow/react_agent/) is obviously the perfect example to show how to implement\ntools in a workflow.\n- [Function Calling Agent](/python/examples/workflow/function_calling_agent/) is a great example of how to use the\nLlamaIndex framework primitives in a workflow, keeping it small and tidy even in complex scenarios like function\ncalling.\n- [CodeAct Agent](/python/examples/agent/from_scratch_code_act_agent/) is a great example of how to create a CodeAct Agent from scratch.\n- [Human In The Loop: Story Crafting](/python/examples/workflow/human_in_the_loop_story_crafting/) is a powerful\nexample showing how workflow runs can be interactive and stateful. In this case, to collect input from a human.\n- [Reliable Structured Generation](/python/examples/workflow/reflection/) shows how to implement loops in a\nworkflow, in this case to improve structured output through reflection.\n- [Query Planning with Workflows](/python/examples/workflow/planning_workflow/) is an example of a workflow\nthat plans a query by breaking it down into smaller items, and executing those smaller items. It highlights how\nto stream events from a workflow, execute steps in parallel, and looping until a condition is met.\n- [Checkpointing Workflows](/python/examples/workflow/checkpointing_workflows/) is a more exhaustive demonstration of how to make full use of `WorkflowCheckpointer` to checkpoint Workflow runs.\n\nLast but not least, a few more advanced use cases that demonstrate how workflows can be extremely handy if you need\nto quickly implement prototypes, for example from literature:\n\n- [Advanced Text-to-SQL](/python/examples/workflow/advanced_text_to_sql/)\n- [JSON Query Engine](/python/examples/workflow/jsonalyze_query_engine/)\n- [Long RAG](/python/examples/workflow/long_rag_pack/)\n- [Multi-Step Query Engine](/python/examples/workflow/multi_step_query_engine/)\n- [Multi-Strategy Workflow](/python/examples/workflow/multi_strategy_workflow/)\n- [Router Query Engine](/python/examples/workflow/router_query_engine/)\n- [Self Discover Workflow](/python/examples/workflow/self_discover_workflow/)\n- [Sub-Question Query Engine](/python/examples/workflow/sub_question_query_engine/)\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/managing_state.md",
    "content": "---\nsidebar:\n  order: 3\ntitle: Managing State\n---\n\nBy default, workflows automatically initialize and untyped state store. You can access this as needed to share information between workflow steps through the `Context` object.\n\n```python\nfrom workflows import Workflow, Context, step\nfrom workflows.events import StartEvent, StopEvent\n\nclass MyWorkflow(Workflow):\n\n    @step\n    async def my_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n       current_count = await ctx.store.get(\"count\", default=0)\n       current_count += 1\n       await ctx.store.set(\"count\", current_count)\n       return StopEvent()\n```\n\n## Locking the State\n\nThere are cases where the state might be manipulated by multiple steps running at the same time. In these cases, in can be useful **lock** the state to prevent race conditions. You can do this by using the `Context` object's `edit_state` method:\n\n```python\n@step\nasync def my_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n   # No other steps can access the state while the `with` block is running\n   async with ctx.store.edit_state() as ctx_state:\n       if \"count\" not in ctx_state:\n           ctx_state[\"count\"] = 0\n       ctx_state[\"count\"] += 1\n   return StopEvent()\n```\n\n## Adding Typed State\n\nOften, you'll have some pre-set shape that you want to use as the state for your workflow. The best way to do this is to use a `Pydantic` model to define the state. This way, you:\n\n- Get type hints for your state\n- Get automatic validation of your state\n- (Optionally) Have full control over the serialization and deserialization of your state using [validators](https://docs.pydantic.dev/latest/concepts/validators/) and [serializers](https://docs.pydantic.dev/latest/concepts/serialization/#custom-serializers)\n\n**NOTE:** You should use a pydantic model that has defaults for all fields. This enables the `Context` object to automatically initialize the state with the defaults.\n\nHere's a quick example of how you can leverage workflows + pydantic to take advantage of all these features:\n\n```python\nfrom pydantic import BaseModel, Field\n\n\nclass CounterState(BaseModel):\n    count: int = Field(default=0)\n```\n\nThen, simply annotate your workflow state with the state model:\n\n```python\nfrom workflows import Workflow, Context, step\nfrom workflows.events import (\n    StartEvent,\n    StopEvent,\n)\n\n\nclass MyWorkflow(Workflow):\n    @step\n    async def start(\n        self,\n        ctx: Context[CounterState],\n        ev: StartEvent\n    ) -> StopEvent:\n        # Allows for atomic state updates\n        async with ctx.store.edit_state() as ctx_state:\n            ctx_state.count += 1\n\n        return StopEvent(result=\"Done!\")\n```\n\n## Maintaining Context Across Runs\n\nAs you have seen, workflows have a `Context` object that can be used to maintain state across steps.\n\nIf you want to maintain state across multiple runs of a workflow, you can pass a previous context into the `.run()` method.\n\n```python\nworkflow = MyWorkflow()\nctx = Context(workflow)\n\nhandler = workflow.run(ctx=ctx)\nresult = await handler\n\n# Optional: save the ctx somewhere and restore\n# ctx_dict = ctx.to_dict()\n# ctx = Context.from_dict(workflow, ctx_dict)\n\n# continue with next run\nhandler = workflow.run(ctx=ctx)\nresult = await handler\n```\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/observability.md",
    "content": "---\nsidebar:\n  order: 15\ntitle: Observability\n---\n\nObservability is key for debugging workflows. Beyond just adding `print()` statements, `workflows` ship with an extensive instrumentation system that tracks the input and output of every workflow step.\n\nFurthermore, you can leverage this instrumentation system to add observability to any function outside of workflow steps! More in-depth examples for all of this information can be found in the [examples folder for observability](https://github.com/run-llama/llama-agents/tree/main/examples/observability).\n\n## OpenTelemetry Integration\n\nWorkflows integrate with OpenTelemetry for exporting traces. Install the `llama-index-observability-otel` package:\n\n```bash\npip install llama-index-observability-otel\n```\n\nThen configure it in your application:\n\n```python\nfrom llama_index.observability.otel import LlamaIndexOpenTelemetry\n\n# Initialize with your span exporter\ninstrumentor = LlamaIndexOpenTelemetry(\n    span_exporter=your_span_exporter,\n    service_name_or_resource=\"your_service_name\",\n)\n\n# Start registering traces\ninstrumentor.start_registering()\n```\n\nAll workflow steps, LLM calls, and custom events are automatically captured and exported as OpenTelemetry spans with detailed attributes including:\n- Span names for each workflow step\n- Start and end times\n- Event attributes (input data, output data, etc.)\n- Nested span relationships showing execution flow\n\n## Third-Party Observability Tools\n\nWorkflows integrate seamlessly with popular observability platforms:\n\n### Arize Phoenix\n\n![Arize Phoenix](./assets/arize.png)\n\n[Arize Phoenix](https://docs.arize.com/phoenix/integrations/frameworks/llamaindex/llamaindex-workflows-tracing) provides real-time tracing and visualization for your workflows.\n\nYou can read more in the [example notebook.](https://github.com/run-llama/llama-agents/blob/main/examples/observability/workflows_observablitiy_arize_phoenix.ipynb)\n\n### Langfuse\n\n[Langfuse](https://github.com/langfuse/langfuse) directly integrates with the instrumentation system that ships with workflows.\n\nYou can read more in the [example notebook.](https://github.com/run-llama/llama-agents/blob/main/examples/observability/workflows_observablitiy_langfuse.ipynb)\n\n### Opik\n\n[Opik](https://github.com/comet-ml/opik) can receive workflow traces through the same OpenTelemetry pipeline used by workflows.\n\nTo configure export to Opik, use the OpenTelemetry setup above and point your exporter to Opik's OTLP endpoint. See the [Opik OpenTelemetry Python SDK guide](https://www.comet.com/docs/opik/integrations/opentelemetry-python-sdk).\n\n## Custom Spans and Events\n\nYou can define custom spans and events using the LlamaIndex dispatcher to add fine-grained tracing to your code:\n\n```python\nfrom llama_index_instrumentation import get_dispatcher\nfrom llama_index_instrumentation.base import BaseEvent\n\ndispatcher = get_dispatcher()\n\n# Define custom events\nclass ExampleEvent(BaseEvent):\n    data: str\n\nclass AnotherExampleEvent(BaseEvent):\n    print_statement: str\n\n# Use the @dispatcher.span decorator\n@dispatcher.span\ndef example_fn(data: str) -> None:\n    dispatcher.event(ExampleEvent(data=data))\n    s = \"This are example string data: \" + data\n    dispatcher.event(AnotherExampleEvent(print_statement=s))\n    print(s)\n```\n\nWhen you call instrumented functions, all spans and events are automatically captured by any configured tracing backend.\n\nSee complete examples in the [examples/observability](https://github.com/run-llama/llama-agents/tree/main/examples/observability) directory.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/resources.md",
    "content": "---\nsidebar:\n  order: 9\ntitle: Resource Objects\n---\n\nResources are external dependencies you can inject into the steps of a workflow.\n\nAs a simple example, look at `memory` from llama-index in the following workflow:\n\n```python\nfrom typing import Annotated\n\nfrom workflows import Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.resource import Resource\nfrom llama_index.core.llms import ChatMessage\nfrom llama_index.core.memory import Memory\n\n\ndef get_memory(*args, **kwargs):\n    return Memory.from_defaults(\"user_id_123\", token_limit=60000)\n\n\nclass SecondEvent(Event):\n    msg: str\n\n\nclass WorkflowWithResource(Workflow):\n    @step\n    async def first_step(\n        self,\n        ev: StartEvent,\n        memory: Annotated[Memory, Resource(get_memory)],\n    ) -> SecondEvent:\n        print(\"Memory before step 1\", memory)\n        await memory.aput(\n            ChatMessage(role=\"user\", content=\"This is the first step\")\n        )\n        print(\"Memory after step 1\", memory)\n        return SecondEvent(msg=\"This is an input for step 2\")\n\n    @step\n    async def second_step(\n        self, ev: SecondEvent, memory: Annotated[Memory, Resource(get_memory)]\n    ) -> StopEvent:\n        print(\"Memory before step 2\", memory)\n        await memory.aput(ChatMessage(role=\"user\", content=ev.msg))\n        print(\"Memory after step 2\", memory)\n        return StopEvent(result=\"Messages put into memory\")\n```\n\nTo inject a resource into a workflow step, you have to add a parameter to the step signature and define its type,\nusing `Annotated` and invoke the `Resource()` wrapper passing a function or callable returning the actual Resource\nobject. The return type of the wrapped function must match the declared type, ensuring consistency between what’s\nexpected and what’s provided during execution. In the example above, `memory: Annotated[Memory, Resource(get_memory)`\ndefines a resource of type `Memory` that will be provided by the `get_memory()` function and passed to the step in the\n`memory` parameter when the workflow runs.\n\nResources are shared among steps of a workflow, and the `Resource()` wrapper will invoke the factory function only once.\nIn case this is not the desired behavior, passing `cache=False` to `Resource()` will inject different resource objects\nin different steps, invoking the factory function as many times.\n\n## Config-backed Resources\n\nFor configuration data stored in JSON files, use `ResourceConfig` instead of `Resource`. It automatically loads a JSON file and parses it into a Pydantic model.\n\n```python\nfrom typing import Annotated\nfrom pydantic import BaseModel\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.resource import ResourceConfig\n\n\nclass ClassifierConfig(BaseModel):\n    categories: list[str]\n    threshold: float\n\n\nclass DocumentClassifier(Workflow):\n    @step\n    async def classify(\n        self,\n        ev: StartEvent,\n        config: Annotated[\n            ClassifierConfig,\n            ResourceConfig(config_file=\"classifier.json\"),\n        ],\n    ) -> StopEvent:\n        # config is loaded from classifier.json and validated as ClassifierConfig\n        return StopEvent(result=f\"Using threshold: {config.threshold}\")\n```\n\n### Parameters\n\n- `config_file`: Path to the JSON file containing the configuration.\n- `path_selector`: Optional \".\" delimited JSON path to extract a nested value from the json in the config file (e.g., `\"settings.classifier\"`).\n- `label`: Optional display name for workflow visualizations.\n- `description`: Optional description for workflow visualizations.\n\n### Selecting nested values\n\nIf your JSON file contains multiple configs, use `path_selector` to extract a specific section:\n\n```python\n# Given config.json: {\"classifier\": {\"categories\": [...], \"threshold\": 0.8}, \"other\": {...}}\nconfig: Annotated[\n    ClassifierConfig,\n    ResourceConfig(config_file=\"config.json\", path_selector=\"classifier\"),\n]\n```\n\n### Labels and descriptions in visualizations\n\nWhen viewing workflows in the debugger or other visualization tools, `label` and `description` help identify configs:\n\n```python\nconfig: Annotated[\n    ClassifierConfig,\n    ResourceConfig(\n        config_file=\"classifier.json\",\n        label=\"Document Classifier\",\n        description=\"Categories and confidence threshold for classification\",\n    ),\n]\n```\n\nIf no label is provided, the Pydantic model's type name is used (e.g., \"ClassifierConfig\").\n\n## Chaining Resources\n\nResources and ResourceConfigs can be chained together. A `Resource` factory function can declare dependencies on other resources using the same `Annotated` pattern:\n\n```python\nfrom typing import Annotated\nfrom pydantic import BaseModel\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.resource import Resource, ResourceConfig\nfrom llama_index.llms.anthropic import Anthropic\n\n\nclass LLMConfig(BaseModel):\n    model: str\n    temperature: float\n    max_tokens: int\n\n\ndef get_llm(\n    config: Annotated[LLMConfig, ResourceConfig(config_file=\"llm.json\")],\n) -> Anthropic:\n    return Anthropic(\n        model=config.model,\n        temperature=config.temperature,\n        max_tokens=config.max_tokens,\n    )\n\n\nclass MyWorkflow(Workflow):\n    @step\n    async def generate(\n        self,\n        ev: StartEvent,\n        llm: Annotated[Anthropic, Resource(get_llm)],\n    ) -> StopEvent:\n        response = await llm.acomplete(ev.input)\n        return StopEvent(result=response.text)\n```\n\nThe dependency chain is resolved automatically. In this example, when the workflow runs:\n1. `llm.json` is loaded and parsed into `LLMConfig`\n2. `get_llm` is called with that config to create the LLM client\n3. The resulting client is passed to the step\n\nThis pattern works with any combination of `Resource` and `ResourceConfig` dependencies.\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/retry_steps.md",
    "content": "---\nsidebar:\n  order: 10\ntitle: Error handling\n---\n\nA step that fails its execution might result in the failure of the entire workflow, but oftentimes errors are\nexpected and the execution can be safely retried. Think of a HTTP request that times out because of a transient\ncongestion of the network, or an external API call that hits a rate limiter.\n\nFor all those situations where you want the step to try again, you can use a **retry policy**. A retry policy\ninstructs the workflow to execute a step multiple times, controlling how long to wait before each new attempt,\nwhich errors are retryable, and when to give up.\n\nThe retry module is built from three families of composable building blocks:\n\n- **Retry conditions** decide whether an exception is retryable.\n- **Wait strategies** decide how long to sleep before the next attempt.\n- **Stop conditions** decide when the workflow should give up.\n\nCompose them into a policy with `retry_policy(retry=..., wait=..., stop=...)` and\npass the result to the `@step` decorator. Retry conditions support `|` and `&`,\nwait strategies support `+`, and stop conditions support `|` and `&`.\n\n## Basic usage\n\n```python\nfrom workflows import Workflow, Context, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.retry_policy import (\n    retry_policy,\n    stop_after_attempt,\n    wait_fixed,\n)\n\n\nclass MyWorkflow(Workflow):\n    @step(\n        retry_policy=retry_policy(\n            wait=wait_fixed(5),\n            stop=stop_after_attempt(10),\n        )\n    )\n    async def flaky_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        result = flaky_call()  # this might raise\n        return StopEvent(result=result)\n```\n\nWith no arguments, `retry_policy()` retries all exceptions up to 3 attempts\nwith a 5-second fixed delay between each. Pass `retry=`, `wait=`, or `stop=`\nto customize any component.\n\n## Filtering exceptions\n\nUse retry conditions to control which exceptions are retried:\n\n```python\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.retry_policy import (\n    retry_policy,\n    retry_if_exception_message,\n    retry_if_exception_type,\n    stop_after_attempt,\n    stop_before_delay,\n    wait_fixed,\n    wait_random,\n)\n\n\nclass MyWorkflow(Workflow):\n    @step(\n        retry_policy=retry_policy(\n            retry=retry_if_exception_type((TimeoutError, ConnectionError))\n            | retry_if_exception_message(match=\"rate limit|temporarily unavailable\"),\n            wait=wait_fixed(1) + wait_random(0, 1),\n            stop=stop_after_attempt(5) | stop_before_delay(30),\n        )\n    )\n    async def call_provider(self, ev: StartEvent) -> StopEvent:\n        result = await flaky_call()\n        return StopEvent(result=result)\n```\n\nThe named combinators are equivalent if you prefer a more explicit style:\n\n```python\nfrom workflows.retry_policy import (\n    retry_policy,\n    retry_any,\n    retry_if_exception_message,\n    retry_if_exception_type,\n    stop_any,\n    stop_after_attempt,\n    stop_before_delay,\n    wait_combine,\n    wait_fixed,\n    wait_random,\n)\n\npolicy = retry_policy(\n    retry=retry_any(\n        retry_if_exception_type((TimeoutError, ConnectionError)),\n        retry_if_exception_message(match=\"rate limit|temporarily unavailable\"),\n    ),\n    wait=wait_combine(wait_fixed(1), wait_random(0, 1)),\n    stop=stop_any(stop_after_attempt(5), stop_before_delay(30)),\n)\n```\n\n## Exponential backoff\n\nFor steps that call LLM providers or rate-limited APIs, exponential backoff avoids\nthundering-herd effects:\n\n```python\nfrom workflows import Workflow, Context, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.retry_policy import (\n    retry_policy,\n    stop_after_attempt,\n    wait_exponential_jitter,\n)\n\n\nclass MyWorkflow(Workflow):\n    @step(\n        retry_policy=retry_policy(\n            wait=wait_exponential_jitter(initial=1, exp_base=2, max=30, jitter=1),\n            stop=stop_after_attempt(5),\n        )\n    )\n    async def call_llm(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        result = await llm_call()  # this might raise on rate-limit\n        return StopEvent(result=result)\n```\n\n## Full API reference\n\nThe module exposes many more retry conditions, wait strategies, and stop conditions\nthan shown here. See the [API reference](/python/workflows-api-reference/retry_policy/)\nfor the complete list of building blocks and their parameters.\n\n## Custom retry policies\n\nIf the composable API doesn't cover your use case, you can write a custom policy.\nThe only requirement is a class with a `next` method matching the `RetryPolicy`\nprotocol:\n\n```python\ndef next(\n    self, elapsed_time: float, attempts: int, error: Exception\n) -> Optional[float]:\n    ...\n```\n\nReturn the number of seconds to wait before retrying, or `None` to stop.\n\nFor example, this policy only retries on Fridays:\n\n```python\nfrom datetime import datetime\nfrom typing import Optional\n\n\nclass RetryOnFridayPolicy:\n    def next(\n        self, elapsed_time: float, attempts: int, error: Exception\n    ) -> Optional[float]:\n        if datetime.today().strftime(\"%A\") == \"Friday\":\n            return 5  # retry in 5 seconds\n        return None  # don't retry\n```\n\n## Deprecated convenience constructors\n\n:::caution\n`ConstantDelayRetryPolicy` and `ExponentialBackoffRetryPolicy` predate the\ncomposable API and are kept for backwards compatibility only. Prefer\n`retry_policy(...)` with explicit retry, wait, and stop arguments.\n:::\n\n```python\nfrom workflows.retry_policy import ConstantDelayRetryPolicy\n\n# Deprecated — equivalent to:\n#   retry_policy(wait=wait_fixed(5), stop=stop_after_attempt(10))\npolicy = ConstantDelayRetryPolicy(delay=5, maximum_attempts=10)\n```\n\n```python\nfrom workflows.retry_policy import ExponentialBackoffRetryPolicy\n\n# Deprecated — equivalent to:\n#   retry_policy(wait=wait_random_exponential(multiplier=1, exp_base=2, max=30),\n#                stop=stop_after_attempt(5))\npolicy = ExponentialBackoffRetryPolicy(\n    initial_delay=1, multiplier=2, max_delay=30, maximum_attempts=5,\n)\n```\n\n## Inspecting retry state inside a step\n\n`Context.retry_info()` returns a `RetryInfo` describing the current attempt.\nUse it to adapt behavior between retries — widen a search, shorten a timeout,\nlog a warning:\n\n```python\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.retry_policy import retry_policy, stop_after_attempt\n\n\nclass MyWorkflow(Workflow):\n    @step(retry_policy=retry_policy(stop=stop_after_attempt(3)))\n    async def flaky(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        info = ctx.retry_info()\n        if info.last_exception is not None:\n            logger.warning(\n                \"retry %d after %s\", info.retry_number, str(info.last_exception)\n            )\n        ...\n```\n\n`retry_number` is `0` on the first run. `last_exception` and `last_failed_at`\nare `None` until the first failure, then describe the most recent prior failure.\n\n## Recovering when retries are exhausted\n\nWhen a policy gives up, the exception propagates and fails the workflow — unless\na `@catch_error` handler is declared. A handler is a step that accepts\n`StepFailedEvent`; it can return a `StopEvent` to end the run gracefully, return\nsome other event to re-route the workflow, or raise to fail with a new\nexception. Inspect `ev.step_name` and `ev.exception` to decide; see the\n[`StepFailedEvent` API reference](/python/workflows-api-reference/events/#workflows.events.StepFailedEvent)\nfor the full set of fields.\n\n```python\nfrom workflows import Context, Workflow, catch_error, step\nfrom workflows.events import StartEvent, StepFailedEvent, StopEvent\nfrom workflows.retry_policy import retry_policy, stop_after_attempt, wait_fixed\n\n\nclass Pipeline(Workflow):\n    @step(retry_policy=retry_policy(wait=wait_fixed(1), stop=stop_after_attempt(3)))\n    async def fetch(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=await call_api())\n\n    @catch_error\n    async def on_failure(\n        self, ctx: Context, ev: StepFailedEvent\n    ) -> StopEvent:\n        return StopEvent(\n            result={\"failed_step\": ev.step_name, \"error\": str(ev.exception)}\n        )\n```\n\nA bare `@catch_error` is a **wildcard** — it catches any step whose retries are\nexhausted and isn't claimed by a more specific handler. Only one wildcard is\nallowed per workflow.\n\n### Scoping to specific steps\n\nPass `for_steps=[...]` to limit which steps a handler covers. A step name may\nappear in at most one scoped handler; unknown names are rejected at\nconstruction time.\n\n```python\nclass Pipeline(Workflow):\n    @step(retry_policy=...)\n    async def fetch(self, ev: StartEvent) -> FetchedEvent: ...\n\n    @step(retry_policy=...)\n    async def parse(self, ev: FetchedEvent) -> StopEvent: ...\n\n    @catch_error(for_steps=[\"fetch\"])\n    async def on_fetch_failure(\n        self, ctx: Context, ev: StepFailedEvent\n    ) -> StopEvent:\n        return StopEvent(result={\"fallback\": True})\n\n    @catch_error  # wildcard — covers `parse` and anything else\n    async def on_any_failure(\n        self, ctx: Context, ev: StepFailedEvent\n    ) -> StopEvent:\n        return StopEvent(result={\"aborted\": ev.step_name})\n```\n\n### Recovery budget\n\nHandlers can themselves emit events that flow back into the graph, so a lineage\nmay re-enter the same handler. `max_recoveries` (default `1`) caps how many\ntimes that can happen per event lineage before the workflow fails instead of\nre-entering. Set it higher for handlers that participate in a bounded retry\nloop; keep it at `1` for terminal handlers.\n\n```python\n@catch_error(for_steps=[\"fetch\", \"retry_fetch\"], max_recoveries=2)\nasync def on_failure(self, ctx: Context, ev: StepFailedEvent) -> RetryFetch:\n    return RetryFetch()\n```\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/streaming.mdx",
    "content": "---\nsidebar:\n  order: 4\ntitle: Streaming events\n---\n\nWorkflows can be complex -- they are designed to handle complex, branching, concurrent logic -- which means they can take time to fully execute. To provide your user with a good experience, you may want to provide an indication of progress by streaming events as they occur. Workflows have built-in support for this on the `Context` object.\n\nTo get this done, let's bring in all the deps we need:\n\n```python\nimport asyncio\nfrom llama_index.llms.openai import OpenAI\n\nfrom workflows import (\n    Workflow,\n    Context,\n    step,\n)\nfrom workflows.events import (\n    StartEvent,\n    StopEvent,\n    Event,\n)\n```\n\nLet's set up some events for a simple three-step workflow, plus an event to handle streaming our progress as we go:\n\n```python\nclass FirstEvent(Event):\n    first_output: str\n\n\nclass SecondEvent(Event):\n    second_output: str\n    response: str\n\n\nclass ProgressEvent(Event):\n    msg: str\n```\n\nAnd define a workflow class that sends events:\n\n```python\nclass MyWorkflow(Workflow):\n    @step\n    async def step_one(self, ctx: Context, ev: StartEvent) -> FirstEvent:\n        ctx.write_event_to_stream(ProgressEvent(msg=\"Step one is happening\"))\n        return FirstEvent(first_output=\"First step complete.\")\n\n    @step\n    async def step_two(self, ctx: Context, ev: FirstEvent) -> SecondEvent:\n        llm = OpenAI(model=\"gpt-4o-mini\")\n        generator = await llm.astream_complete(\n            \"Please give me the first 3 paragraphs of Moby Dick, a book in the public domain.\"\n        )\n        full_resp = \"\"\n        async for response in generator:\n            # Allow the workflow to stream this piece of response\n            ctx.write_event_to_stream(ProgressEvent(msg=response.delta))\n            full_resp += response.delta\n\n        return SecondEvent(\n            second_output=\"Second step complete, full response attached\",\n            response=full_resp,\n        )\n\n    @step\n    async def step_three(self, ctx: Context, ev: SecondEvent) -> StopEvent:\n        ctx.write_event_to_stream(ProgressEvent(msg=\"Step three is happening\"))\n        return StopEvent(result=\"Workflow complete.\")\n```\n\n<Aside type=\"tip\">\n  `OpenAI()` here assumes you have an `OPENAI_API_KEY` set in your environment.\n  You could also pass one in using the `api_key` parameter.\n</Aside>\n\nIn `step_one` and `step_three` we write individual events to the event stream. In `step_two` we use `astream_complete` to produce an iterable generator of the LLM's response, then we produce an event for each chunk of data the LLM sends back to us -- roughly one per word -- before returning the final response to `step_three`.\n\nTo actually get this output, we need to run the workflow asynchronously and listen for the events, like this:\n\n```python\nasync def main():\n    w = MyWorkflow(timeout=30, verbose=True)\n    handler = w.run(first_input=\"Start the workflow.\")\n\n    async for ev in handler.stream_events():\n        if isinstance(ev, ProgressEvent):\n            print(ev.msg)\n\n    final_result = await handler\n    print(\"Final result\", final_result)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n`run` runs the workflow in the background, while `stream_events` will provide any event that gets written to the stream. It stops when the stream delivers a `StopEvent`, after which you can get the final result of the workflow as you normally would.\n\n## Handling workflow termination\n\nWhen a workflow ends abnormally (timeout, cancellation, or step failure), a specific `StopEvent` subclass is published to the stream before the exception is raised:\n\n- **`WorkflowTimedOutEvent`** - Published when the workflow exceeds its timeout. Contains `timeout` (seconds) and `active_steps` (list of step names that were running).\n- **`WorkflowCancelledEvent`** - Published when the workflow is cancelled by the user.\n- **`WorkflowFailedEvent`** - Published when a step fails permanently after exhausting retries. Contains `step_name`, `exception`, `attempts`, and `elapsed_seconds`.\n\n```python\nfrom workflows.events import (\n    WorkflowTimedOutEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n)\n\nasync for ev in handler.stream_events():\n    if isinstance(ev, WorkflowTimedOutEvent):\n        print(f\"Workflow timed out after {ev.timeout}s\")\n    elif isinstance(ev, WorkflowCancelledEvent):\n        print(\"Workflow was cancelled\")\n    elif isinstance(ev, WorkflowFailedEvent):\n        print(f\"Step '{ev.step_name}' failed after {ev.attempts} attempts: {ev.exception}\")\n```\n"
  },
  {
    "path": "docs/src/content/docs/llamaagents/workflows/unbound_functions.md",
    "content": "---\nsidebar:\n  order: 11\ntitle: Workflows from unbound functions\n---\n\nThroughout these docs, we have been showing workflows defined as classes. However, this is not the only way to define a workflow: you can also define the steps in your workflow through independent or \"unbound\" functions and assign them to a workflow using the `@step()` decorator. Let's see how that works.\n\nFirst we create an empty class to hold the steps:\n\n```python\nfrom workflows import Workflow\n\nclass TestWorkflow(Workflow):\n    pass\n```\n\nNow we can add steps to the workflow by defining functions and decorating them with the `@step()` decorator:\n\n```python\nfrom workflows import step\nfrom workflows.events import StartEvent, StopEvent\n\n@step(workflow=TestWorkflow)\ndef some_step(ev: StartEvent) -> StopEvent:\n    return StopEvent()\n```\n\nIn this example, we're adding a starting step to the `TestWorkflow` class. The `@step()` decorator takes the `workflow` argument, which is the class to which the step will be added. The function signature is the same as for a regular step, with the exception of the `workflow` argument.\n\nYou can also add steps this way to any existing workflow class! This can be handy if you just need one extra step in your workflow and don't want to subclass an entire workflow to do it.\n"
  },
  {
    "path": "docs/src/content.config.ts",
    "content": "import { defineCollection } from 'astro:content';\nimport { docsLoader } from '@astrojs/starlight/loaders';\nimport { docsSchema } from '@astrojs/starlight/schema';\nimport { autoSidebarLoader } from 'starlight-auto-sidebar/loader'\nimport { autoSidebarSchema } from 'starlight-auto-sidebar/schema'\n\nexport const collections = {\n\tdocs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),\n\tautoSidebar: defineCollection({\n\t\tloader: autoSidebarLoader(),\n\t\tschema: autoSidebarSchema(),\n\t}),\n};\n"
  },
  {
    "path": "docs/src/pages/index.astro",
    "content": "---\nreturn Astro.redirect('/python/llamaagents/overview/');\n---\n"
  },
  {
    "path": "docs.config.mjs",
    "content": "export default {\n  section: \"llama-index-workflows\",\n  label: \"LlamaAgents\",\n  content: [\n    { src: \"./docs/src/content/docs/llamaagents\", dest: \"python/llamaagents\" },\n  ],\n  sidebar: [\n    {\n      label: \"LlamaAgents\",\n      content: {\n        type: \"autogenerate\",\n        directory: \"python/llamaagents\",\n        collapsed: true,\n      },\n      append: [\n        {\n          label: \"Agent Workflows Reference \\u{1F517}\",\n          link: \"/python/workflows-api-reference/\",\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "examples/README.md",
    "content": "# LlamaAgents Examples\n\nA collection of runnable examples showing how to build, serve, and deploy agent workflows with `llama-index-workflows` and `llama-agents-*`.\n\nNew to the project? Start at the top of the list and work down — each step builds on the previous.\n\n## Start here\n\n1. **[`feature_walkthrough.ipynb`](feature_walkthrough.ipynb)** — The single best place to begin. A guided tour of workflows, steps, events, context, branching, loops, and streaming, all in one notebook.\n2. **[`agent.ipynb`](agent.ipynb)** — Build a simple agent as a workflow. Covers tool calling and the agent loop pattern.\n3. **[`document_processing.ipynb`](document_processing.ipynb)** — A realistic document pipeline: parsing, extraction, and orchestration.\n\n## Serving workflows as an API\n\n4. **[`server/`](server/)** — Wrap a workflow as a REST API with `WorkflowServer`, standalone or mounted inside an existing FastAPI app.\n5. **[`client/`](client/)** — Call a running workflow server from Python with `WorkflowClient`, including streaming and human-in-the-loop.\n\n## Durability and scale\n\n6. **[`durable_workflows.ipynb`](durable_workflows.ipynb)** — Save and resume workflow runs using pluggable storage.\n7. **[`dbos/`](dbos/)** — Production-grade durability with DBOS: crash recovery, multi-replica servers, and idle release.\n\n## Deployment\n\n8. **[`docker/`](docker/)** — Containerize a workflow server with Docker.\n9. **[`k8s-otel/`](k8s-otel/)** — Deploy to Kubernetes with OpenTelemetry, Tilt, and a kind cluster.\n\n## Observability and evaluation\n\n10. **[`observability/`](observability/)** — Trace workflows with Arize Phoenix, Langfuse, and the built-in context logger.\n11. **[`eval_driven_prompt_refinement.ipynb`](eval_driven_prompt_refinement.ipynb)** — Iterate on prompts using evaluation-driven feedback loops.\n\n## Advanced patterns\n\n- **[`streaming_internal_events.ipynb`](streaming_internal_events.ipynb)** — Stream intermediate events from nested workflow steps.\n- **[`state_management_with_vector_databases.ipynb`](state_management_with_vector_databases.ipynb)** — Persist workflow state in a vector database.\n- **[`document_agents/`](document_agents/)** — A finance triage agent built with document workflows.\n- **[`visualization/`](visualization/)** — Visualize workflow graphs, including resource nodes.\n\n---\n\nFor more on the library, see the [`llama-index-workflows` package README](../packages/llama-index-workflows/README.md) and the [project root README](../README.md).\n"
  },
  {
    "path": "examples/agent.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Workflow for a Function Calling Agent\\n\",\n    \"\\n\",\n    \"This notebook walks through setting up a `Workflow` to construct a function calling agent from scratch.\\n\",\n    \"\\n\",\n    \"Function calling agents work by using an LLM that supports tools/functions in its API (OpenAI, Ollama, Anthropic, etc.) to call functions an use tools.\\n\",\n    \"\\n\",\n    \"Our workflow will be stateful with memory, and will be able to call the LLM to select tools and process incoming user messages.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!pip install llama-index-workflows llama-index-llms-openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import os\\n\",\n    \"\\n\",\n    \"os.environ[\\\"OPENAI_API_KEY\\\"] = \\\"sk-proj-...\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Designing the Workflow\\n\",\n    \"\\n\",\n    \"An agent consists of several steps\\n\",\n    \"1. Handling the latest incoming user message, including adding to memory and getting the latest chat history\\n\",\n    \"2. Calling the LLM with tools + chat history\\n\",\n    \"3. Parsing out tool calls (if any)\\n\",\n    \"4. If there are tool calls, call them, and loop until there are none\\n\",\n    \"5. When there is no tool calls, return the LLM response\\n\",\n    \"\\n\",\n    \"### The Workflow Events\\n\",\n    \"\\n\",\n    \"To handle these steps, we need to define a few events:\\n\",\n    \"1. An event to handle new messages and prepare the chat history\\n\",\n    \"2. An event to handle streaming responses\\n\",\n    \"3. An event to trigger tool calls\\n\",\n    \"4. An event to handle the results of tool calls\\n\",\n    \"\\n\",\n    \"The other steps will use the built-in `StartEvent` and `StopEvent` events.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index.core.llms import ChatMessage\\n\",\n    \"from llama_index.core.tools import ToolOutput, ToolSelection\\n\",\n    \"from llama_index.core.workflow import Event\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InputEvent(Event):\\n\",\n    \"    input: list[ChatMessage]\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class StreamEvent(Event):\\n\",\n    \"    delta: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ToolCallEvent(Event):\\n\",\n    \"    tool_calls: list[ToolSelection]\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class FunctionOutputEvent(Event):\\n\",\n    \"    output: ToolOutput\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### The Workflow Itself\\n\",\n    \"\\n\",\n    \"With our events defined, we can construct our workflow and steps. \\n\",\n    \"\\n\",\n    \"Note that the workflow automatically validates itself using type annotations, so the type annotations on our steps are very helpful!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Any\\n\",\n    \"\\n\",\n    \"from llama_index.core.llms.function_calling import FunctionCallingLLM\\n\",\n    \"from llama_index.core.memory import ChatMemoryBuffer\\n\",\n    \"from llama_index.core.tools.types import BaseTool\\n\",\n    \"from llama_index.core.workflow import (\\n\",\n    \"    Context,\\n\",\n    \"    StartEvent,\\n\",\n    \"    StopEvent,\\n\",\n    \"    Workflow,\\n\",\n    \"    step,\\n\",\n    \")\\n\",\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class FuncationCallingAgent(Workflow):\\n\",\n    \"    def __init__(\\n\",\n    \"        self,\\n\",\n    \"        llm: FunctionCallingLLM | None = None,\\n\",\n    \"        tools: list[BaseTool] | None = None,\\n\",\n    \"        **workflow_kwargs: Any,\\n\",\n    \"    ) -> None:\\n\",\n    \"        super().__init__(**workflow_kwargs)\\n\",\n    \"        self.tools = tools or []\\n\",\n    \"\\n\",\n    \"        self.llm = llm or OpenAI()\\n\",\n    \"        assert self.llm.metadata.is_function_calling_model\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:\\n\",\n    \"        # clear sources\\n\",\n    \"        await ctx.store.set(\\\"sources\\\", [])\\n\",\n    \"\\n\",\n    \"        # check if memory is setup\\n\",\n    \"        memory = await ctx.store.get(\\\"memory\\\", default=None)\\n\",\n    \"        if not memory:\\n\",\n    \"            memory = ChatMemoryBuffer.from_defaults(llm=self.llm)\\n\",\n    \"\\n\",\n    \"        # get user input\\n\",\n    \"        user_input = ev.input\\n\",\n    \"        user_msg = ChatMessage(role=\\\"user\\\", content=user_input)\\n\",\n    \"        await memory.aput(user_msg)\\n\",\n    \"\\n\",\n    \"        # get chat history\\n\",\n    \"        chat_history = await memory.aget()\\n\",\n    \"\\n\",\n    \"        # update context\\n\",\n    \"        await ctx.store.set(\\\"memory\\\", memory)\\n\",\n    \"\\n\",\n    \"        return InputEvent(input=chat_history)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def handle_llm_input(\\n\",\n    \"        self, ctx: Context, ev: InputEvent\\n\",\n    \"    ) -> ToolCallEvent | StopEvent:\\n\",\n    \"        chat_history = ev.input\\n\",\n    \"\\n\",\n    \"        # stream the response\\n\",\n    \"        response_stream = await self.llm.astream_chat_with_tools(\\n\",\n    \"            self.tools, chat_history=chat_history\\n\",\n    \"        )\\n\",\n    \"        async for response in response_stream:\\n\",\n    \"            ctx.write_event_to_stream(StreamEvent(delta=response.delta or \\\"\\\"))\\n\",\n    \"\\n\",\n    \"        # save the final response, which should have all content\\n\",\n    \"        memory = await ctx.store.get(\\\"memory\\\")\\n\",\n    \"        await memory.aput(response.message)\\n\",\n    \"        await ctx.store.set(\\\"memory\\\", memory)\\n\",\n    \"\\n\",\n    \"        # get tool calls\\n\",\n    \"        tool_calls = self.llm.get_tool_calls_from_response(\\n\",\n    \"            response, error_on_no_tool_call=False\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        if not tool_calls:\\n\",\n    \"            sources = await ctx.store.get(\\\"sources\\\", default=[])\\n\",\n    \"            return StopEvent(result={\\\"response\\\": response, \\\"sources\\\": [*sources]})\\n\",\n    \"        else:\\n\",\n    \"            return ToolCallEvent(tool_calls=tool_calls)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def handle_tool_calls(self, ctx: Context, ev: ToolCallEvent) -> InputEvent:\\n\",\n    \"        tool_calls = ev.tool_calls\\n\",\n    \"        tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools}\\n\",\n    \"\\n\",\n    \"        tool_msgs = []\\n\",\n    \"        sources = await ctx.store.get(\\\"sources\\\", default=[])\\n\",\n    \"\\n\",\n    \"        # call tools -- safely!\\n\",\n    \"        for tool_call in tool_calls:\\n\",\n    \"            tool = tools_by_name.get(tool_call.tool_name)\\n\",\n    \"            additional_kwargs = {\\n\",\n    \"                \\\"tool_call_id\\\": tool_call.tool_id,\\n\",\n    \"                \\\"name\\\": tool.metadata.get_name(),\\n\",\n    \"            }\\n\",\n    \"            if not tool:\\n\",\n    \"                tool_msgs.append(\\n\",\n    \"                    ChatMessage(\\n\",\n    \"                        role=\\\"tool\\\",\\n\",\n    \"                        content=f\\\"Tool {tool_call.tool_name} does not exist\\\",\\n\",\n    \"                        additional_kwargs=additional_kwargs,\\n\",\n    \"                    )\\n\",\n    \"                )\\n\",\n    \"                continue\\n\",\n    \"\\n\",\n    \"            try:\\n\",\n    \"                tool_output = tool(**tool_call.tool_kwargs)\\n\",\n    \"                sources.append(tool_output)\\n\",\n    \"                tool_msgs.append(\\n\",\n    \"                    ChatMessage(\\n\",\n    \"                        role=\\\"tool\\\",\\n\",\n    \"                        content=tool_output.content,\\n\",\n    \"                        additional_kwargs=additional_kwargs,\\n\",\n    \"                    )\\n\",\n    \"                )\\n\",\n    \"            except Exception as e:\\n\",\n    \"                tool_msgs.append(\\n\",\n    \"                    ChatMessage(\\n\",\n    \"                        role=\\\"tool\\\",\\n\",\n    \"                        content=f\\\"Encountered error in tool call: {e}\\\",\\n\",\n    \"                        additional_kwargs=additional_kwargs,\\n\",\n    \"                    )\\n\",\n    \"                )\\n\",\n    \"\\n\",\n    \"        # update memory\\n\",\n    \"        memory = await ctx.store.get(\\\"memory\\\")\\n\",\n    \"        for msg in tool_msgs:\\n\",\n    \"            await memory.aput(msg)\\n\",\n    \"\\n\",\n    \"        await ctx.store.set(\\\"sources\\\", sources)\\n\",\n    \"        await ctx.store.set(\\\"memory\\\", memory)\\n\",\n    \"\\n\",\n    \"        chat_history = await memory.aget()\\n\",\n    \"        return InputEvent(input=chat_history)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"And thats it! Let's explore the workflow we wrote a bit.\\n\",\n    \"\\n\",\n    \"`prepare_chat_history()`:\\n\",\n    \"This is our main entry point. It handles adding the user message to memory, and uses the memory to get the latest chat history. It returns an `InputEvent`.\\n\",\n    \"\\n\",\n    \"`handle_llm_input()`:\\n\",\n    \"Triggered by an `InputEvent`, it uses the chat history and tools to prompt the llm. If tool calls are found, a `ToolCallEvent` is emitted. Otherwise, we say the workflow is done an emit a `StopEvent`\\n\",\n    \"\\n\",\n    \"`handle_tool_calls()`:\\n\",\n    \"Triggered by `ToolCallEvent`, it calls tools with error handling and returns tool outputs. This event triggers a **loop** since it emits an `InputEvent`, which takes us back to `handle_llm_input()`\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Run the Workflow!\\n\",\n    \"\\n\",\n    \"**NOTE:** With loops, we need to be mindful of runtime. Here, we set a timeout of 120s.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Running step prepare_chat_history\\n\",\n      \"Step prepare_chat_history produced event InputEvent\\n\",\n      \"Running step handle_llm_input\\n\",\n      \"Step handle_llm_input produced event StopEvent\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from llama_index.core.tools import FunctionTool\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def add(x: int, y: int) -> int:\\n\",\n    \"    \\\"\\\"\\\"Useful function to add two numbers.\\\"\\\"\\\"\\n\",\n    \"    return x + y\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def multiply(x: int, y: int) -> int:\\n\",\n    \"    \\\"\\\"\\\"Useful function to multiply two numbers.\\\"\\\"\\\"\\n\",\n    \"    return x * y\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"tools = [\\n\",\n    \"    FunctionTool.from_defaults(add),\\n\",\n    \"    FunctionTool.from_defaults(multiply),\\n\",\n    \"]\\n\",\n    \"\\n\",\n    \"agent = FuncationCallingAgent(\\n\",\n    \"    llm=OpenAI(model=\\\"gpt-4o-mini\\\"), tools=tools, timeout=120, verbose=True\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"ret = await agent.run(input=\\\"Hello!\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"assistant: Hello! How can I assist you today?\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(ret[\\\"response\\\"])\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Running step prepare_chat_history\\n\",\n      \"Step prepare_chat_history produced event InputEvent\\n\",\n      \"Running step handle_llm_input\\n\",\n      \"Step handle_llm_input produced event ToolCallEvent\\n\",\n      \"Running step handle_tool_calls\\n\",\n      \"Step handle_tool_calls produced event InputEvent\\n\",\n      \"Running step handle_llm_input\\n\",\n      \"Step handle_llm_input produced event ToolCallEvent\\n\",\n      \"Running step handle_tool_calls\\n\",\n      \"Step handle_tool_calls produced event InputEvent\\n\",\n      \"Running step handle_llm_input\\n\",\n      \"Step handle_llm_input produced event StopEvent\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"ret = await agent.run(input=\\\"What is (2123 + 2321) * 312?\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Chat History\\n\",\n    \"\\n\",\n    \"By default, the workflow is creating a fresh `Context` for each run. This means that the chat history is not preserved between runs. However, we can pass our own `Context` to the workflow to preserve chat history.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Running step prepare_chat_history\\n\",\n      \"Step prepare_chat_history produced event InputEvent\\n\",\n      \"Running step handle_llm_input\\n\",\n      \"Step handle_llm_input produced event StopEvent\\n\",\n      \"assistant: Hello, Logan! How can I assist you today?\\n\",\n      \"Running step prepare_chat_history\\n\",\n      \"Step prepare_chat_history produced event InputEvent\\n\",\n      \"Running step handle_llm_input\\n\",\n      \"Step handle_llm_input produced event StopEvent\\n\",\n      \"assistant: Your name is Logan.\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from llama_index.core.workflow import Context\\n\",\n    \"\\n\",\n    \"ctx = Context(agent)\\n\",\n    \"\\n\",\n    \"ret = await agent.run(input=\\\"Hello! My name is Logan.\\\", ctx=ctx)\\n\",\n    \"print(ret[\\\"response\\\"])\\n\",\n    \"\\n\",\n    \"ret = await agent.run(input=\\\"What is my name?\\\", ctx=ctx)\\n\",\n    \"print(ret[\\\"response\\\"])\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Streaming\\n\",\n    \"\\n\",\n    \"Using the `handler` returned from the `.run()` method, we can also access the streaming events.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Once upon a time in a quaint little village, there lived a curious cat named Whiskers. Whiskers was no ordinary cat; he had a beautiful coat of orange and white fur that shimmered in the sunlight, and his emerald green eyes sparkled with mischief.\\n\",\n      \"\\n\",\n      \"Every day, Whiskers would explore the village, visiting the bakery for a whiff of freshly baked bread and the flower shop to sniff the colorful blooms. The villagers adored him, often leaving out little treats for their favorite feline.\\n\",\n      \"\\n\",\n      \"One sunny afternoon, while wandering near the edge of the village, Whiskers stumbled upon a hidden path that led into the woods. His curiosity piqued, he decided to follow the path, which was lined with tall trees and vibrant wildflowers. As he ventured deeper, he heard a soft, melodic sound that seemed to beckon him.\\n\",\n      \"\\n\",\n      \"Following the enchanting music, Whiskers soon found himself in a clearing where a group of woodland creatures had gathered. They were having a grand celebration, complete with dancing, singing, and a feast of berries and nuts. The animals welcomed Whiskers with open paws, inviting him to join their festivities.\\n\",\n      \"\\n\",\n      \"Whiskers, delighted by the warmth and joy of his new friends, danced and played until the sun began to set. As the sky turned shades of pink and orange, he realized it was time to return home. The woodland creatures gifted him a small, sparkling acorn as a token of their friendship.\\n\",\n      \"\\n\",\n      \"From that day on, Whiskers would often visit the clearing, sharing stories of the village and enjoying the company of his woodland friends. He learned that adventure and friendship could be found in the most unexpected places, and he cherished every moment spent in the magical woods.\\n\",\n      \"\\n\",\n      \"And so, Whiskers continued to live his life filled with curiosity, laughter, and the warmth of friendship, reminding everyone that sometimes, the best adventures are just a whisker away.\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"agent = FuncationCallingAgent(\\n\",\n    \"    llm=OpenAI(model=\\\"gpt-4o-mini\\\"), tools=tools, timeout=120, verbose=False\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"handler = agent.run(input=\\\"Hello! Write me a short story about a cat.\\\")\\n\",\n    \"\\n\",\n    \"async for event in handler.stream_events():\\n\",\n    \"    if isinstance(event, StreamEvent):\\n\",\n    \"        print(event.delta, end=\\\"\\\", flush=True)\\n\",\n    \"\\n\",\n    \"response = await handler\\n\",\n    \"# print(ret[\\\"response\\\"])\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.3\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/client/README.md",
    "content": "# Client Examples\n\nCall a running `WorkflowServer` from Python with `llama_agents.client.WorkflowClient`. Each subfolder is a self-contained pair: a server script you run first, and a client script you run against it.\n\n## Examples\n\n| Folder | What it shows |\n| --- | --- |\n| [`base/`](base/) | The minimal client/server pair — submit a run, stream events, get the result. **Start here.** |\n| [`human_in_the_loop/`](human_in_the_loop/) | A workflow that pauses to wait for human input via `InputRequiredEvent` / `HumanResponseEvent`, and a client that responds to it. |\n\n## Running\n\nIn one terminal, start the server:\n\n```bash\nuv run examples/client/base/workflow_server.py\n```\n\nIn another, run the client:\n\n```bash\nuv run examples/client/base/workflow_client.py\n```\n"
  },
  {
    "path": "examples/client/base/workflow_client.py",
    "content": "import asyncio\nfrom typing import Literal\n\nfrom llama_agents.client import WorkflowClient\nfrom pydantic import Field\nfrom workflows.events import StartEvent\n\n\nclass InputNumbers(StartEvent):\n    a: int\n    b: int\n    operation: Literal[\"addition\", \"subtraction\"] = Field(default=\"addition\")\n\n\nasync def main() -> None:\n    client = WorkflowClient(base_url=\"http://localhost:8000\")\n    workflows = await client.list_workflows()\n    print(\"===== AVAILABLE WORKFLOWS ====\")\n    print(workflows)\n    await client.is_healthy()  # will raise an exception if the server is not healthy\n    handler = await client.run_workflow_nowait(\n        \"add_or_subtract\",\n        start_event=InputNumbers(a=1, b=3, operation=\"addition\"),\n        context=None,\n    )\n    handler_id = handler.handler_id\n    print(\"==== STARTING THE WORKFLOW ===\")\n    print(f\"Workflow running with handler ID: {handler_id}\")\n    print(\"=== STREAMING EVENTS ===\")\n\n    async for event in client.get_workflow_events(handler_id=handler_id):\n        print(f\"Received event type={event.type} data={event.value}\")\n    result = await client.get_handler(handler_id)\n\n    print(f\"Final result: {result.result} (status: {result.status})\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/client/base/workflow_server.py",
    "content": "from typing import Literal\n\nfrom llama_agents.server import WorkflowServer\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\nclass InputNumbers(StartEvent):\n    a: int\n    b: int\n    operation: Literal[\"addition\", \"subtraction\"] = Field(default=\"addition\")\n\n\nclass CalculationEvent(Event):\n    result: int\n\n\nclass OutputEvent(StopEvent):\n    message: str\n\n\nclass AddOrSubtractWorkflow(Workflow):\n    @step\n    async def first_step(self, ev: InputNumbers, ctx: Context) -> OutputEvent | None:\n        ctx.write_event_to_stream(ev)\n        result = ev.a + ev.b if ev.operation == \"addition\" else ev.a - ev.b\n        ctx.write_event_to_stream(CalculationEvent(result=result))\n        return OutputEvent(\n            message=f\"You {ev.operation} operation ({ev.operation}) between {ev.a} and {ev.b}: {result}\"\n        )\n\n\nasync def main() -> None:\n    server = WorkflowServer()\n    server.add_workflow(\"add_or_subtract\", AddOrSubtractWorkflow(timeout=1000))\n    try:\n        await server.serve(\"localhost\", 8000)\n    except KeyboardInterrupt:\n        return\n    except Exception as e:\n        raise ValueError(f\"An error occurred: {e}\")\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/client/human_in_the_loop/workflow_client_hitl.py",
    "content": "import asyncio\n\nfrom llama_agents.client import WorkflowClient\nfrom workflows.events import (\n    HumanResponseEvent,\n    StopEvent,\n)\n\n\nclass ResponseEvent(HumanResponseEvent):\n    response: str\n\n\nclass OutEvent(StopEvent):\n    output: str\n\n\nasync def main() -> None:\n    client = WorkflowClient(base_url=\"http://localhost:8000\")\n    handler = await client.run_workflow_nowait(\"human\")\n    handler_id = handler.handler_id\n    print(handler_id)\n    async for event in client.get_workflow_events(handler_id=handler_id):\n        if \"RequestEvent\" == (event.type):\n            print(\n                \"Workflow is requiring human input:\",\n                (event.value or {}).get(\"prompt\", \"\"),\n            )\n            name = input(\"Reply here: \")\n            sent_event = await client.send_event(\n                handler_id=handler_id,\n                event=ResponseEvent(response=name.capitalize().strip()),\n            )\n            msg = \"Event has been sent\" if sent_event else \"Event failed to send\"\n            print(msg)\n    result = await client.get_handler(handler_id)\n    print(f\"Workflow complete with status: {result.status})\")\n    res = OutEvent.model_validate(result.result)\n    print(\"Received final message:\", res.output)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/client/human_in_the_loop/workflow_server_hitl.py",
    "content": "from llama_agents.server import WorkflowServer\nfrom workflows import Workflow, step\nfrom workflows.context import Context\nfrom workflows.events import (\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\n\n\nclass RequestEvent(InputRequiredEvent):\n    prompt: str\n\n\nclass ResponseEvent(HumanResponseEvent):\n    response: str\n\n\nclass OutEvent(StopEvent):\n    output: str\n\n\nclass HumanInTheLoopWorkflow(Workflow):\n    @step\n    async def prompt_human(self, ev: StartEvent, ctx: Context) -> RequestEvent:\n        return RequestEvent(prompt=\"What is your name?\")\n\n    @step\n    async def greet_human(self, ev: ResponseEvent) -> OutEvent:\n        return OutEvent(output=f\"Hello, {ev.response}\")\n\n\nasync def main() -> None:\n    server = WorkflowServer()\n    server.add_workflow(\"human\", HumanInTheLoopWorkflow(timeout=1000))\n    try:\n        await server.serve(\"localhost\", 8000)\n    except KeyboardInterrupt:\n        return\n    except Exception as e:\n        raise ValueError(f\"An error occurred: {e}\")\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/dbos/.gitignore",
    "content": ".last_handler_id\n.last_run_id\n"
  },
  {
    "path": "examples/dbos/README.md",
    "content": "# DBOS Durability Examples\n\nExamples of running durable workflows backed by [DBOS](https://github.com/dbos-inc/dbos-transact-py). DBOS gives you crash recovery, resumable runs, and multi-replica coordination with either SQLite (zero setup) or Postgres.\n\nSee [`packages/llama-agents-dbos/ARCHITECTURE.md`](../../packages/llama-agents-dbos/ARCHITECTURE.md) for the underlying model.\n\n## Examples\n\n| File | What it shows |\n| --- | --- |\n| [`server_quickstart.py`](server_quickstart.py) | The simplest durable `WorkflowServer` setup, using SQLite out of the box. **Start here.** |\n| [`durable_workflow.py`](durable_workflow.py) | A looping counter workflow you can interrupt with Ctrl+C and resume with `--resume`. Demonstrates checkpointing without a server. |\n| [`server_replicas.py`](server_replicas.py) | Two `WorkflowServer` replicas sharing a Postgres-backed event store. Start a run on replica A, stream events from replica B, interrupt, and resume. Requires Docker (uses [`docker-compose.yml`](docker-compose.yml)). |\n| [`idle_release_demo.py`](idle_release_demo.py) | Shows how long-idle workflows are released from memory and automatically resumed when a new event arrives. |\n| [`_replica.py`](_replica.py) | Single-replica server process used internally by `server_replicas.py`. |\n\n## Running\n\n```bash\n# Quickest path — no database required\nuv run examples/dbos/server_quickstart.py\n\n# Multi-replica (needs Docker for Postgres)\ndocker compose -f examples/dbos/docker-compose.yml up -d\nuv run examples/dbos/server_replicas.py\n```\n"
  },
  {
    "path": "examples/dbos/_replica.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nMulti-Replica Server\n\nSingle replica server that can be run standalone.\n\nUsage:\n    python examples/multi_replica/serve.py --port 8001\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\n\nfrom dbos import DBOS\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import WorkflowServer\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nPOSTGRES_DSN = \"postgresql://workflows:workflows@localhost:5433/workflows\"\n\n\nclass Tick(Event):\n    count: int = Field(description=\"Current count\")\n\n\nclass WaitDone(Event):\n    count: int = Field(description=\"Current count after waiting\")\n\n\nclass CounterResult(StopEvent):\n    final_count: int = Field(description=\"Final counter value\")\n\n\nclass CounterWorkflow(Workflow):\n    \"\"\"Counts to 20 with 1s delays, emitting Tick stream events.\n\n    Split into a slow wait step and a fast tick step so that\n    the stream event is the last thing written before the next\n    wait. This minimizes duplicate ticks on DBOS replay.\n    \"\"\"\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> WaitDone:\n        print(\"[Start] Initializing counter\")\n        return WaitDone(count=0)\n\n    @step\n    async def tick(self, ctx: Context, ev: WaitDone) -> Tick | CounterResult:\n        count = ev.count + 1\n        await ctx.store.set(\"count\", count)\n        ctx.write_event_to_stream(Tick(count=count))\n        print(f\"[Tick {count:2d}] count = {count}\")\n\n        if count >= 20:\n            return CounterResult(final_count=count)\n        return Tick(count=count)\n\n    @step\n    async def wait(self, ctx: Context, ev: Tick) -> WaitDone:\n        await asyncio.sleep(1.0)\n        return WaitDone(count=ev.count)\n\n\nasync def main() -> None:\n    parser = argparse.ArgumentParser(description=\"Multi-Replica Server\")\n    parser.add_argument(\"--port\", type=int, default=8001)\n    args = parser.parse_args()\n\n    DBOS(\n        config={\n            \"name\": \"multi-replica\",\n            \"system_database_url\": POSTGRES_DSN,\n            \"run_admin_server\": False,\n            \"executor_id\": f\"replica-{args.port}\",\n        }\n    )\n\n    runtime = DBOSRuntime()\n\n    server = WorkflowServer(\n        workflow_store=runtime.create_workflow_store(),\n        runtime=runtime.build_server_runtime(),\n    )\n    server.add_workflow(\"counter\", CounterWorkflow(runtime=runtime, timeout=None))\n\n    print(f\"Serving on port {args.port}\")\n    await server.start()\n    try:\n        await server.serve(host=\"0.0.0.0\", port=args.port)\n    finally:\n        await server.stop()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/dbos/docker-compose.yml",
    "content": "services:\n  postgres:\n    image: postgres:16\n    environment:\n      POSTGRES_DB: workflows\n      POSTGRES_USER: workflows\n      POSTGRES_PASSWORD: workflows\n    ports:\n      - \"5433:5432\"\n"
  },
  {
    "path": "examples/dbos/durable_workflow.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nDBOS Counter Example\n\nSimple looping workflow that increments a counter until it reaches 20.\n\nUsage:\n    python -m examples.dbos_durability.counter_example              # Start new\n    python -m examples.dbos_durability.counter_example --resume     # Resume last\n    python -m examples.dbos_durability.counter_example --clean      # Reset state\n\nTry Ctrl+C mid-run to test resume behavior.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport uuid\nfrom pathlib import Path\n\nfrom dbos import DBOS\nfrom llama_agents.dbos import DBOSRuntime\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.handler import WorkflowHandler\n\n_DIR = Path(__file__).parent\n_DB_FILE = _DIR / \".dbos_data.sqlite3\"\n_RUN_FILE = _DIR / \".last_run_id\"\n\n\nclass Tick(Event):\n    count: int = Field(description=\"Current count\")\n\n\nclass CounterResult(StopEvent):\n    final_count: int = Field(description=\"Final counter value\")\n\n\nclass CounterWorkflow(Workflow):\n    \"\"\"Looping counter workflow - increments until reaching 20.\"\"\"\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> Tick:\n        await ctx.store.set(\"count\", 0)\n        print(\"[Start] Initializing counter to 0\")\n        return Tick(count=0)\n\n    @step\n    async def increment(self, ctx: Context, ev: Tick) -> Tick | CounterResult:\n        count = ev.count + 1\n        await ctx.store.set(\"count\", count)\n        print(f\"[Tick {count:2d}] count = {count}\")\n\n        if count >= 20:\n            return CounterResult(final_count=count)\n\n        await asyncio.sleep(0.5)\n        return Tick(count=count)\n\n\ndef run(run_id: str, resume: bool = False) -> None:\n    \"\"\"Run the counter workflow.\"\"\"\n    DBOS(\n        config={\n            \"name\": \"counter-example\",\n            \"system_database_url\": f\"sqlite+pysqlite:///{_DB_FILE}?check_same_thread=false\",\n            \"run_admin_server\": False,\n        }\n    )\n\n    runtime = DBOSRuntime()\n    workflow = CounterWorkflow(runtime=runtime, timeout=None)\n    runtime.launch_sync()\n\n    async def _run() -> None:\n        if resume:\n            external_adapter = runtime.get_external_adapter(run_id)\n            handler = WorkflowHandler(workflow, external_adapter)\n        else:\n            handler = workflow.run(start_event=StartEvent(), run_id=run_id)\n        result = await handler\n        print(f\"\\nResult: final_count = {result.final_count}\")\n\n    try:\n        asyncio.run(_run())\n    except KeyboardInterrupt:\n        print(\"\\nInterrupted - workflow state saved. Use --resume to continue.\")\n    else:\n        runtime.destroy_sync()\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"DBOS Counter Example\")\n    parser.add_argument(\"--resume\", action=\"store_true\", help=\"Resume last workflow\")\n    parser.add_argument(\"--clean\", action=\"store_true\", help=\"Remove state files\")\n    args = parser.parse_args()\n\n    if args.clean:\n        for f in [_DB_FILE, _RUN_FILE]:\n            if f.exists():\n                f.unlink()\n                print(f\"Removed {f}\")\n        return\n\n    if args.resume and _RUN_FILE.exists():\n        run_id = _RUN_FILE.read_text().strip()\n        print(f\"Resuming: {run_id}\")\n    else:\n        run_id = f\"counter-{uuid.uuid4().hex[:8]}\"\n        _RUN_FILE.write_text(run_id)\n        print(f\"Starting: {run_id}\")\n\n    run(run_id, resume=args.resume)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/dbos/idle_release_demo.py",
    "content": "\"\"\"\nIdle release demo — confirms INFO logging when a workflow idles and resumes.\n\nStarts a server with a short idle_timeout (5s). A background task starts\na workflow, waits for idle release, then sends an event to trigger resume.\nWatch the server logs for:\n  - INFO ... Released idle DBOS handler [run_id=...]\n  - INFO ... Resumed DBOS workflow via continue-as-new [old_run_id=..., new_run_id=...]\n\nRun:\n    uv run examples/dbos/idle_release_demo.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\n\nfrom dbos import DBOS\nfrom llama_agents.client import WorkflowClient\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import WorkflowServer\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\n\nlogging.basicConfig(level=logging.INFO)\n\nDBOS(config={\"name\": \"idle-release-demo\", \"run_admin_server\": False})\n\nIDLE_TIMEOUT = 5.0\n\n\nclass AskName(InputRequiredEvent):\n    prompt: str = Field(default=\"What is your name?\")\n\n\nclass UserInput(HumanResponseEvent):\n    response: str = Field(default=\"\")\n\n\nclass GreeterWorkflow(Workflow):\n    @step\n    async def ask(self, ctx: Context, ev: StartEvent) -> AskName:\n        return AskName()\n\n    @step\n    async def greet(self, ctx: Context, ev: UserInput) -> StopEvent:\n        return StopEvent(result={\"greeting\": f\"Hello, {ev.response}!\"})\n\n\nasync def drive_workflow(port: int) -> None:\n    \"\"\"Background task that exercises the idle → release → resume cycle.\"\"\"\n    await asyncio.sleep(1.0)  # let server start\n\n    client = WorkflowClient(base_url=f\"http://localhost:{port}\")\n    print(\"\\n--- Starting workflow (nowait) ---\")\n    handler = await client.run_workflow_nowait(\"greeter\")\n    print(f\"--- handler_id={handler.handler_id} ---\")\n\n    print(f\"--- Waiting {IDLE_TIMEOUT + 2}s for idle release ---\")\n    await asyncio.sleep(IDLE_TIMEOUT + 2)\n\n    print(\"--- Sending UserInput to trigger resume ---\")\n    await client.send_event(\n        handler.handler_id,\n        UserInput(response=\"world\"),\n    )\n\n    print(\"--- Streaming result ---\")\n    stream = client.get_workflow_events(handler.handler_id)\n    async for event in stream:\n        print(f\"--- Received event: {event.type} ---\")\n        if event.type == \"StopEvent\":\n            result = event.value.get(\"result\")\n            print(f\"--- Result: {result} ---\")\n            break\n\n\nasync def main() -> None:\n    port = 8000\n    runtime = DBOSRuntime()\n\n    server = WorkflowServer(\n        workflow_store=runtime.create_workflow_store(),\n        runtime=runtime.build_server_runtime(idle_timeout=IDLE_TIMEOUT),\n    )\n    server.add_workflow(\"greeter\", GreeterWorkflow(runtime=runtime, timeout=None))\n\n    print(f\"Serving on http://localhost:{port} (idle_timeout={IDLE_TIMEOUT}s)\")\n    driver = asyncio.create_task(drive_workflow(port))\n    await server.start()\n    try:\n        await server.serve(host=\"0.0.0.0\", port=port)\n    finally:\n        driver.cancel()\n        await server.stop()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/dbos/server_quickstart.py",
    "content": "\"\"\"\nQuick-start: durable workflow server with DBOS.\n\nRun with:\n    uv run examples/dbos/server_quickstart.py\n\nBy default this uses SQLite (zero setup). To use Postgres instead,\ncomment out the SQLite section and uncomment the Postgres section below.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\n# ---------------------------------------------------------------------------\n# SQLite (default — no external database needed)\n# ---------------------------------------------------------------------------\nfrom dbos import DBOS\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import WorkflowServer\nfrom pydantic import Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\n# ---------------------------------------------------------------------------\n# SQLite (default — no external database needed)\n# ---------------------------------------------------------------------------\nDBOS(config={\"name\": \"quickstart\", \"run_admin_server\": False})\n\n# ---------------------------------------------------------------------------\n# Postgres (uncomment this block and comment out the SQLite block above)\n# ---------------------------------------------------------------------------\n# from dbos import DBOS\n#\n# DBOS(\n#     config={\n#         \"name\": \"quickstart\",\n#         \"system_database_url\": \"postgresql://user:pass@localhost:5432/mydb\",\n#         \"run_admin_server\": False,\n#         # Assign a unique executor_id per replica for multi-node setups:\n#         # \"executor_id\": \"replica-1\",\n#     }\n# )\n# ---------------------------------------------------------------------------\n\n\n# -- Define a simple workflow ------------------------------------------------\n\n\nclass Tick(Event):\n    count: int = Field(description=\"Current count\")\n\n\nclass CounterResult(StopEvent):\n    final_count: int = Field(description=\"Final counter value\")\n\n\nclass CounterWorkflow(Workflow):\n    \"\"\"Counts to 20, emitting stream events along the way.\"\"\"\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> Tick:\n        return Tick(count=0)\n\n    @step\n    async def tick(self, ctx: Context, ev: Tick) -> Tick | CounterResult:\n        count = ev.count + 1\n        ctx.write_event_to_stream(Tick(count=count))\n        print(f\"  tick {count}\")\n        await asyncio.sleep(0.5)\n        if count >= 20:\n            return CounterResult(final_count=count)\n        return Tick(count=count)\n\n\n# -- Wire it up and serve ----------------------------------------------------\n\n\nasync def main() -> None:\n    runtime = DBOSRuntime()\n\n    server = WorkflowServer(\n        workflow_store=runtime.create_workflow_store(),\n        runtime=runtime.build_server_runtime(),\n    )\n    server.add_workflow(\"counter\", CounterWorkflow(runtime=runtime, timeout=None))\n\n    print(\"Serving on http://localhost:8000\")\n    print(\"Try: curl -X POST http://localhost:8000/workflows/counter/run\")\n    await server.start()\n    await server.serve(\n        host=\"0.0.0.0\",\n        port=8000,\n        uvicorn_config={\"timeout_graceful_shutdown\": 0.1},\n    )\n\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except KeyboardInterrupt:\n        pass\n"
  },
  {
    "path": "examples/dbos/server_replicas.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nMulti-Replica Demo\n==================\n\nDemonstrates durable workflow execution across multiple server replicas\nbacked by a shared Postgres database with DBOS.\n\n  - Two WorkflowServer replicas share a Postgres-backed event store\n  - A counter workflow is triggered on Replica A (port 8001)\n  - Events are streamed in real-time from Replica B (port 8002)\n  - Ctrl+C interrupts the workflow mid-flight\n  - --resume picks up exactly where it left off via DBOS recovery\n\nUsage:\n    python examples/dbos/server_replicas.py              # Start new\n    python examples/dbos/server_replicas.py --resume     # Resume after Ctrl+C\n    python examples/dbos/server_replicas.py --clean      # Tear down everything\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport signal\nimport subprocess\nimport sys\nimport time\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Any\n\nimport httpx\nfrom llama_agents.client import WorkflowClient\n\n_DIR = Path(__file__).parent\n_HANDLER_FILE = _DIR / \".last_handler_id\"\n_COMPOSE_FILE = _DIR / \"docker-compose.yml\"\n\n# -- Pretty output -----------------------------------------------------------\n\nBLUE = \"\\033[94m\"\nGREEN = \"\\033[92m\"\nYELLOW = \"\\033[93m\"\nCYAN = \"\\033[96m\"\nDIM = \"\\033[2m\"\nBOLD = \"\\033[1m\"\nRESET = \"\\033[0m\"\n\n\ndef ts() -> str:\n    return datetime.now().strftime(\"%H:%M:%S\")\n\n\ndef log(msg: str, color: str = DIM) -> None:\n    print(f\"  {color}{ts()}{RESET}  {msg}\")\n\n\n# -- Infrastructure -----------------------------------------------------------\n\n\ndef run_cmd(*args: str, **kwargs: Any) -> subprocess.CompletedProcess[str]:\n    try:\n        return subprocess.run(\n            args, check=True, text=True, capture_output=True, **kwargs\n        )\n    except subprocess.CalledProcessError as e:\n        parts = [f\"Command failed: {' '.join(args)}\"]\n        if e.stdout:\n            parts.append(f\"stdout:\\n{e.stdout}\")\n        if e.stderr:\n            parts.append(f\"stderr:\\n{e.stderr}\")\n        raise subprocess.CalledProcessError(\n            e.returncode, e.cmd, e.stdout, e.stderr\n        ) from RuntimeError(\"\\n\".join(parts))\n\n\ndef start_postgres() -> None:\n    log(\"Starting Postgres container...\", BLUE)\n    run_cmd(\"docker\", \"compose\", \"-f\", str(_COMPOSE_FILE), \"up\", \"-d\")\n    for _ in range(30):\n        try:\n            run_cmd(\n                \"docker\",\n                \"compose\",\n                \"-f\",\n                str(_COMPOSE_FILE),\n                \"exec\",\n                \"-T\",\n                \"postgres\",\n                \"pg_isready\",\n                \"-U\",\n                \"workflows\",\n            )\n            log(\"Postgres ready\", GREEN)\n            return\n        except subprocess.CalledProcessError:\n            time.sleep(1)\n    raise RuntimeError(\"Postgres failed to start\")\n\n\ndef start_replica(port: int) -> subprocess.Popen[str]:\n    return subprocess.Popen(\n        [sys.executable, str(_DIR / \"_replica.py\"), \"--port\", str(port)],\n        text=True,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        start_new_session=True,\n    )\n\n\ndef wait_for_server(port: int, timeout: float = 30.0) -> None:\n    deadline = time.monotonic() + timeout\n    while time.monotonic() < deadline:\n        try:\n            resp = httpx.get(f\"http://localhost:{port}/workflows\", timeout=2.0)\n            if resp.status_code == 200:\n                return\n        except httpx.ConnectError:\n            pass\n        time.sleep(0.5)\n    raise RuntimeError(f\"Server on port {port} did not start in {timeout}s\")\n\n\n# -- Workflow operations ------------------------------------------------------\n\n\nasync def start_workflow(client: WorkflowClient) -> str:\n    handler = await client.run_workflow_nowait(\"counter\")\n    return handler.handler_id\n\n\nasync def stream_events(\n    client: WorkflowClient, handler_id: str, resume: bool = False\n) -> bool:\n    after: int | str = \"now\" if resume else -1\n    completed = False\n    async for event in client.get_workflow_events(handler_id, after_sequence=after):\n        event_type = event.type\n        event_data = event.value or {}\n        if event_type == \"Tick\":\n            count = event_data.get(\"count\", \"?\")\n            bar = \"#\" * int(count) + \".\" * (20 - int(count))\n            log(f\"Tick {count:>2}/20  [{bar}]\", CYAN)\n        elif event_type == \"CounterResult\":\n            final = event_data.get(\"final_count\", \"?\")\n            log(f\"Done! final_count={final}\", GREEN)\n            completed = True\n        else:\n            log(f\"{event_type}: {event_data}\", DIM)\n    return completed\n\n\n# -- Main ---------------------------------------------------------------------\n\n\nasync def async_main() -> None:\n    parser = argparse.ArgumentParser(description=\"Multi-Replica Demo\")\n    parser.add_argument(\"--resume\", action=\"store_true\", help=\"Resume last workflow\")\n    parser.add_argument(\"--clean\", action=\"store_true\", help=\"Tear down everything\")\n    args = parser.parse_args()\n\n    if args.clean:\n        if _HANDLER_FILE.exists():\n            _HANDLER_FILE.unlink()\n        subprocess.run(\n            [\"docker\", \"compose\", \"-f\", str(_COMPOSE_FILE), \"down\", \"-v\"],\n            check=False,\n        )\n        print(\"Cleaned up.\")\n        return\n\n    print()\n    print(f\"  {BOLD}Multi-Replica Workflow Demo{RESET}\")\n    print(f\"  {DIM}Two servers, one Postgres, durable execution{RESET}\")\n    print()\n\n    replicas: list[subprocess.Popen[str]] = []\n\n    def cleanup() -> None:\n        for proc in replicas:\n            proc.kill()\n        for proc in replicas:\n            proc.wait()\n\n    current_task = asyncio.current_task()\n\n    def handle_sigint() -> None:\n        print()\n        log(\"Interrupted. Workflow state is safe in Postgres.\", YELLOW)\n        log(f\"Run with {BOLD}--resume{RESET} to continue where you left off.\", YELLOW)\n        print()\n        if current_task:\n            current_task.cancel()\n\n    asyncio.get_running_loop().add_signal_handler(signal.SIGINT, handle_sigint)\n\n    replica_a = WorkflowClient(base_url=\"http://localhost:8001\")\n    replica_b = WorkflowClient(base_url=\"http://localhost:8002\")\n\n    try:\n        # --- Postgres ---\n        start_postgres()\n        print()\n\n        # --- Replicas ---\n        log(\n            f\"Starting Replica A on :8001  {DIM}(executor_id=replica-8001){RESET}\", BLUE\n        )\n        replicas.append(start_replica(8001))\n        log(\n            f\"Starting Replica B on :8002  {DIM}(executor_id=replica-8002){RESET}\", BLUE\n        )\n        replicas.append(start_replica(8002))\n        wait_for_server(8001)\n        log(\"Replica A ready\", GREEN)\n        wait_for_server(8002)\n        log(\"Replica B ready\", GREEN)\n        print()\n\n        # --- Workflow ---\n        if args.resume and _HANDLER_FILE.exists():\n            handler_id = _HANDLER_FILE.read_text().strip()\n            log(f\"Resuming workflow  handler_id={BOLD}{handler_id}{RESET}\", YELLOW)\n            log(f\"{DIM}DBOS recovers the workflow on the owning replica{RESET}\", DIM)\n        else:\n            log(\"Triggering counter workflow on Replica A (:8001)...\", BLUE)\n            handler_id = await start_workflow(replica_a)\n            _HANDLER_FILE.write_text(handler_id)\n            log(f\"Workflow started  handler_id={BOLD}{handler_id}{RESET}\", GREEN)\n\n        print()\n        log(\"Streaming events from Replica B (:8002)...\", BLUE)\n        log(f\"{DIM}Events flow: Replica A -> Postgres -> Replica B -> here{RESET}\", DIM)\n        print()\n\n        completed = await stream_events(replica_b, handler_id, resume=args.resume)\n\n        print()\n        if completed:\n            log(f\"{GREEN}{BOLD}Workflow completed across replicas!{RESET}\", GREEN)\n            if _HANDLER_FILE.exists():\n                _HANDLER_FILE.unlink()\n        print()\n    finally:\n        cleanup()\n\n\ndef main() -> None:\n    try:\n        asyncio.run(async_main())\n    except (KeyboardInterrupt, asyncio.CancelledError):\n        pass\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/docker/Dockerfile",
    "content": "FROM python:3.13-alpine3.22\n\nCOPY . /opt/app\nWORKDIR /opt/app\n\n# Install system deps\nRUN apk add --no-cache git\n\n# Install workflows\nRUN pip install -r requirements.txt\n\n# Default configuration, override with \"docker run -e NAME=value\"\nENV SERVER_HOST=0.0.0.0\nENV SERVER_PORT=8080\n\nCMD [ \"python\", \"-m\", \"workflows.server\", \"app.py\" ]\n"
  },
  {
    "path": "examples/docker/README.md",
    "content": "# Docker Example\n\nA minimal container image that serves a workflow with `WorkflowServer`.\n\n## Files\n\n| File | Purpose |\n| --- | --- |\n| [`app.py`](app.py) | The workflow and `WorkflowServer` entrypoint. |\n| [`Dockerfile`](Dockerfile) | Builds a slim Python image with the app and its dependencies. |\n| [`requirements.txt`](requirements.txt) | Runtime dependencies installed into the image. |\n\n## Running\n\n```bash\ndocker build -t workflows-example examples/docker\ndocker run --rm -p 8000:8000 workflows-example\n```\n\nThen call it from the host:\n\n```bash\ncurl -X POST http://localhost:8000/workflows/echo/run -d '{\"start_event\": {\"message\": \"hi\"}}'\n```\n"
  },
  {
    "path": "examples/docker/app.py",
    "content": "import asyncio\n\nfrom llama_agents.server import WorkflowServer\nfrom workflows import Workflow, step\nfrom workflows.context import Context\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\nclass StreamEvent(Event):\n    sequence: int\n\n\nclass GreetingWorkflow(Workflow):\n    @step\n    async def greet(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        for i in range(3):\n            ctx.write_event_to_stream(StreamEvent(sequence=i))\n            await asyncio.sleep(0.3)\n\n        name = getattr(ev, \"name\", \"World\")\n        return StopEvent(result=f\"Hello, {name}!\")\n\n\nserver = WorkflowServer()\nserver.add_workflow(\"greeting\", GreetingWorkflow())\n\n\n# This is to test the server locally, run with `uv run python app.py`\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(server.serve(host=\"localhost\", port=8080))\n    except KeyboardInterrupt:\n        pass\n"
  },
  {
    "path": "examples/docker/requirements.txt",
    "content": "llama-index-workflows[server]\n"
  },
  {
    "path": "examples/document_agents/finance_triage_agent.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"c9bd2f93\"\n   },\n   \"source\": [\n    \"# Finance Team Assistant Agent with Classify & Exrtact\\n\",\n    \"\\n\",\n    \"In this example, we use [LlamaExtract](https://developers.llamaindex.ai/python/cloud/llamaextract/getting_started/) and [LlamaClassify](https://developers.llamaindex.ai/python/cloud/llamaclassify/getting_started/), along with [Agent Workflows](https://developers.llamaindex.ai/python/llamaagents/workflows/) to build an intelligent agent that can triage incoming emails with attachments (like invoices or expenses) and respond accordingly.\\n\",\n    \"\\n\",\n    \"This process consists of a few steps:\\n\",\n    \"1. We want to classify incoming attachments: for this demo, we're classifying invoices and expenses\\n\",\n    \"2. Next, based on what the result of the classification is, we want to extract some specific information: such as payee, due date for payment etc.\\n\",\n    \"3. Finally, we want to take action accordingly. Here, we're simulating an email acknowledgement, and we're checking expenses against a budget.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"f67ca47a\"\n   },\n   \"source\": [\n    \"## Setup\\n\",\n    \"\\n\",\n    \"First, we need to install all the required packages and add the required API keys.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"abe487e7\"\n   },\n   \"source\": [\n    \"### Define Data Schemas and Extraction Agents\\n\",\n    \"\\n\",\n    \"We'll define Pydantic models for the data we want to extract (Expense and Invoice) and then create LlamaExtract agents for each.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"WK8oJvQSsULW\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!pip install llama-cloud-services llama-index-workflows llama-index-llms-openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"4m8nb7Ka-YeW\",\n    \"outputId\": \"5722a1f8-59f7-403c-9a51-c131729236ae\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Enter your OpenAI API Key··········\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"import os\\n\",\n    \"from getpass import getpass\\n\",\n    \"\\n\",\n    \"if os.getenv(\\\"LLAMA_CLOUD_API_KEY\\\") is None:\\n\",\n    \"    os.environ[\\\"LLAMA_CLOUD_API_KEY\\\"] = getpass(\\\"Enter your LlamaCloud API Key\\\")\\n\",\n    \"\\n\",\n    \"if os.getenv(\\\"OPENAI_API_KEY\\\") is None:\\n\",\n    \"    os.environ[\\\"OPENAI_API_KEY\\\"] = getpass(\\\"Enter your OpenAI API Key\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"azJF8JrcYf64\"\n   },\n   \"source\": [\n    \"## Create Extract Agents\\n\",\n    \"\\n\",\n    \"In this scenario, we want to be able to simulate an inbox where employees or parnters can forward emails with attachments. These attachments could be invoices to be payed out, or internal expenses by employees that we need to check agains budgets etc.\\n\",\n    \"\\n\",\n    \"Below, we create out `Expense` and `Invoice` schemas that we will use as the structure of extration agents.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"651Xf21g9TNt\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_cloud_services import LlamaExtract\\n\",\n    \"from pydantic import BaseModel, Field\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class Expense(BaseModel):\\n\",\n    \"    amount: float = Field(description=\\\"The amount of the expense\\\")\\n\",\n    \"    currency: str = Field(description=\\\"The currency of the expense\\\")\\n\",\n    \"    description: str = Field(description=\\\"A description of the expense\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class Invoice(BaseModel):\\n\",\n    \"    amount: float = Field(description=\\\"The amount of the invoice\\\")\\n\",\n    \"    currency: str = Field(description=\\\"The currency of the invoice\\\")\\n\",\n    \"    due_date: str = Field(description=\\\"The due date of the invoice\\\")\\n\",\n    \"    payee: str = Field(description=\\\"The payee of the invoice\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"llama_extract = LlamaExtract()\\n\",\n    \"invoice_extract_agent = llama_extract.create_agent(\\n\",\n    \"    name=\\\"Invoice_Extractor\\\", data_schema=Invoice\\n\",\n    \")\\n\",\n    \"expense_extract_agent = llama_extract.create_agent(\\n\",\n    \"    name=\\\"Expense_Extractor\\\", data_schema=Expense\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"KsiTFLK0Y9iN\"\n   },\n   \"source\": [\n    \"## Build the Agent Workflow\\n\",\n    \"\\n\",\n    \"Next, we define the custom events we want for our finance triage agent.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"yaR2CtqGsehq\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"from workflows.events import Event, StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class EmailReceived(StartEvent):\\n\",\n    \"    sender: str\\n\",\n    \"    subject: str\\n\",\n    \"    body: str\\n\",\n    \"    attachment: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ClassificationResult(Event):\\n\",\n    \"    classification: str\\n\",\n    \"    reason: str\\n\",\n    \"    email: str\\n\",\n    \"    attachment: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class SendEmail(StopEvent):\\n\",\n    \"    body: str\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Wjag8U4ZZUe_\"\n   },\n   \"source\": [\n    \"Our final `FinanceTeamAgent` has just 2 steps:\\n\",\n    \"- `classify_email`: This is where we create a classifier with LlamaClassify, providinf rules for when an attacnhment is an invoice, vs when it's an expense\\n\",\n    \"- `extract_contents`: Which is where we can design the next steps (in this case, we're simulating sending an appropriate email) depending on what the attachment has been classified as. We use our extract agents to extract the relevant information invoices or expenses.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 83,\n   \"metadata\": {\n    \"id\": \"i-e5Bt7LzCuP\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_cloud.client import AsyncLlamaCloud\\n\",\n    \"from llama_cloud.types import ClassifierRule\\n\",\n    \"from llama_cloud_services.beta.classifier.client import ClassifyClient\\n\",\n    \"from workflows import Workflow, step\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class FinanceTeamAgent(Workflow):\\n\",\n    \"    def __init__(self, invoice_extract_agent, expense_extract_agent, *args, **kwargs):\\n\",\n    \"        client = AsyncLlamaCloud(token=os.environ[\\\"LLAMA_CLOUD_API_KEY\\\"])\\n\",\n    \"        self.invoice_extract_agent = invoice_extract_agent\\n\",\n    \"        self.expense_extract_agent = expense_extract_agent\\n\",\n    \"        self.llm = OpenAI(model=\\\"gpt-5\\\")\\n\",\n    \"        self.classifier = ClassifyClient(client)\\n\",\n    \"        super().__init__(*args, **kwargs)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def classify_email(self, ev: EmailReceived) -> ClassificationResult:\\n\",\n    \"        rules = [\\n\",\n    \"            ClassifierRule(\\n\",\n    \"                type=\\\"invoice\\\",\\n\",\n    \"                description=\\\"This is an invoice for a contract that has to be payed out by the company. It may be forwarded from the partner or employee\\\",\\n\",\n    \"            ),\\n\",\n    \"            ClassifierRule(\\n\",\n    \"                type=\\\"expense\\\",\\n\",\n    \"                description=\\\"This is an expsense that's been submitted for a business trip that should be payed back to the employee in the next pay out cycle.\\\",\\n\",\n    \"            ),\\n\",\n    \"        ]\\n\",\n    \"        classification = await self.classifier.aclassify(\\n\",\n    \"            files=ev.attachment, rules=rules\\n\",\n    \"        )\\n\",\n    \"        return ClassificationResult(\\n\",\n    \"            classification=classification.items[0].result.type,\\n\",\n    \"            reason=classification.items[0].result.reasoning,\\n\",\n    \"            email=ev.sender,\\n\",\n    \"            attachment=ev.attachment,\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def extract_contents(self, ev: ClassificationResult) -> SendEmail:\\n\",\n    \"        if ev.classification == \\\"expense\\\":\\n\",\n    \"            extracted_data = await self.expense_extract_agent.aextract(ev.attachment)\\n\",\n    \"            if extracted_data.data[\\\"amount\\\"] < 1000.0:\\n\",\n    \"                body = self.llm.complete(f\\\"\\\"\\\"Construct an email acknowledging to {ev.email} that their expense of\\n\",\n    \"                                    {extracted_data.data[\\\"amount\\\"]} for {extracted_data.data[\\\"description\\\"]} was accepted and will be payed back in the next payment cycle.\\\"\\\"\\\")\\n\",\n    \"                return SendEmail(body=body.text)\\n\",\n    \"            else:\\n\",\n    \"                body = self.llm.complete(f\\\"\\\"\\\"Contruct an email the their expense of {extracted_data.data[\\\"amount\\\"]} for {extracted_data.data[\\\"description\\\"]} exceeds the\\n\",\n    \"                                    budget so has been denied. Explain that they can reach out if this seems wrong\\\"\\\"\\\")\\n\",\n    \"                return SendEmail(body=body.text)\\n\",\n    \"        elif ev.classification == \\\"invoice\\\":\\n\",\n    \"            extracted_data = await self.invoice_extract_agent.aextract(ev.attachment)\\n\",\n    \"            body = self.llm.complete(f\\\"\\\"\\\"Construct a reply to {ev.email}, that the invoice has been received and gibe in for on who will\\n\",\n    \"                                  be payed and how much based on the info in {extracted_data}\\\"\\\"\\\")\\n\",\n    \"            return SendEmail(body=body.text)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 84,\n   \"metadata\": {\n    \"id\": \"vYmKqqTW2hk_\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"agent = FinanceTeamAgent(\\n\",\n    \"    invoice_extract_agent=invoice_extract_agent,\\n\",\n    \"    expense_extract_agent=expense_extract_agent,\\n\",\n    \"    timeout=100.0,\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"oTawjuKDZvu3\"\n   },\n   \"source\": [\n    \"## Try the Agent\\n\",\n    \"\\n\",\n    \"To try thi agent, you can constuct an email below. Provide a file that could be an invoice or expense.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 85,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"zjtGd1JoDFdn\",\n    \"outputId\": \"653686a2-55a6-41f7-c9f0-d7792be2a0ce\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Uploading files: 100%|██████████| 1/1 [00:00<00:00,  2.09it/s]\\n\",\n      \"Creating extraction jobs: 100%|██████████| 1/1 [00:01<00:00,  1.86s/it]\\n\",\n      \"Extracting files: 100%|██████████| 1/1 [00:05<00:00,  5.77s/it]\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"email = EmailReceived(\\n\",\n    \"    sender=\\\"tuana@runllama.ai\\\",\\n\",\n    \"    subject=\\\"Cowork Invoice\\\",\\n\",\n    \"    body=\\\"\\\",\\n\",\n    \"    attachment=\\\"/content/sb-receipt.png\\\",\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"result = await agent.run(start_event=email)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 86,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"vL8SlFINEn6n\",\n    \"outputId\": \"c9a64fa8-e6fa-415a-adb9-e7404fdfae61\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"To: tuana@runllama.ai\\n\",\n      \"Subject: Expense Accepted – Starbucks Store #63225 (Mt. Juliet, TN) – $38.02\\n\",\n      \"\\n\",\n      \"Hi Tuana,\\n\",\n      \"\\n\",\n      \"We’ve reviewed your expense submission for $38.02 from Starbucks Store #63225 in Mt. Juliet, TN, and it has been accepted. The claim covers:\\n\",\n      \"- One Venti Mocha Latte with oat milk\\n\",\n      \"- One chocolate pie\\n\",\n      \"- One grande white mocha with dat milk\\n\",\n      \"\\n\",\n      \"Reimbursement will be included in the next payment cycle.\\n\",\n      \"\\n\",\n      \"If you have any questions, please let us know.\\n\",\n      \"\\n\",\n      \"Best regards,\\n\",\n      \"[Your Name]\\n\",\n      \"[Title]\\n\",\n      \"[Company]\\n\",\n      \"[Contact Information]\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(result.body)\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"name\": \"python\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/document_processing.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Iterative Extraction with Workflows + LlamaCloud\\n\",\n    \"\\n\",\n    \"In this example, we'll build a workflow that can\\n\",\n    \"1. Parse a document (using `LlamaParse`)\\n\",\n    \"2. Use an LLM to generate a JSON schema for the data we want to extract (using `OpenAI`). This includes error handling and retries for when the JSON schema is invalid!\\n\",\n    \"3. Use human-in-the-loop to either accept or provide feedback on the proposed schema.\\n\",\n    \"4. Send the finalized schema and parsed content to `LlamaExtract` to extract the data\\n\",\n    \"\\n\",\n    \"To use `LlamaCloud` and `OpenAI`, you'll need a few API keys:\\n\",\n    \"1. [LlamaCloud](https://cloud.llamaindex.ai)\\n\",\n    \"2. [OpenAI](https://platform.openai.com/account/api-keys)\\n\",\n    \"\\n\",\n    \"You'll also need to install the some required libraries:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install llama-index-workflows llama-cloud-services jsonschema, openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Setup\\n\",\n    \"\\n\",\n    \"### Workflow Resources\\n\",\n    \"\\n\",\n    \"First, we'll define some resource functions for our workflow.\\n\",\n    \"\\n\",\n    \"This will be called once per workflow run, and will be used to return clients for various services.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_cloud_services.extract import (\\n\",\n    \"    ExtractConfig,\\n\",\n    \"    ExtractMode,\\n\",\n    \"    LlamaExtract,\\n\",\n    \"    SourceText,\\n\",\n    \")\\n\",\n    \"from llama_cloud_services.parse import LlamaParse\\n\",\n    \"from openai import AsyncOpenAI\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_parse_client(**kwargs):\\n\",\n    \"    return LlamaParse(api_key=\\\"llx-...\\\", parse_mode=\\\"parse_page_with_agent\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_extract_client(**kwargs):\\n\",\n    \"    return LlamaExtract(api_key=\\\"llx-...\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_openai_client(**kwargs):\\n\",\n    \"    return AsyncOpenAI(api_key=\\\"sk-...\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Workflow State\\n\",\n    \"\\n\",\n    \"As our workflow runs, we can store global state between steps. While we could skip this step, providing a state class allows us to have more type safety, and also control over how the state is serialized and deserialized (using optional pydantic serializers and validators).\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from pydantic import BaseModel, Field\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowState(BaseModel):\\n\",\n    \"    file_path: str | None = Field(default=None)\\n\",\n    \"    file_content: str | None = Field(default=None)\\n\",\n    \"    current_schema: dict | None = Field(default=None)\\n\",\n    \"    current_feedback: str | None = Field(default=None)\\n\",\n    \"    original_prompt: str | None = Field(default=None)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Schema Extraction Prompt\\n\",\n    \"\\n\",\n    \"Our prompt for generating a JSON schema will be doing double duty:\\n\",\n    \"1. Handles the initial generation of the schema\\n\",\n    \"2. Placeholders for handling the feedback from past generations/human feedback.\\n\",\n    \"\\n\",\n    \"To make the output easy to parse, we'll instruct the LLM to use the `<schema>` and `</schema>` tags to wrap the schema.\\n\",\n    \"\\n\",\n    \"`LlamaExtract` expects a JSON schema that has a root node with \\\"type\\\": \\\"object\\\" and fields inside \\\"properties\\\", so we'll instruct the LLM to output a schema that matches this format.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"extract_prompt = \\\"\\\"\\\"\\\\\\n\",\n    \"<file>\\n\",\n    \"{file_content}\\n\",\n    \"</file>\\n\",\n    \"\\n\",\n    \"<user_prompt>\\n\",\n    \"{prompt}\\n\",\n    \"</user_prompt>\\n\",\n    \"{past_attempt}\\n\",\n    \"\\n\",\n    \"Given the file content above, and the user prompt, output a JSON schema that will be used to extract the data from the file.\\n\",\n    \"\\n\",\n    \"Your JSON schema should have a root node with \\\"type\\\": \\\"object\\\" and fields inside \\\"properties\\\".\\n\",\n    \"\\n\",\n    \"Wrap your schema in <schema>...</schema> tags.\\n\",\n    \"\\\"\\\"\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Workflow Events\\n\",\n    \"\\n\",\n    \"We'll define a few events for our workflow.\\n\",\n    \"\\n\",\n    \"- `InputEvent`: Start the workflow by providing a file path and a prompt. Subclass of `StartEvent`, so that the workflow knows to start with this event.\\n\",\n    \"- `ParsedContent`: Parses the content and carry forward the content and prompt. Will also be used to re-trigger the schema generation if the user provides feedback on the current proposed schema.\\n\",\n    \"- `RunExtraction`: Runs the extraction on the provided file content and schema.\\n\",\n    \"- `ProposedSchema`: Proposes a schema for the extraction. Needs human approval. Subclass of `InputRequiredEvent`, so that the workflow knows to wait for the human approval before continuing.\\n\",\n    \"- `ApprovedSchema`: Handles the human approval of the proposed schema. Subclass of `HumanResponseEvent`, so that the workflow knows to wait for the human approval before continuing.\\n\",\n    \"- `ExtractedData`: Outputs the extracted data and the agent ID from the workflow run. Subclass of `StopEvent`, so that the workflow knows to stop when this event is received.\\n\",\n    \"\\n\",\n    \"### Workflow Design\\n\",\n    \"\\n\",\n    \"Using all the pieces above, we can now define our workflow.\\n\",\n    \"\\n\",\n    \"This workflow has a few key features that make it unique:\\n\",\n    \"1. It uses a human-in-the-loop to iteratively improve the schema for the extraction.\\n\",\n    \"2. The generated schema is validated using `jsonschema` before being sent to `LlamaExtract`.\\n\",\n    \"3. It loops between the schema generation and human approval until the schema is approved.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from workflows.events import (\\n\",\n    \"    Event,\\n\",\n    \"    HumanResponseEvent,\\n\",\n    \"    InputRequiredEvent,\\n\",\n    \"    StartEvent,\\n\",\n    \"    StopEvent,\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InputEvent(StartEvent):\\n\",\n    \"    \\\"\\\"\\\"Start the workflow by providing a file path and a prompt.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    file_path: str\\n\",\n    \"    prompt: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ParsedContent(Event):\\n\",\n    \"    \\\"\\\"\\\"Parses the content and carry forward the content and prompt.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    file_content: str\\n\",\n    \"    prompt: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class RunExtraction(Event):\\n\",\n    \"    \\\"\\\"\\\"Runs the extraction on the provided file content and schema.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    generated_schema: dict\\n\",\n    \"    file_content: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ProposedSchema(InputRequiredEvent):\\n\",\n    \"    \\\"\\\"\\\"Proposes a schema for the extraction. Needs human approval.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    generated_schema: dict\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ApprovedSchema(HumanResponseEvent):\\n\",\n    \"    \\\"\\\"\\\"Handles the human approval of the proposed schema.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    approved: bool\\n\",\n    \"    feedback: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ExtractedData(StopEvent):\\n\",\n    \"    \\\"\\\"\\\"Outputs the extracted data and the agent ID from the workflow run.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    data: dict\\n\",\n    \"    agent_id: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ProgressEvent(Event):\\n\",\n    \"    \\\"\\\"\\\"Propagates a progress message to the user as the workflow runs.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    msg: str\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Workflow Design\\n\",\n    \"\\n\",\n    \"Using all the pieces above, we can now define our workflow.\\n\",\n    \"\\n\",\n    \"This workflow has a few key features that make it unique:\\n\",\n    \"1. It uses a human-in-the-loop to iteratively improve the schema for the extraction.\\n\",\n    \"2. The generated schema is validated using `jsonschema` before being sent to `LlamaExtract`.\\n\",\n    \"3. It loops between the schema generation and human approval until the schema is approved.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 14,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import re\\n\",\n    \"import uuid\\n\",\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from jsonschema import Draft202012Validator\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class IterativeExtractionWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def parse_file(\\n\",\n    \"        self,\\n\",\n    \"        ev: InputEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        parser: Annotated[LlamaParse, Resource(get_parse_client)],\\n\",\n    \"    ) -> ParsedContent:\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=f\\\"Parsing file: {ev.file_path}\\\"))\\n\",\n    \"        result = await parser.aparse(ev.file_path)\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"File parsed successfully\\\"))\\n\",\n    \"\\n\",\n    \"        # Update the state with the file content and path\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.original_prompt = ev.prompt\\n\",\n    \"            state.file_path = ev.file_path\\n\",\n    \"            state.file_content = \\\"\\\\n\\\\n\\\".join([page.md for page in result.pages])\\n\",\n    \"\\n\",\n    \"        return ParsedContent(file_content=state.file_content, prompt=ev.prompt)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def propose_schema(\\n\",\n    \"        self,\\n\",\n    \"        ev: ParsedContent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        client: Annotated[AsyncOpenAI, Resource(get_openai_client)],\\n\",\n    \"    ) -> ProposedSchema:\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Proposing schema\\\"))\\n\",\n    \"\\n\",\n    \"        # Inject feedback from previous attempts if available\\n\",\n    \"        state = await ctx.store.get_state()\\n\",\n    \"        if state.current_feedback and state.current_schema:\\n\",\n    \"            past_attempt_str = f\\\"\\\\n<past_attempt>\\\\n<feedback>{state.current_feedback}</feedback>\\\\n<schema>{str(state.current_schema)}</schema>\\\\n</past_attempt>\\\\n\\\"\\n\",\n    \"        else:\\n\",\n    \"            past_attempt_str = \\\"\\\"\\n\",\n    \"\\n\",\n    \"        # Start the extraction process with a fresh chat history\\n\",\n    \"        prompt = extract_prompt.format(\\n\",\n    \"            file_content=ev.file_content,\\n\",\n    \"            prompt=ev.prompt,\\n\",\n    \"            past_attempt=past_attempt_str,\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        history = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": prompt}]\\n\",\n    \"\\n\",\n    \"        # Generate a new schema using OpenAI\\n\",\n    \"        response = await client.responses.create(\\n\",\n    \"            input=history,\\n\",\n    \"            model=\\\"gpt-4.1\\\",\\n\",\n    \"            temperature=1.0,\\n\",\n    \"            store=False,\\n\",\n    \"        )\\n\",\n    \"        history.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.output_text})\\n\",\n    \"\\n\",\n    \"        # Try to parse the schema from the response. If it fails, try again, using chat history\\n\",\n    \"        # to keep track of failed attempts.\\n\",\n    \"        attempts = 1\\n\",\n    \"        schema = {}\\n\",\n    \"        while attempts <= 3 and not schema:\\n\",\n    \"            try:\\n\",\n    \"                ctx.write_event_to_stream(\\n\",\n    \"                    ProgressEvent(\\n\",\n    \"                        msg=f\\\"Attempting to parse schema string from:\\\\n{response.output_text}\\\"\\n\",\n    \"                    )\\n\",\n    \"                )\\n\",\n    \"                json_str = re.sub(\\n\",\n    \"                    r\\\"<schema>([\\\\s\\\\S]*)<\\\\/schema>\\\", r\\\"\\\\1\\\", response.output_text\\n\",\n    \"                )\\n\",\n    \"\\n\",\n    \"                # Validate the schema\\n\",\n    \"                schema = json.loads(json_str)\\n\",\n    \"                Draft202012Validator.check_schema(schema)\\n\",\n    \"\\n\",\n    \"                async with ctx.store.edit_state() as state:\\n\",\n    \"                    state.current_schema = schema\\n\",\n    \"\\n\",\n    \"                break\\n\",\n    \"            except Exception as e:\\n\",\n    \"                ctx.write_event_to_stream(\\n\",\n    \"                    ProgressEvent(msg=f\\\"Schema parsing failed:\\\\n{e}\\\\n\\\\nTrying again...\\\")\\n\",\n    \"                )\\n\",\n    \"                history.append(\\n\",\n    \"                    {\\\"role\\\": \\\"user\\\", \\\"content\\\": f\\\"Error: {e}\\\\n\\\\nPlease try again.\\\"}\\n\",\n    \"                )\\n\",\n    \"                response = await client.responses.create(\\n\",\n    \"                    input=history,\\n\",\n    \"                    model=\\\"gpt-4.1\\\",\\n\",\n    \"                    temperature=1.0,\\n\",\n    \"                    store=False,\\n\",\n    \"                )\\n\",\n    \"                history.append({\\\"role\\\": \\\"assistant\\\", \\\"content\\\": response.output_text})\\n\",\n    \"                attempts += 1\\n\",\n    \"\\n\",\n    \"        if attempts > 3:\\n\",\n    \"            raise Exception(\\\"Failed to propose a valid schema after 3 attempts!\\\")\\n\",\n    \"\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Schema proposed successfully\\\"))\\n\",\n    \"        return ProposedSchema(generated_schema=schema)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def handle_schema_approval(\\n\",\n    \"        self,\\n\",\n    \"        ev: ApprovedSchema,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"    ) -> ParsedContent | RunExtraction:\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.current_feedback = ev.feedback\\n\",\n    \"\\n\",\n    \"        # If the schema is approved, run the extraction. Otherwise, go back to the start and try again.\\n\",\n    \"        if ev.approved:\\n\",\n    \"            return RunExtraction(\\n\",\n    \"                generated_schema=state.current_schema, file_content=state.file_content\\n\",\n    \"            )\\n\",\n    \"        else:\\n\",\n    \"            return ParsedContent(\\n\",\n    \"                file_content=state.file_content, prompt=state.original_prompt\\n\",\n    \"            )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def run_extraction(\\n\",\n    \"        self,\\n\",\n    \"        ev: RunExtraction,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        extract: Annotated[LlamaExtract, Resource(get_extract_client)],\\n\",\n    \"    ) -> ExtractedData:\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Running extraction\\\"))\\n\",\n    \"\\n\",\n    \"        # Persist an extraction agent + schema to llama-cloud\\n\",\n    \"        agent = extract.create_agent(\\n\",\n    \"            name=f\\\"extraction_workflow_{uuid.uuid4()}\\\",\\n\",\n    \"            data_schema=ev.generated_schema,\\n\",\n    \"            config=ExtractConfig(\\n\",\n    \"                extraction_mode=ExtractMode.BALANCED,\\n\",\n    \"            ),\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        # Run the extraction\\n\",\n    \"        file_path = await ctx.store.get(\\\"file_path\\\")\\n\",\n    \"        result = await agent.aextract(\\n\",\n    \"            files=SourceText(text_content=ev.file_content, filename=file_path),\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        return ExtractedData(data=result.data, agent_id=agent.id)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Running the Workflow\\n\",\n    \"\\n\",\n    \"First, let's download some sample data to work with.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!wget https://arxiv.org/pdf/2506.05176 -O qwen3_embed_paper.pdf\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"When running the workflow, since we have human-in-the-loop, we have two options for how to run the workflow:\\n\",\n    \"1. Assume the workflow will run to completion while waiting for human input. This setup is useful for \\\"online\\\" applications like websockets, running in a CLI, or similar environments where you expect a response from the user quickly.\\n\",\n    \"2. Assuming the workflow will need to pause and restart once human input is recieved. In this case, we need to serialize the workflow context and restart the workflow from the point of the pause when the human input is recieved. This setup is useful for more asynchronous applications, like a REST API, where you expect the user to take some time to respond.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Option 1: Running the Workflow to Completion\\n\",\n    \"\\n\",\n    \"In this option, we'll run the workflow to completion while waiting for human input. This setup is useful for \\\"online\\\" applications like websockets, running in a CLI, or similar environments where you expect a response from the user quickly.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 15,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Parsing file: ./qwen3_embed_paper.pdf\\n\",\n      \"Started parsing the file under job_id 9315e44f-6f5e-439f-a088-0e8aeacc1d56\\n\",\n      \"File parsed successfully\\n\",\n      \"Proposing schema\\n\",\n      \"Attempting to parse schema string from:\\n\",\n      \"<schema>\\n\",\n      \"{\\n\",\n      \"  \\\"type\\\": \\\"object\\\",\\n\",\n      \"  \\\"properties\\\": {\\n\",\n      \"    \\\"title\\\": {\\n\",\n      \"      \\\"type\\\": \\\"string\\\",\\n\",\n      \"      \\\"description\\\": \\\"The title of the paper.\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"authors\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"description\\\": \\\"A list of the authors of the paper.\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      }\\n\",\n      \"    },\\n\",\n      \"    \\\"key_takeaways\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"description\\\": \\\"A list of the main findings or key insights from the paper.\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      }\\n\",\n      \"    }\\n\",\n      \"  },\\n\",\n      \"  \\\"required\\\": [\\\"title\\\", \\\"authors\\\", \\\"key_takeaways\\\"]\\n\",\n      \"}\\n\",\n      \"</schema>\\n\",\n      \"Schema proposed successfully\\n\",\n      \"Proposed schema: {'type': 'object', 'properties': {'title': {'type': 'string', 'description': 'The title of the paper.'}, 'authors': {'type': 'array', 'description': 'A list of the authors of the paper.', 'items': {'type': 'string'}}, 'key_takeaways': {'type': 'array', 'description': 'A list of the main findings or key insights from the paper.', 'items': {'type': 'string'}}}, 'required': ['title', 'authors', 'key_takeaways']}\\n\",\n      \"Approved? can you add a section about the datasets used in the paper?\\n\",\n      \"Proposing schema\\n\",\n      \"Attempting to parse schema string from:\\n\",\n      \"<schema>\\n\",\n      \"{\\n\",\n      \"  \\\"type\\\": \\\"object\\\",\\n\",\n      \"  \\\"properties\\\": {\\n\",\n      \"    \\\"title\\\": {\\n\",\n      \"      \\\"type\\\": \\\"string\\\",\\n\",\n      \"      \\\"description\\\": \\\"The title of the paper.\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"authors\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"description\\\": \\\"A list of the authors of the paper.\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      }\\n\",\n      \"    },\\n\",\n      \"    \\\"key_takeaways\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"description\\\": \\\"A list of the main findings or key insights from the paper.\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      }\\n\",\n      \"    },\\n\",\n      \"    \\\"datasets\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"description\\\": \\\"A list of datasets used in the paper, including synthetic and publicly available datasets.\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"object\\\",\\n\",\n      \"        \\\"properties\\\": {\\n\",\n      \"          \\\"name\\\": { \\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"The name of the dataset.\\\" },\\n\",\n      \"          \\\"type\\\": { \\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"The type/category of the dataset, e.g. 'synthetic', 'benchmark', 'retrieval', etc.\\\" },\\n\",\n      \"          \\\"description\\\": { \\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"A brief description of the dataset and its role in the study.\\\" },\\n\",\n      \"          \\\"size\\\": { \\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Approximate size/count, if available.\\\" }\\n\",\n      \"        },\\n\",\n      \"        \\\"required\\\": [\\\"name\\\"]\\n\",\n      \"      }\\n\",\n      \"    }\\n\",\n      \"  },\\n\",\n      \"  \\\"required\\\": [\\\"title\\\", \\\"authors\\\", \\\"key_takeaways\\\", \\\"datasets\\\"]\\n\",\n      \"}\\n\",\n      \"</schema>\\n\",\n      \"\\n\",\n      \"Schema proposed successfully\\n\",\n      \"Proposed schema: {'type': 'object', 'properties': {'title': {'type': 'string', 'description': 'The title of the paper.'}, 'authors': {'type': 'array', 'description': 'A list of the authors of the paper.', 'items': {'type': 'string'}}, 'key_takeaways': {'type': 'array', 'description': 'A list of the main findings or key insights from the paper.', 'items': {'type': 'string'}}, 'datasets': {'type': 'array', 'description': 'A list of datasets used in the paper, including synthetic and publicly available datasets.', 'items': {'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name of the dataset.'}, 'type': {'type': 'string', 'description': \\\"The type/category of the dataset, e.g. 'synthetic', 'benchmark', 'retrieval', etc.\\\"}, 'description': {'type': 'string', 'description': 'A brief description of the dataset and its role in the study.'}, 'size': {'type': 'string', 'description': 'Approximate size/count, if available.'}}, 'required': ['name']}}}, 'required': ['title', 'authors', 'key_takeaways', 'datasets']}\\n\",\n      \"Approved? y\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Uploading files:   0%|          | 0/1 [00:00<?, ?it/s]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Running extraction\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Uploading files: 100%|██████████| 1/1 [00:01<00:00,  1.04s/it]\\n\",\n      \"Creating extraction jobs: 100%|██████████| 1/1 [00:01<00:00,  1.12s/it]\\n\",\n      \"Extracting files: 100%|██████████| 1/1 [00:15<00:00, 15.05s/it]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Agent ID: 374730cc-974e-42f6-a64b-b5f163ad8818\\n\",\n      \"Extracted data: {'title': 'Qwen3 Embedding: Advancing Text Embedding and Reranking Through Foundation Models', 'authors': ['Yanzhao Zhang', 'Mingxin Li', 'Dingkun Long', 'Xin Zhang', 'Huan Lin', 'Baosong Yang', 'Pengjun Xie', 'An Yang', 'Dayiheng Liu', 'Junyang Lin', 'Fei Huang', 'Jingren Zhou'], 'key_takeaways': ['Qwen3 Embedding series introduces advanced text embedding and reranking models based on Qwen3 foundation models, supporting multiple model sizes (0.6B, 4B, 8B) for diverse deployment needs.', 'The models leverage a multi-stage training pipeline: large-scale weakly supervised pre-training on synthetic data, supervised fine-tuning on high-quality datasets, and model merging for robustness and generalization.', 'Qwen3 LLMs are used not only as backbones but also to synthesize high-quality, diverse, multilingual training data, enhancing the training process.', 'The Qwen3 Embedding models achieve state-of-the-art results on multiple benchmarks, including MTEB (Multilingual, English, Code), CMTEB, MMTEB, and various retrieval tasks, outperforming or matching leading proprietary and open-source models.', 'Ablation studies show that both large-scale synthetic data pre-training and model merging are critical for achieving superior performance.', 'The models and code are open-sourced under Apache 2.0 to promote reproducibility and community research.'], 'datasets': [{'name': 'Synthetic Data (for weakly supervised pre-training)', 'type': 'synthetic', 'description': 'Large-scale synthetic text pair data generated using Qwen3-32B for tasks such as retrieval, bitext mining, classification, and semantic textual similarity. Data is generated with diverse prompts to ensure variety in task, language, length, and difficulty.', 'size': '~150M pairs'}, {'name': 'High-quality Synthetic Data (for supervised fine-tuning)', 'type': 'synthetic', 'description': 'Filtered subset of synthetic data with cosine similarity > 0.7, used for supervised fine-tuning to enhance model performance and generalization.', 'size': '~12M pairs'}, {'name': 'Labeled Data (for supervised fine-tuning)', 'type': 'benchmark', 'description': 'A collection of high-quality labeled datasets used in supervised fine-tuning, including MS MARCO, NQ, HotpotQA, NLI, Dureader, T²-Ranking, SimCLUE, MIRACL, MLDR, Mr.TyDi, Multi-CPR, CodeSearchNet, etc.', 'size': '~7M pairs'}, {'name': 'MTEB (Massive Text Embedding Benchmark)', 'type': 'benchmark', 'description': 'A large-scale benchmark for evaluating text embedding models, including multilingual, English, and code tasks. Used for comprehensive evaluation of model performance.', 'size': 'MTEB Multilingual: 131 tasks; MTEB English v2: 41 tasks; MTEB Code: 12 tasks'}, {'name': 'MMTEB (Massive Multilingual Text Embedding Benchmark)', 'type': 'benchmark', 'description': 'An expansion of MTEB, covering over 500 quality-controlled evaluation tasks across multiple languages and domains.', 'size': '216 tasks used in evaluation (from MTEB Multilingual, English, CMTEB, and Code)'}, {'name': 'CMTEB (Chinese Massive Text Embedding Benchmark)', 'type': 'benchmark', 'description': 'A benchmark for evaluating Chinese text embedding models, used in the evaluation of Qwen3 models.', 'size': '32 tasks'}, {'name': 'MLDR (Multilingual Long Document Retrieval)', 'type': 'benchmark', 'description': 'A benchmark for multilingual long document retrieval tasks, used in reranking evaluation.', 'size': None}, {'name': 'FollowIR', 'type': 'benchmark', 'description': 'A benchmark for complex instruction retrieval tasks, used in reranking evaluation.', 'size': None}]}\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"wf = IterativeExtractionWorkflow(timeout=None)\\n\",\n    \"\\n\",\n    \"handler = wf.run(\\n\",\n    \"    file_path=\\\"./qwen3_embed_paper.pdf\\\",\\n\",\n    \"    prompt=\\\"Extract the title, authors, and key takeaways from the paper.\\\",\\n\",\n    \")\\n\",\n    \"async for ev in handler.stream_events():\\n\",\n    \"    if isinstance(ev, ProgressEvent):\\n\",\n    \"        print(ev.msg, flush=True)\\n\",\n    \"    elif isinstance(ev, ProposedSchema):\\n\",\n    \"        print(f\\\"Proposed schema: {ev.generated_schema}\\\", flush=True)\\n\",\n    \"        approved = input(\\\"Approve? (y/<reason>): \\\").strip().lower()\\n\",\n    \"        print(f\\\"Approved? {approved}\\\", flush=True)\\n\",\n    \"        if approved == \\\"y\\\":\\n\",\n    \"            handler.ctx.send_event(ApprovedSchema(approved=True, feedback=\\\"Approved\\\"))\\n\",\n    \"        else:\\n\",\n    \"            handler.ctx.send_event(ApprovedSchema(approved=False, feedback=approved))\\n\",\n    \"\\n\",\n    \"result = await handler\\n\",\n    \"print(f\\\"Agent ID: {result.agent_id}\\\", flush=True)\\n\",\n    \"print(f\\\"Extracted data: {result.data}\\\", flush=True)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Option 2: Serializing the Workflow Context\\n\",\n    \"\\n\",\n    \"In this option, we'll serialize the workflow context and restart the workflow from the point of the pause when the human input is recieved. This setup is useful for more asynchronous applications, like a REST API, where you expect the user to take some time to respond.\\n\",\n    \"\\n\",\n    \"```\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 16,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# Faking some DB for storing the workflow context between runs\\n\",\n    \"in_memory_store = {}\\n\",\n    \"\\n\",\n    \"# Define the workflow instance\\n\",\n    \"wf = IterativeExtractionWorkflow(timeout=None)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"# Define a function to run the workflow\\n\",\n    \"async def run_workflow(\\n\",\n    \"    file_path: str | None = None,\\n\",\n    \"    prompt: str | None = None,\\n\",\n    \"    response_ev: ApprovedSchema | None = None,\\n\",\n    \") -> ProposedSchema | ExtractedData:\\n\",\n    \"    # Check if there's existing context from a previous run\\n\",\n    \"    existing_context = in_memory_store.get(\\\"workflow_context\\\")\\n\",\n    \"    if existing_context:\\n\",\n    \"        ctx = Context.from_dict(wf, existing_context)\\n\",\n    \"        handler = wf.run(ctx=ctx)\\n\",\n    \"        handler.ctx.send_event(response_ev)\\n\",\n    \"    else:\\n\",\n    \"        handler = wf.run(file_path=file_path, prompt=prompt)\\n\",\n    \"\\n\",\n    \"    # Stream events until we get to the end of the workflow or hit a input required event\\n\",\n    \"    async for ev in handler.stream_events():\\n\",\n    \"        if isinstance(ev, ProgressEvent):\\n\",\n    \"            print(ev.msg, flush=True)\\n\",\n    \"        elif isinstance(ev, ProposedSchema):\\n\",\n    \"            # Here, you would serialize the workflow context and save it to some DB or storage\\n\",\n    \"            # And resume the workflow from the point of the pause when the human input is recieved.\\n\",\n    \"            in_memory_store[\\\"workflow_context\\\"] = handler.ctx.to_dict()\\n\",\n    \"            await handler.cancel_run()\\n\",\n    \"            return ev\\n\",\n    \"\\n\",\n    \"    return await handler\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 17,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Parsing file: ./qwen3_embed_paper.pdf\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Started parsing the file under job_id cc7807e8-bc53-43ec-a467-7b5df172ae83\\n\",\n      \"File parsed successfully\\n\",\n      \"Proposing schema\\n\",\n      \"Attempting to parse schema string from:\\n\",\n      \"<schema>\\n\",\n      \"{\\n\",\n      \"  \\\"type\\\": \\\"object\\\",\\n\",\n      \"  \\\"properties\\\": {\\n\",\n      \"    \\\"title\\\": {\\n\",\n      \"      \\\"type\\\": \\\"string\\\",\\n\",\n      \"      \\\"description\\\": \\\"The full title of the paper\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"authors\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      },\\n\",\n      \"      \\\"description\\\": \\\"A list of authors as full names\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"key_takeaways\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      },\\n\",\n      \"      \\\"description\\\": \\\"A list of the main contributions, findings, or conclusions drawn from the paper\\\"\\n\",\n      \"    }\\n\",\n      \"  },\\n\",\n      \"  \\\"required\\\": [\\\"title\\\", \\\"authors\\\", \\\"key_takeaways\\\"]\\n\",\n      \"}\\n\",\n      \"</schema>\\n\",\n      \"\\n\",\n      \"Schema proposed successfully\\n\",\n      \"Proposed schema: {'type': 'object', 'properties': {'title': {'type': 'string', 'description': 'The full title of the paper'}, 'authors': {'type': 'array', 'items': {'type': 'string'}, 'description': 'A list of authors as full names'}, 'key_takeaways': {'type': 'array', 'items': {'type': 'string'}, 'description': 'A list of the main contributions, findings, or conclusions drawn from the paper'}}, 'required': ['title', 'authors', 'key_takeaways']}\\n\",\n      \"Approved? can you add a section about the datasets used in the paper?\\n\",\n      \"Proposing schema\\n\",\n      \"Attempting to parse schema string from:\\n\",\n      \"<schema>\\n\",\n      \"{\\n\",\n      \"  \\\"type\\\": \\\"object\\\",\\n\",\n      \"  \\\"properties\\\": {\\n\",\n      \"    \\\"title\\\": {\\n\",\n      \"      \\\"type\\\": \\\"string\\\",\\n\",\n      \"      \\\"description\\\": \\\"The full title of the paper\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"authors\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      },\\n\",\n      \"      \\\"description\\\": \\\"A list of authors as full names\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"key_takeaways\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"string\\\"\\n\",\n      \"      },\\n\",\n      \"      \\\"description\\\": \\\"A list of the main contributions, findings, or conclusions drawn from the paper\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"datasets\\\": {\\n\",\n      \"      \\\"type\\\": \\\"array\\\",\\n\",\n      \"      \\\"items\\\": {\\n\",\n      \"        \\\"type\\\": \\\"object\\\",\\n\",\n      \"        \\\"properties\\\": {\\n\",\n      \"          \\\"name\\\": { \\\"type\\\": \\\"string\\\" },\\n\",\n      \"          \\\"type\\\": { \\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Type or purpose of the dataset (e.g., Weakly Supervised, Synthetic, Supervised Fine Tuning)\\\" },\\n\",\n      \"          \\\"size\\\": { \\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Size or number of examples\\\" },\\n\",\n      \"          \\\"notes\\\": { \\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Additional details about the dataset, such as source, languages, or tasks covered\\\" }\\n\",\n      \"        },\\n\",\n      \"        \\\"required\\\": [\\\"name\\\"]\\n\",\n      \"      },\\n\",\n      \"      \\\"description\\\": \\\"A list of datasets used or introduced in the paper, including key attributes for each dataset.\\\"\\n\",\n      \"    }\\n\",\n      \"  },\\n\",\n      \"  \\\"required\\\": [\\\"title\\\", \\\"authors\\\", \\\"key_takeaways\\\", \\\"datasets\\\"]\\n\",\n      \"}\\n\",\n      \"</schema>\\n\",\n      \"\\n\",\n      \"Schema proposed successfully\\n\",\n      \"Proposed schema: {'type': 'object', 'properties': {'title': {'type': 'string', 'description': 'The full title of the paper'}, 'authors': {'type': 'array', 'items': {'type': 'string'}, 'description': 'A list of authors as full names'}, 'key_takeaways': {'type': 'array', 'items': {'type': 'string'}, 'description': 'A list of the main contributions, findings, or conclusions drawn from the paper'}, 'datasets': {'type': 'array', 'items': {'type': 'object', 'properties': {'name': {'type': 'string'}, 'type': {'type': 'string', 'description': 'Type or purpose of the dataset (e.g., Weakly Supervised, Synthetic, Supervised Fine Tuning)'}, 'size': {'type': 'string', 'description': 'Size or number of examples'}, 'notes': {'type': 'string', 'description': 'Additional details about the dataset, such as source, languages, or tasks covered'}}, 'required': ['name']}, 'description': 'A list of datasets used or introduced in the paper, including key attributes for each dataset.'}}, 'required': ['title', 'authors', 'key_takeaways', 'datasets']}\\n\",\n      \"Approved? y\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Uploading files:   0%|          | 0/1 [00:00<?, ?it/s]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Running extraction\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Uploading files: 100%|██████████| 1/1 [00:01<00:00,  1.21s/it]\\n\",\n      \"Creating extraction jobs: 100%|██████████| 1/1 [00:00<00:00,  1.01it/s]\\n\",\n      \"Extracting files: 100%|██████████| 1/1 [00:14<00:00, 14.94s/it]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Extracted data: {'title': 'Qwen3 Embedding: Advancing Text Embedding and Reranking Through Foundation Models', 'authors': ['Yanzhao Zhang', 'Mingxin Li', 'Dingkun Long', 'Xin Zhang', 'Huan Lin', 'Baosong Yang', 'Pengjun Xie', 'An Yang', 'Dayiheng Liu', 'Junyang Lin', 'Fei Huang', 'Jingren Zhou'], 'key_takeaways': ['Qwen3 Embedding series introduces advanced text embedding and reranking models based on Qwen3 foundation models, significantly improving over the previous GTE-Qwen series.', 'The models leverage a multi-stage training pipeline: large-scale weakly supervised pre-training on synthetic data, supervised fine-tuning on high-quality datasets, and model merging for robustness and generalization.', 'Qwen3 LLMs are used both as backbone models and as generators of high-quality, diverse, multilingual synthetic training data, enhancing the training process.', 'The series includes models of various sizes (0.6B, 4B, 8B parameters) for both embedding and reranking, supporting flexible deployment for efficiency or effectiveness.', 'Empirical results show state-of-the-art performance on multiple benchmarks, including MTEB (multilingual, English, Chinese, code), MMTEB, and various retrieval tasks, outperforming or matching leading proprietary and open-source models.', 'The synthetic dataset generation is highly controllable, allowing for task, language, length, and difficulty customization, resulting in 150M weakly supervised pairs and 12M high-quality pairs for fine-tuning.', 'Model merging via spherical linear interpolation (slerp) of checkpoints further boosts robustness and generalization.', 'The models and code are open-sourced under Apache 2.0 to promote reproducibility and community research.'], 'datasets': [{'name': 'Synthetic Data (for Weakly Supervised Pre-Training)', 'type': 'Weakly Supervised / Synthetic', 'size': '~150M pairs', 'notes': 'Generated using Qwen3-32B; covers retrieval, bitext mining, classification, and semantic textual similarity tasks; multilingual and cross-lingual; data synthesis is highly controllable (task, language, length, difficulty).'}, {'name': 'High-quality Synthetic Data (for Supervised Fine Tuning)', 'type': 'Synthetic (filtered)', 'size': '~12M pairs', 'notes': 'Filtered from the 150M synthetic pairs using cosine similarity > 0.7; used in the second stage of supervised training.'}, {'name': 'Labeled Data (for Supervised Fine Tuning)', 'type': 'Supervised Fine Tuning', 'size': '~7M pairs', 'notes': 'Includes MS MARCO, NQ, HotpotQA, NLI, Dureader, T²-Ranking, SimCLUE, MIRACL, MLDR, Mr.TyDi, Multi-CPR, CodeSearchNet, etc.; covers multiple languages and tasks.'}, {'name': 'MMTEB (Massive Multilingual Text Embedding Benchmark)', 'type': 'Evaluation Benchmark', 'size': '216 tasks (131 MTEB Multilingual, 41 MTEB English v2, 32 CMTEB, 12 MTEB Code)', 'notes': 'Covers over 500 evaluation tasks in total; used for comprehensive evaluation of embedding models.'}, {'name': 'MTEB (Massive Text Embedding Benchmark)', 'type': 'Evaluation Benchmark', 'size': 'Not specified (part of MMTEB)', 'notes': 'Includes MTEB Multilingual, MTEB English v2, MTEB Code; used for evaluation.'}, {'name': 'CMTEB (Chinese Massive Text Embedding Benchmark)', 'type': 'Evaluation Benchmark', 'size': '32 tasks (as part of MMTEB)', 'notes': 'Chinese language evaluation.'}, {'name': 'MTEB-Code', 'type': 'Evaluation Benchmark', 'size': '12 code retrieval tasks', 'notes': 'Code-related retrieval evaluation.'}, {'name': 'MLDR, MIRACL, Mr.TyDi, Multi-CPR, CodeSearchNet, T²-Ranking, SimCLUE, Dureader, NQ, HotpotQA, NLI, MS MARCO', 'type': 'Supervised Fine Tuning / Evaluation', 'size': 'Included in ~7M labeled data', 'notes': 'Used for supervised fine-tuning and/or evaluation; covers various languages and tasks.'}, {'name': 'FollowIR', 'type': 'Evaluation Benchmark', 'size': 'Not specified', 'notes': 'Used for complex instruction retrieval evaluation.'}]}\\n\",\n      \"Agent ID: a4c1a25c-c00e-4a0d-b89d-1ac7099355db\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Run the workflow in a loop, waiting for human input between schema generations.\\n\",\n    \"# Loop is over once we get to the ExtractedData event.\\n\",\n    \"ev = await run_workflow(\\n\",\n    \"    file_path=\\\"./qwen3_embed_paper.pdf\\\",\\n\",\n    \"    prompt=\\\"Extract the title, authors, and key takeaways from the paper.\\\",\\n\",\n    \")\\n\",\n    \"while not isinstance(ev, ExtractedData):\\n\",\n    \"    if isinstance(ev, ProposedSchema):\\n\",\n    \"        print(f\\\"Proposed schema: {ev.generated_schema}\\\", flush=True)\\n\",\n    \"        approved = input(\\\"Approve? (y/<reason>): \\\").strip()\\n\",\n    \"        print(f\\\"Approved? {approved}\\\", flush=True)\\n\",\n    \"        if approved.lower() == \\\"y\\\":\\n\",\n    \"            ev = await run_workflow(\\n\",\n    \"                response_ev=ApprovedSchema(approved=True, feedback=\\\"Approved\\\")\\n\",\n    \"            )\\n\",\n    \"        else:\\n\",\n    \"            ev = await run_workflow(\\n\",\n    \"                response_ev=ApprovedSchema(approved=False, feedback=approved)\\n\",\n    \"            )\\n\",\n    \"    else:\\n\",\n    \"        break\\n\",\n    \"\\n\",\n    \"print(f\\\"Extracted data: {ev.data}\\\", flush=True)\\n\",\n    \"print(f\\\"Agent ID: {ev.agent_id}\\\", flush=True)\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.3\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/durable_workflows.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"7bHdUvTD1MGa\"\n   },\n   \"source\": [\n    \"Workflows are ephemeral by default, meaning that once the `run()` method returns its result, the workflow state is lost. A subsequent call to `run()` on the same workflow instance will start from a fresh state.\\n\",\n    \"\\n\",\n    \"If the use case requires to persist the workflow state  across multiple runs and possibly different processes, there are a few strategies that can be used to make workflows more durable.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"N0PWAEUE1AN2\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!pip install llama-index-workflows\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"10SuevfQ1Suz\"\n   },\n   \"source\": [\n    \"## Storing data in the workflow instance\\n\",\n    \"\\n\",\n    \"Workflows are regular Python classes, and data can be stored in class or instance variables, so that subsequent `run()` invocations can access it.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"vT5JIA5V1Ugx\",\n    \"outputId\": \"37ade20e-534a-4049-b67b-dcab70ba98d4\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"The step ran 1 times\\n\",\n      \"The step ran 2 times\\n\",\n      \"The step ran 3 times\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    def __init__(self, *args, **kwargs):\\n\",\n    \"        self.counter = 0\\n\",\n    \"        super().__init__(*args, **kwargs)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    def count(self, ev: StartEvent) -> StopEvent:\\n\",\n    \"        self.counter += 1\\n\",\n    \"        return StopEvent(result=f\\\"The step ran {self.counter} times\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\\n\",\n    \"for _ in range(3):\\n\",\n    \"    print(await w.run())\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"HhIoDaeT22n5\"\n   },\n   \"source\": [\n    \"## Storing data in the context object\\n\",\n    \"\\n\",\n    \"Each workflow comes with a special object responsible for its runtime operations called `Context`. The context instance is available to any step of a workflow and comes with a `store` property that can be used to store and load state data. Using the state store has two major advantages compared to class and instance variables:\\n\",\n    \"\\n\",\n    \"- It’s async safe and supports concurrent access\\n\",\n    \"- It can be serialized\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"w5x_Z4-_5vGd\",\n    \"outputId\": \"c75afe24-3aee-44e7-fe8e-743941055402\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"The step ran 1 times\\n\",\n      \"The step ran 2 times\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def count(self, ctx: Context, ev: StartEvent) -> StopEvent:\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            counter = state.get(\\\"counter\\\", 1)\\n\",\n    \"            retval = StopEvent(result=f\\\"The step ran {counter} times\\\")\\n\",\n    \"            state[\\\"counter\\\"] = counter + 1\\n\",\n    \"        return retval\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\\n\",\n    \"handler = w.run()\\n\",\n    \"print(await handler)\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\\n\",\n    \"handler = w.run(ctx=handler.ctx)\\n\",\n    \"print(await handler)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"BDB7BAHB7iY_\"\n   },\n   \"source\": [\n    \"## Using external resources to checkpoint execution\\n\",\n    \"\\n\",\n    \"To avoid any overhead, workflows don’t take snapshots of the current state automatically, so they can’t survive a fatal error on their own. However, any step can rely on some external database like Redis and snapshot the current context on sensitive parts of the code.\\n\",\n    \"\\n\",\n    \"For example, given a long running workflow processing hundreds of documents, we could save the id of the last document successfully processed in the state store:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"42MML8XG7kLT\",\n    \"outputId\": \"773ad015-a40d-4d61-dc31-191dfa675526\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"The step ran 1 times\\n\",\n      \"The step ran 2 times\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"import json\\n\",\n    \"import sqlite3\\n\",\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.context import JsonSerializer\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_db() -> sqlite3.Connection:\\n\",\n    \"    return sqlite3.connect(\\\"mydb.db\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def count(\\n\",\n    \"        self,\\n\",\n    \"        ctx: Context,\\n\",\n    \"        ev: StartEvent,\\n\",\n    \"        db: Annotated[sqlite3.Connection, Resource(get_db)],\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            counter = state.get(\\\"counter\\\", 1)\\n\",\n    \"            retval = StopEvent(result=f\\\"The step ran {counter} times\\\")\\n\",\n    \"            state[\\\"counter\\\"] = counter + 1\\n\",\n    \"\\n\",\n    \"        cursor = db.cursor()\\n\",\n    \"        ctx_dict = ctx.to_dict(serializer=JsonSerializer())\\n\",\n    \"        cursor.execute(\\n\",\n    \"            \\\"INSERT OR REPLACE INTO state VALUES (?, ?)\\\",\\n\",\n    \"            (\\\"last_ctx\\\", json.dumps(ctx_dict)),\\n\",\n    \"        )\\n\",\n    \"        db.commit()\\n\",\n    \"\\n\",\n    \"        return retval\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"# Create a simple key-value table\\n\",\n    \"db = get_db()\\n\",\n    \"db.cursor().execute(\\n\",\n    \"    \\\"CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT)\\\"\\n\",\n    \")\\n\",\n    \"db.commit()\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\\n\",\n    \"print(await w.run())\\n\",\n    \"\\n\",\n    \"# State is stored in a DB now, we could restart the process here...\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\\n\",\n    \"cursor = db.cursor()\\n\",\n    \"cursor.execute(\\\"SELECT value FROM state WHERE key=?\\\", (\\\"last_ctx\\\",))\\n\",\n    \"ctx_json = cursor.fetchone()[0]\\n\",\n    \"restored_ctx = Context.from_dict(w, json.loads(ctx_json), serializer=JsonSerializer())\\n\",\n    \"print(await w.run(ctx=restored_ctx))\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"name\": \"python\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/eval_driven_prompt_refinement.ipynb",
    "content": "{\n  \"cells\": [\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"# Eval-Driven Prompt Refinement\\n\",\n        \"\\n\",\n        \"This notebook demonstrates a self-contained workflow that refines prompts through an evaluate-and-iterate loop.\\n\",\n        \"It combines four key workflow patterns in a single cohesive scenario:\\n\",\n        \"\\n\",\n        \"- **Looping with conditional exit** — retry until the evaluation passes or max iterations are reached\\n\",\n        \"- **Branching** — approve, retry with feedback, or escalate to a human\\n\",\n        \"- **State management** — track prompt history, evaluation scores, and iteration count\\n\",\n        \"- **Human-in-the-loop** — escalation path when automated refinement fails\\n\",\n        \"\\n\",\n        \"The workflow uses a mock evaluator so **no API keys are needed**.\\n\",\n        \"\\n\",\n        \"### How it works\\n\",\n        \"\\n\",\n        \"```\\n\",\n        \"StartEvent\\n\",\n        \"    │\\n\",\n        \"    ▼\\n\",\n        \"generate  ◄──── GeneratePromptEvent (automated retry)\\n\",\n        \"    │          ◄──── HumanFeedbackResponse (HITL resume)\\n\",\n        \"    │\\n\",\n        \"    ▼\\n\",\n        \"EvalResultEvent\\n\",\n        \"    │\\n\",\n        \"    ▼\\n\",\n        \"evaluate_and_decide\\n\",\n        \"    ├── score >= threshold  ──►  handle_approval  ──►  StopEvent\\n\",\n        \"    ├── retries remaining   ──►  GeneratePromptEvent (loop)\\n\",\n        \"    └── retries exhausted   ──►  HumanFeedbackRequired (HITL)\\n\",\n        \"                                        │\\n\",\n        \"                                        ▼\\n\",\n        \"                               HumanFeedbackResponse → generate\\n\",\n        \"```\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 1,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"!uv pip install llama-index-workflows -q\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## 1. Define Events\\n\",\n        \"\\n\",\n        \"Each event type defines a distinct edge in the workflow graph.\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 2,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"from workflows.events import (\\n\",\n        \"    Event,\\n\",\n        \"    HumanResponseEvent,\\n\",\n        \"    InputRequiredEvent,\\n\",\n        \"    StartEvent,\\n\",\n        \"    StopEvent,\\n\",\n        \")\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class GeneratePromptEvent(Event):\\n\",\n        \"    \\\"\\\"\\\"Triggers prompt generation. Carries optional feedback from a previous evaluation.\\\"\\\"\\\"\\n\",\n        \"\\n\",\n        \"    feedback: str = \\\"\\\"\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class EvalResultEvent(Event):\\n\",\n        \"    \\\"\\\"\\\"Carries the evaluation outcome for a candidate prompt.\\\"\\\"\\\"\\n\",\n        \"\\n\",\n        \"    prompt: str\\n\",\n        \"    score: float\\n\",\n        \"    feedback: str\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class ApprovedEvent(Event):\\n\",\n        \"    \\\"\\\"\\\"The prompt passed evaluation.\\\"\\\"\\\"\\n\",\n        \"\\n\",\n        \"    prompt: str\\n\",\n        \"    score: float\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class HumanFeedbackRequired(InputRequiredEvent):\\n\",\n        \"    \\\"\\\"\\\"Subclass of InputRequiredEvent so the caller can distinguish this prompt.\\\"\\\"\\\"\\n\",\n        \"\\n\",\n        \"    prompt: str\\n\",\n        \"    history: list[dict]\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class HumanFeedbackResponse(HumanResponseEvent):\\n\",\n        \"    \\\"\\\"\\\"Carries the human's feedback to resume the refinement loop.\\\"\\\"\\\"\\n\",\n        \"\\n\",\n        \"    response: str\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## 2. Mock Evaluator\\n\",\n        \"\\n\",\n        \"A deterministic evaluator that scores prompts based on simple heuristics.\\n\",\n        \"Each criterion awards partial credit so the score increases as the prompt improves.\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 3,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"DATASET = [\\n\",\n        \"    {\\\"input\\\": \\\"What is 2+2?\\\", \\\"expected\\\": \\\"4\\\"},\\n\",\n        \"    {\\\"input\\\": \\\"Summarize: The cat sat on the mat.\\\", \\\"expected\\\": \\\"A cat sat on a mat.\\\"},\\n\",\n        \"    {\\\"input\\\": \\\"Translate to French: Hello\\\", \\\"expected\\\": \\\"Bonjour\\\"},\\n\",\n        \"]\\n\",\n        \"\\n\",\n        \"CRITERIA = [\\n\",\n        \"    (\\\"specific\\\", [\\\"specific\\\", \\\"precise\\\", \\\"exact\\\", \\\"concise\\\"]),\\n\",\n        \"    (\\\"structured\\\", [\\\"step\\\", \\\"first\\\", \\\"then\\\", \\\"finally\\\", \\\"1.\\\", \\\"2.\\\"]),\\n\",\n        \"    (\\\"role\\\", [\\\"you are\\\", \\\"act as\\\", \\\"your role\\\", \\\"as a\\\"]),\\n\",\n        \"    (\\\"examples\\\", [\\\"example\\\", \\\"e.g.\\\", \\\"for instance\\\", \\\"such as\\\"]),\\n\",\n        \"    (\\\"constraints\\\", [\\\"must\\\", \\\"do not\\\", \\\"avoid\\\", \\\"ensure\\\", \\\"always\\\"]),\\n\",\n        \"]\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class MockEvaluator:\\n\",\n        \"    \\\"\\\"\\\"Scores prompts 0.0-1.0 based on keyword heuristics.\\\"\\\"\\\"\\n\",\n        \"\\n\",\n        \"    def __init__(self, dataset: list[dict]):\\n\",\n        \"        self.dataset = dataset\\n\",\n        \"\\n\",\n        \"    def evaluate(self, prompt: str) -> tuple[float, str]:\\n\",\n        \"        lower = prompt.lower()\\n\",\n        \"        hits = []\\n\",\n        \"        misses = []\\n\",\n        \"        for name, keywords in CRITERIA:\\n\",\n        \"            if any(kw in lower for kw in keywords):\\n\",\n        \"                hits.append(name)\\n\",\n        \"            else:\\n\",\n        \"                misses.append(name)\\n\",\n        \"\\n\",\n        \"        score = len(hits) / len(CRITERIA)\\n\",\n        \"\\n\",\n        \"        if misses:\\n\",\n        \"            feedback = f\\\"Missing qualities: {', '.join(misses)}. Try adding language that is {', '.join(misses)}.\\\"\\n\",\n        \"        else:\\n\",\n        \"            feedback = \\\"All criteria met.\\\"\\n\",\n        \"\\n\",\n        \"        return round(score, 2), feedback\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## 3. Mock Prompt Generator\\n\",\n        \"\\n\",\n        \"A template-based generator that incorporates feedback to produce\\n\",\n        \"incrementally better prompts. It adds at most **two** improvements per\\n\",\n        \"iteration, simulating realistic gradual refinement.\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 4,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"BASE_PROMPT = \\\"Answer the question.\\\"\\n\",\n        \"\\n\",\n        \"REFINEMENT_ADDITIONS = {\\n\",\n        \"    \\\"specific\\\": \\\"Be precise and concise in your answer.\\\",\\n\",\n        \"    \\\"structured\\\": \\\"First, understand the question. Then, provide a step-by-step answer.\\\",\\n\",\n        \"    \\\"role\\\": \\\"You are a helpful and knowledgeable assistant.\\\",\\n\",\n        \"    \\\"examples\\\": \\\"For instance, if asked about math, show your working (e.g. 2+2=4).\\\",\\n\",\n        \"    \\\"constraints\\\": \\\"You must answer accurately. Do not guess. Always verify your reasoning.\\\",\\n\",\n        \"}\\n\",\n        \"\\n\",\n        \"MAX_ADDITIONS_PER_ITERATION = 2\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"def generate_prompt(feedback: str, previous_prompt: str | None = None) -> str:\\n\",\n        \"    \\\"\\\"\\\"Build a prompt by appending up to MAX_ADDITIONS_PER_ITERATION sentences\\n\",\n        \"    that address missing qualities mentioned in the feedback.\\\"\\\"\\\"\\n\",\n        \"    base = previous_prompt or BASE_PROMPT\\n\",\n        \"    additions = []\\n\",\n        \"    lower_feedback = feedback.lower()\\n\",\n        \"    for quality, sentence in REFINEMENT_ADDITIONS.items():\\n\",\n        \"        if quality in lower_feedback and sentence not in base:\\n\",\n        \"            additions.append(sentence)\\n\",\n        \"            if len(additions) >= MAX_ADDITIONS_PER_ITERATION:\\n\",\n        \"                break\\n\",\n        \"    if additions:\\n\",\n        \"        return base + \\\" \\\" + \\\" \\\".join(additions)\\n\",\n        \"    return base\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## 4. Define the Workflow\\n\",\n        \"\\n\",\n        \"The `PromptRefinementWorkflow` ties everything together. HITL is modeled by `HumanFeedbackRequired` (a subclass of `InputRequiredEvent`) from the decide step and `HumanFeedbackResponse` (a `HumanResponseEvent`) feeding back into `generate`, without extra routing steps.\\n\",\n        \"\\n\",\n        \"| Step | Consumes | Produces | Pattern |\\n\",\n        \"|------|----------|----------|---------|\\n\",\n        \"| `generate` | `StartEvent`, `GeneratePromptEvent`, `HumanFeedbackResponse` | `EvalResultEvent` | **Loop** (re-entry); **HITL** resume |\\n\",\n        \"| `evaluate_and_decide` | `EvalResultEvent` | `ApprovedEvent` / `GeneratePromptEvent` / `HumanFeedbackRequired` | **Branching** (3-way) |\\n\",\n        \"| `handle_approval` | `ApprovedEvent` | `StopEvent` | Terminal |\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 5,\n      \"metadata\": {},\n      \"outputs\": [],\n      \"source\": [\n        \"from pydantic import BaseModel\\n\",\n        \"from workflows import Context, Workflow, step\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class RefinementState(BaseModel):\\n\",\n        \"    iteration: int = 0\\n\",\n        \"    history: list[dict] = []\\n\",\n        \"    current_prompt: str = \\\"\\\"\\n\",\n        \"\\n\",\n        \"\\n\",\n        \"class PromptRefinementWorkflow(Workflow):\\n\",\n        \"    def __init__(\\n\",\n        \"        self,\\n\",\n        \"        evaluator: MockEvaluator,\\n\",\n        \"        max_retries: int = 3,\\n\",\n        \"        threshold: float = 0.8,\\n\",\n        \"        **kwargs,\\n\",\n        \"    ):\\n\",\n        \"        super().__init__(**kwargs)\\n\",\n        \"        self.evaluator = evaluator\\n\",\n        \"        self.max_retries = max_retries\\n\",\n        \"        self.threshold = threshold\\n\",\n        \"\\n\",\n        \"    # -- Step 1: Generate a candidate prompt --------------------------------\\n\",\n        \"\\n\",\n        \"    @step\\n\",\n        \"    async def generate(\\n\",\n        \"        self,\\n\",\n        \"        ctx: Context[RefinementState],\\n\",\n        \"        ev: StartEvent | GeneratePromptEvent | HumanFeedbackResponse,\\n\",\n        \"    ) -> EvalResultEvent:\\n\",\n        \"        if isinstance(ev, HumanFeedbackResponse):\\n\",\n        \"            async with ctx.store.edit_state() as state:\\n\",\n        \"                state.iteration = 0\\n\",\n        \"            feedback = ev.response\\n\",\n        \"            print(f\\\"  Human feedback received: {ev.response!r}\\\")\\n\",\n        \"        elif isinstance(ev, GeneratePromptEvent):\\n\",\n        \"            feedback = ev.feedback\\n\",\n        \"        else:\\n\",\n        \"            feedback = \\\"\\\"\\n\",\n        \"\\n\",\n        \"        async with ctx.store.edit_state() as state:\\n\",\n        \"            state.iteration += 1\\n\",\n        \"            previous = state.current_prompt or None\\n\",\n        \"            candidate = generate_prompt(feedback, previous)\\n\",\n        \"            state.current_prompt = candidate\\n\",\n        \"\\n\",\n        \"        print(f\\\"  [iteration {state.iteration}] Generated prompt: {candidate!r}\\\")\\n\",\n        \"\\n\",\n        \"        score, eval_feedback = self.evaluator.evaluate(candidate)\\n\",\n        \"        return EvalResultEvent(prompt=candidate, score=score, feedback=eval_feedback)\\n\",\n        \"\\n\",\n        \"    # -- Step 2: Decide — approve, retry, or HITL ---------------------------\\n\",\n        \"\\n\",\n        \"    @step\\n\",\n        \"    async def evaluate_and_decide(\\n\",\n        \"        self, ctx: Context[RefinementState], ev: EvalResultEvent\\n\",\n        \"    ) -> ApprovedEvent | GeneratePromptEvent | HumanFeedbackRequired:\\n\",\n        \"        async with ctx.store.edit_state() as state:\\n\",\n        \"            state.history.append(\\n\",\n        \"                {\\\"prompt\\\": ev.prompt, \\\"score\\\": ev.score, \\\"feedback\\\": ev.feedback}\\n\",\n        \"            )\\n\",\n        \"            iteration = state.iteration\\n\",\n        \"            history = list(state.history)\\n\",\n        \"\\n\",\n        \"        print(\\n\",\n        \"            f\\\"  [iteration {iteration}] Score: {ev.score} (threshold: {self.threshold})\\\"\\n\",\n        \"        )\\n\",\n        \"\\n\",\n        \"        if ev.score >= self.threshold:\\n\",\n        \"            return ApprovedEvent(prompt=ev.prompt, score=ev.score)\\n\",\n        \"\\n\",\n        \"        if iteration < self.max_retries:\\n\",\n        \"            print(f\\\"  [iteration {iteration}] Retrying with feedback: {ev.feedback}\\\")\\n\",\n        \"            return GeneratePromptEvent(feedback=ev.feedback)\\n\",\n        \"\\n\",\n        \"        print(f\\\"  [iteration {iteration}] Max retries reached — escalating to human.\\\")\\n\",\n        \"        return HumanFeedbackRequired(prompt=ev.prompt, history=history)\\n\",\n        \"\\n\",\n        \"    # -- Step 3: Approval — terminal step -----------------------------------\\n\",\n        \"\\n\",\n        \"    @step\\n\",\n        \"    async def handle_approval(\\n\",\n        \"        self, ctx: Context[RefinementState], ev: ApprovedEvent\\n\",\n        \"    ) -> StopEvent:\\n\",\n        \"        state = await ctx.store.get_state()\\n\",\n        \"        print(f\\\"  Approved after {state.iteration} iteration(s) with score {ev.score}.\\\")\\n\",\n        \"        return StopEvent(\\n\",\n        \"            result={\\n\",\n        \"                \\\"status\\\": \\\"approved\\\",\\n\",\n        \"                \\\"prompt\\\": ev.prompt,\\n\",\n        \"                \\\"score\\\": ev.score,\\n\",\n        \"                \\\"iterations\\\": state.iteration,\\n\",\n        \"            }\\n\",\n        \"        )\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## 5. Run: Auto-Approve Path\\n\",\n        \"\\n\",\n        \"With `max_retries=5` and `threshold=0.8`, the mock evaluator will approve\\n\",\n        \"once the prompt accumulates enough quality keywords (4 out of 5 criteria = 0.8).\\n\",\n        \"Since the generator adds at most 2 improvements per iteration, this takes ~3 iterations.\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 6,\n      \"metadata\": {},\n      \"outputs\": [\n        {\n          \"name\": \"stdout\",\n          \"output_type\": \"stream\",\n          \"text\": [\n            \"=== Auto-Approve Run ===\\n\",\n            \"  [iteration 1] Generated prompt: 'Answer the question.'\\n\",\n            \"  [iteration 1] Score: 0.0 (threshold: 0.8)\\n\",\n            \"  [iteration 1] Retrying with feedback: Missing qualities: specific, structured, role, examples, constraints. Try adding language that is specific, structured, role, examples, constraints.\\n\",\n            \"  [iteration 2] Generated prompt: 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer.'\\n\",\n            \"  [iteration 2] Score: 0.4 (threshold: 0.8)\\n\",\n            \"  [iteration 2] Retrying with feedback: Missing qualities: role, examples, constraints. Try adding language that is role, examples, constraints.\\n\",\n            \"  [iteration 3] Generated prompt: 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4).'\\n\",\n            \"  [iteration 3] Score: 0.8 (threshold: 0.8)\\n\",\n            \"  Approved after 3 iteration(s) with score 0.8.\\n\",\n            \"\\n\",\n            \"Result: {'status': 'approved', 'prompt': 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4).', 'score': 0.8, 'iterations': 3}\\n\"\n          ]\n        }\n      ],\n      \"source\": [\n        \"evaluator = MockEvaluator(dataset=DATASET)\\n\",\n        \"wf = PromptRefinementWorkflow(\\n\",\n        \"    evaluator=evaluator, max_retries=5, threshold=0.8, timeout=30\\n\",\n        \")\\n\",\n        \"\\n\",\n        \"print(\\\"=== Auto-Approve Run ===\\\")\\n\",\n        \"result = await wf.run()\\n\",\n        \"print(f\\\"\\\\nResult: {result}\\\")\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## 6. Run: HITL Escalation Path\\n\",\n        \"\\n\",\n        \"Here we set `max_retries=2` so the workflow can only loop twice before\\n\",\n        \"escalating. With only 2 iterations adding 2 improvements each, the prompt\\n\",\n        \"reaches at most 4/5 criteria (score 0.8) which is below `threshold=1.0`.\\n\",\n        \"This forces escalation to a human.\\n\",\n        \"\\n\",\n        \"We consume events from the handler, simulate a human providing targeted\\n\",\n        \"feedback, and watch the loop resume.\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 7,\n      \"metadata\": {},\n      \"outputs\": [\n        {\n          \"name\": \"stdout\",\n          \"output_type\": \"stream\",\n          \"text\": [\n            \"=== HITL Escalation Run ===\\n\",\n            \"  [iteration 1] Generated prompt: 'Answer the question.'\\n\",\n            \"  [iteration 1] Score: 0.0 (threshold: 1.0)\\n\",\n            \"  [iteration 1] Retrying with feedback: Missing qualities: specific, structured, role, examples, constraints. Try adding language that is specific, structured, role, examples, constraints.\\n\",\n            \"  [iteration 2] Generated prompt: 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer.'\\n\",\n            \"  [iteration 2] Score: 0.4 (threshold: 1.0)\\n\",\n            \"  [iteration 2] Max retries reached — escalating to human.\\n\",\n            \"\\n\",\n            \"  HITL requested! Current prompt: 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer.'\\n\",\n            \"  History (2 attempts):\\n\",\n            \"    score=0.0 | Answer the question.\\n\",\n            \"    score=0.4 | Answer the question. Be precise and concise in your answer. First, understand th\\n\",\n            \"\\n\",\n            \"  Sending human feedback: 'specific, structured, role, examples, constraints'\\n\",\n            \"  Human feedback received: 'specific, structured, role, examples, constraints'\\n\",\n            \"  [iteration 1] Generated prompt: 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4).'\\n\",\n            \"  [iteration 1] Score: 0.8 (threshold: 1.0)\\n\",\n            \"  [iteration 1] Retrying with feedback: Missing qualities: constraints. Try adding language that is constraints.\\n\",\n            \"  [iteration 2] Generated prompt: 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4). You must answer accurately. Do not guess. Always verify your reasoning.'\\n\",\n            \"  [iteration 2] Score: 1.0 (threshold: 1.0)\\n\",\n            \"  Approved after 2 iteration(s) with score 1.0.\\n\",\n            \"\\n\",\n            \"Result: {'status': 'approved', 'prompt': 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4). You must answer accurately. Do not guess. Always verify your reasoning.', 'score': 1.0, 'iterations': 2}\\n\"\n          ]\n        }\n      ],\n      \"source\": [\n        \"evaluator = MockEvaluator(dataset=DATASET)\\n\",\n        \"wf = PromptRefinementWorkflow(\\n\",\n        \"    evaluator=evaluator, max_retries=2, threshold=1.0, timeout=30\\n\",\n        \")\\n\",\n        \"\\n\",\n        \"print(\\\"=== HITL Escalation Run ===\\\")\\n\",\n        \"handler = wf.run()\\n\",\n        \"\\n\",\n        \"async for ev in handler.stream_events():\\n\",\n        \"    if isinstance(ev, HumanFeedbackRequired):\\n\",\n        \"        print(f\\\"\\\\n  HITL requested! Current prompt: {ev.prompt!r}\\\")\\n\",\n        \"        print(f\\\"  History ({len(ev.history)} attempts):\\\")\\n\",\n        \"        for entry in ev.history:\\n\",\n        \"            print(f\\\"    score={entry['score']} | {entry['prompt'][:80]}\\\")\\n\",\n        \"\\n\",\n        \"        # Simulate a human providing targeted feedback\\n\",\n        \"        human_feedback = \\\"specific, structured, role, examples, constraints\\\"\\n\",\n        \"        print(f\\\"\\\\n  Sending human feedback: {human_feedback!r}\\\")\\n\",\n        \"        handler.ctx.send_event(HumanFeedbackResponse(response=human_feedback))\\n\",\n        \"\\n\",\n        \"result = await handler\\n\",\n        \"print(f\\\"\\\\nResult: {result}\\\")\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## 7. Inspect State\\n\",\n        \"\\n\",\n        \"After a run, we can read the workflow state to see the full refinement history\\n\",\n        \"— including the attempts before and after human intervention.\"\n      ]\n    },\n    {\n      \"cell_type\": \"code\",\n      \"execution_count\": 8,\n      \"metadata\": {},\n      \"outputs\": [\n        {\n          \"name\": \"stdout\",\n          \"output_type\": \"stream\",\n          \"text\": [\n            \"Total iterations: 2\\n\",\n            \"Final prompt: 'Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4). You must answer accurately. Do not guess. Always verify your reasoning.'\\n\",\n            \"\\n\",\n            \"Full history (4 entries):\\n\",\n            \"  1. score=0.00 | Answer the question.\\n\",\n            \"  2. score=0.40 | Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer.\\n\",\n            \"  3. score=0.80 | Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4).\\n\",\n            \"  4. score=1.00 | Answer the question. Be precise and concise in your answer. First, understand the question. Then, provide a step-by-step answer. You are a helpful and knowledgeable assistant. For instance, if asked about math, show your working (e.g. 2+2=4). You must answer accurately. Do not guess. Always verify your reasoning.\\n\"\n          ]\n        }\n      ],\n      \"source\": [\n        \"state = await handler.ctx.store.get_state()\\n\",\n        \"\\n\",\n        \"print(f\\\"Total iterations: {state.iteration}\\\")\\n\",\n        \"print(f\\\"Final prompt: {state.current_prompt!r}\\\")\\n\",\n        \"print(f\\\"\\\\nFull history ({len(state.history)} entries):\\\")\\n\",\n        \"for i, entry in enumerate(state.history, 1):\\n\",\n        \"    print(f\\\"  {i}. score={entry['score']:.2f} | {entry['prompt']}\\\")\"\n      ]\n    },\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {},\n      \"source\": [\n        \"## Summary\\n\",\n        \"\\n\",\n        \"This example demonstrated four workflow patterns working together:\\n\",\n        \"\\n\",\n        \"| Pattern | How it was used |\\n\",\n        \"|---------|----------------|\\n\",\n        \"| **Looping** | `evaluate_and_decide` returns `GeneratePromptEvent` to re-enter `generate` |\\n\",\n        \"| **Branching** | `evaluate_and_decide` chooses between `ApprovedEvent`, `GeneratePromptEvent`, or `HumanFeedbackRequired` |\\n\",\n        \"| **State management** | `RefinementState` tracks iteration count, prompt history, and current prompt via `ctx.store` |\\n\",\n        \"| **Human-in-the-loop** | At max retries, `evaluate_and_decide` emits `HumanFeedbackRequired`; the caller responds with `HumanFeedbackResponse`, which re-enters `generate` directly |\\n\",\n        \"\\n\",\n        \"### Next steps\\n\",\n        \"\\n\",\n        \"- Replace `MockEvaluator` with a real LLM-based judge (e.g., OpenAI)\\n\",\n        \"- Replace `generate_prompt()` with an LLM call that takes feedback as input\\n\",\n        \"- Add parallel evaluation of multiple prompt candidates using `ctx.send_event()` + `ctx.collect_events()`\\n\",\n        \"- Persist state across restarts using `ctx.to_dict()` / `Context.from_dict()`\\n\",\n        \"\\n\",\n        \"See also:\\n\",\n        \"- [Feature Walkthrough](feature_walkthrough.ipynb) for individual pattern walkthroughs\\n\",\n        \"- [Durable Workflows](durable_workflows.ipynb) for state persistence\\n\",\n        \"- [Document Processing](document_processing.ipynb) for a real-world HITL example with LlamaCloud\"\n      ]\n    }\n  ],\n  \"metadata\": {\n    \"kernelspec\": {\n      \"display_name\": \".venv\",\n      \"language\": \"python\",\n      \"name\": \"python3\"\n    },\n    \"language_info\": {\n      \"codemirror_mode\": {\n        \"name\": \"ipython\",\n        \"version\": 3\n      },\n      \"file_extension\": \".py\",\n      \"mimetype\": \"text/x-python\",\n      \"name\": \"python\",\n      \"nbconvert_exporter\": \"python\",\n      \"pygments_lexer\": \"ipython3\",\n      \"version\": \"3.10.19\"\n    }\n  },\n  \"nbformat\": 4,\n  \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "examples/feature_walkthrough.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Workflows Feature Walkthrough\\n\",\n    \"\\n\",\n    \"This notebook provides a walkthrough of the key features of Workflows.\\n\",\n    \"\\n\",\n    \"Through this notebook, you'll learn how to:\\n\",\n    \"- Create a simple workflow\\n\",\n    \"- Add branches and loops\\n\",\n    \"- Add state to your workflow between steps and across runs\\n\",\n    \"- Adding human-in-the-loop to your workflow\\n\",\n    \"- Injecting dynamic resources into your workflow\\n\",\n    \"- Integrating an observability layer to your workflow\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install llama-index-workflows\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 1. Create a simple workflow\\n\",\n    \"\\n\",\n    \"At their core, workflows are just Python classes that contain a set of steps. Each step handles a specific event type, and emits a specific event type.\\n\",\n    \"\\n\",\n    \"Let's start by creating a simple workflow that just converts a string to uppercase.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"HELLO\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class UppercaseWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def to_upper(self, ev: StartEvent) -> StopEvent:\\n\",\n    \"        input_text = ev.get(\\\"input_text\\\")\\n\",\n    \"        return StopEvent(result=input_text.upper())\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = UppercaseWorkflow(timeout=10)\\n\",\n    \"result = await w.run(input_text=\\\"hello\\\")\\n\",\n    \"print(result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"It works! There's a few things to note:\\n\",\n    \"- The `@step` decorator is used to define a class method as a step in the workflow.\\n\",\n    \"- The type annotations on the workflow steps are used to validate the workflow before it runs (i.e. all produced steps have consumers).\\n\",\n    \"-  The `StartEvent` and `StopEvent` are the first and last events in the workflow. They are special pre-defined events that are used to start and stop the workflow.\\n\",\n    \"\\n\",\n    \"Using events, we can actually add more steps and define what is passed to each step. We can even subclass the `StartEvent` and `StopEvent` to add more type safety to our workflow.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from workflows.events import Event, StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InputEvent(StartEvent):\\n\",\n    \"    input_text: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class OutputEvent(StopEvent):\\n\",\n    \"    output_text: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InnerEvent(Event):\\n\",\n    \"    text: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class UppercaseWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def validate_input(self, ev: InputEvent) -> InnerEvent:\\n\",\n    \"        if len(ev.input_text) < 1:\\n\",\n    \"            raise ValueError(\\\"Input text must be at least 1 character long\\\")\\n\",\n    \"\\n\",\n    \"        return InnerEvent(text=ev.input_text)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def to_upper(self, ev: InnerEvent) -> OutputEvent:\\n\",\n    \"        return OutputEvent(output_text=ev.text.upper())\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"HELLO\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"w = UppercaseWorkflow(timeout=10)\\n\",\n    \"result = await w.run(input_text=\\\"hello\\\")\\n\",\n    \"print(result.output_text)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Notice that we the type-safety of events, we cannot pass in a number to the workflow for example.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"ename\": \"WorkflowRuntimeError\",\n     \"evalue\": \"Failed creating a start event of type 'InputEvent' with the keyword arguments: {'input_text': 1234}\",\n     \"output_type\": \"error\",\n     \"traceback\": [\n      \"\\u001b[31m---------------------------------------------------------------------------\\u001b[39m\",\n      \"\\u001b[31mValidationError\\u001b[39m                           Traceback (most recent call last)\",\n      \"\\u001b[36mFile \\u001b[39m\\u001b[32m~/giant_change/workflows/src/workflows/workflow.py:334\\u001b[39m, in \\u001b[36mWorkflow._get_start_event_instance\\u001b[39m\\u001b[34m(self, start_event, **kwargs)\\u001b[39m\\n\\u001b[32m    333\\u001b[39m \\u001b[38;5;28;01mtry\\u001b[39;00m:\\n\\u001b[32m--> \\u001b[39m\\u001b[32m334\\u001b[39m     \\u001b[38;5;28;01mreturn\\u001b[39;00m \\u001b[38;5;28;43mself\\u001b[39;49m\\u001b[43m.\\u001b[49m\\u001b[43m_start_event_class\\u001b[49m\\u001b[43m(\\u001b[49m\\u001b[43m*\\u001b[49m\\u001b[43m*\\u001b[49m\\u001b[43mkwargs\\u001b[49m\\u001b[43m)\\u001b[49m\\n\\u001b[32m    335\\u001b[39m \\u001b[38;5;28;01mexcept\\u001b[39;00m ValidationError \\u001b[38;5;28;01mas\\u001b[39;00m e:\\n\",\n      \"\\u001b[36mFile \\u001b[39m\\u001b[32m~/giant_change/workflows/src/workflows/events.py:81\\u001b[39m, in \\u001b[36mEvent.__init__\\u001b[39m\\u001b[34m(self, **params)\\u001b[39m\\n\\u001b[32m     80\\u001b[39m         data[k] = v\\n\\u001b[32m---> \\u001b[39m\\u001b[32m81\\u001b[39m \\u001b[38;5;28;43msuper\\u001b[39;49m\\u001b[43m(\\u001b[49m\\u001b[43m)\\u001b[49m\\u001b[43m.\\u001b[49m\\u001b[34;43m__init__\\u001b[39;49m\\u001b[43m(\\u001b[49m\\u001b[43m*\\u001b[49m\\u001b[43m*\\u001b[49m\\u001b[43mfields\\u001b[49m\\u001b[43m)\\u001b[49m\\n\\u001b[32m     82\\u001b[39m \\u001b[38;5;28;01mfor\\u001b[39;00m private_attr, value \\u001b[38;5;129;01min\\u001b[39;00m private_attrs.items():\\n\",\n      \"\\u001b[36mFile \\u001b[39m\\u001b[32m~/giant_change/workflows/.venv/lib/python3.12/site-packages/pydantic/main.py:253\\u001b[39m, in \\u001b[36mBaseModel.__init__\\u001b[39m\\u001b[34m(self, **data)\\u001b[39m\\n\\u001b[32m    252\\u001b[39m __tracebackhide__ = \\u001b[38;5;28;01mTrue\\u001b[39;00m\\n\\u001b[32m--> \\u001b[39m\\u001b[32m253\\u001b[39m validated_self = \\u001b[38;5;28;43mself\\u001b[39;49m\\u001b[43m.\\u001b[49m\\u001b[43m__pydantic_validator__\\u001b[49m\\u001b[43m.\\u001b[49m\\u001b[43mvalidate_python\\u001b[49m\\u001b[43m(\\u001b[49m\\u001b[43mdata\\u001b[49m\\u001b[43m,\\u001b[49m\\u001b[43m \\u001b[49m\\u001b[43mself_instance\\u001b[49m\\u001b[43m=\\u001b[49m\\u001b[38;5;28;43mself\\u001b[39;49m\\u001b[43m)\\u001b[49m\\n\\u001b[32m    254\\u001b[39m \\u001b[38;5;28;01mif\\u001b[39;00m \\u001b[38;5;28mself\\u001b[39m \\u001b[38;5;129;01mis\\u001b[39;00m \\u001b[38;5;129;01mnot\\u001b[39;00m validated_self:\\n\",\n      \"\\u001b[31mValidationError\\u001b[39m: 1 validation error for InputEvent\\ninput_text\\n  Input should be a valid string [type=string_type, input_value=1234, input_type=int]\\n    For further information visit https://errors.pydantic.dev/2.11/v/string_type\",\n      \"\\nDuring handling of the above exception, another exception occurred:\\n\",\n      \"\\u001b[31mWorkflowRuntimeError\\u001b[39m                      Traceback (most recent call last)\",\n      \"\\u001b[36mCell\\u001b[39m\\u001b[36m \\u001b[39m\\u001b[32mIn[8]\\u001b[39m\\u001b[32m, line 1\\u001b[39m\\n\\u001b[32m----> \\u001b[39m\\u001b[32m1\\u001b[39m result = \\u001b[38;5;28;01mawait\\u001b[39;00m w.run(input_text=\\u001b[32m1234\\u001b[39m)\\n\",\n      \"\\u001b[36mFile \\u001b[39m\\u001b[32m~/giant_change/workflows/src/workflows/workflow.py:371\\u001b[39m, in \\u001b[36mWorkflow.run.<locals>._run_workflow\\u001b[39m\\u001b[34m()\\u001b[39m\\n\\u001b[32m    368\\u001b[39m \\u001b[38;5;28;01mtry\\u001b[39;00m:\\n\\u001b[32m    369\\u001b[39m     \\u001b[38;5;28;01mif\\u001b[39;00m \\u001b[38;5;129;01mnot\\u001b[39;00m ctx.is_running:\\n\\u001b[32m    370\\u001b[39m         \\u001b[38;5;66;03m# Send the first event\\u001b[39;00m\\n\\u001b[32m--> \\u001b[39m\\u001b[32m371\\u001b[39m         start_event_instance = \\u001b[38;5;28;43mself\\u001b[39;49m\\u001b[43m.\\u001b[49m\\u001b[43m_get_start_event_instance\\u001b[49m\\u001b[43m(\\u001b[49m\\n\\u001b[32m    372\\u001b[39m \\u001b[43m            \\u001b[49m\\u001b[43mstart_event\\u001b[49m\\u001b[43m,\\u001b[49m\\u001b[43m \\u001b[49m\\u001b[43m*\\u001b[49m\\u001b[43m*\\u001b[49m\\u001b[43mkwargs\\u001b[49m\\n\\u001b[32m    373\\u001b[39m \\u001b[43m        \\u001b[49m\\u001b[43m)\\u001b[49m\\n\\u001b[32m    374\\u001b[39m         ctx.send_event(start_event_instance)\\n\\u001b[32m    376\\u001b[39m         \\u001b[38;5;66;03m# the context is now running\\u001b[39;00m\\n\",\n      \"\\u001b[36mFile \\u001b[39m\\u001b[32m~/giant_change/workflows/src/workflows/workflow.py:339\\u001b[39m, in \\u001b[36mWorkflow._get_start_event_instance\\u001b[39m\\u001b[34m(self, start_event, **kwargs)\\u001b[39m\\n\\u001b[32m    337\\u001b[39m msg = \\u001b[33mf\\u001b[39m\\u001b[33m\\\"\\u001b[39m\\u001b[33mFailed creating a start event of type \\u001b[39m\\u001b[33m'\\u001b[39m\\u001b[38;5;132;01m{\\u001b[39;00mev_name\\u001b[38;5;132;01m}\\u001b[39;00m\\u001b[33m'\\u001b[39m\\u001b[33m with the keyword arguments: \\u001b[39m\\u001b[38;5;132;01m{\\u001b[39;00mkwargs\\u001b[38;5;132;01m}\\u001b[39;00m\\u001b[33m\\\"\\u001b[39m\\n\\u001b[32m    338\\u001b[39m logger.debug(e)\\n\\u001b[32m--> \\u001b[39m\\u001b[32m339\\u001b[39m \\u001b[38;5;28;01mraise\\u001b[39;00m WorkflowRuntimeError(msg)\\n\",\n      \"\\u001b[31mWorkflowRuntimeError\\u001b[39m: Failed creating a start event of type 'InputEvent' with the keyword arguments: {'input_text': 1234}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run(input_text=1234)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 2. Add branches and loops\\n\",\n    \"\\n\",\n    \"In this section, we'll explore several approaches to adding branches and loops to our workflows.\\n\",\n    \"\\n\",\n    \"This example might seem a little contrived, but it's a good way to show how you can use branches and loops to create more complex workflows!\\n\",\n    \"\\n\",\n    \"Remember that steps in a workflow are triggered by specific input events. So, as long as a step emits an event that is consumed by another step, we can add branches and loops to our workflows.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 12,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import Event, StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InputEvent(StartEvent):\\n\",\n    \"    input: str | int\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class OutputEvent(StopEvent):\\n\",\n    \"    output: str | int\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class AddTextEvent(Event):\\n\",\n    \"    text: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class AddNumberEvent(Event):\\n\",\n    \"    number: int\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class Add10Workflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def route(\\n\",\n    \"        self, ev: InputEvent\\n\",\n    \"    ) -> AddTextEvent | AddNumberEvent | OutputEvent:\\n\",\n    \"        if isinstance(ev.input, str):\\n\",\n    \"            if \\\"!\\\" * 10 not in ev.input:\\n\",\n    \"                return AddTextEvent(text=ev.input)\\n\",\n    \"            else:\\n\",\n    \"                return OutputEvent(output=ev.input)\\n\",\n    \"        elif isinstance(ev.input, int):\\n\",\n    \"            if ev.input < 10:\\n\",\n    \"                return AddNumberEvent(number=ev.input)\\n\",\n    \"            else:\\n\",\n    \"                return OutputEvent(output=ev.input)\\n\",\n    \"        else:\\n\",\n    \"            raise ValueError(\\\"Input must be a string or int\\\")\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def add_text(self, ev: AddTextEvent) -> InputEvent:\\n\",\n    \"        return InputEvent(input=ev.text + \\\"!\\\")\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def add_number(self, ev: AddNumberEvent) -> InputEvent:\\n\",\n    \"        return InputEvent(input=ev.number + 1)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = Add10Workflow(timeout=10)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Here, we have a more complex workflow\\n\",\n    \"- It has branching logic based in the input\\n\",\n    \"- If it's a string, it loops until it has 10 \\\"!\\\"s\\n\",\n    \"- If it's a number, it adds 1 to it until it's we hit 10!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 11,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"hello!!!!!!!!!!\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run(input=\\\"hello\\\")\\n\",\n    \"print(result.output)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 13,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"10\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run(input=2)\\n\",\n    \"print(result.output)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 3. Add state to your workflow between steps and across runs\\n\",\n    \"\\n\",\n    \"Rather than passing all information within events, it can also be useful to store state that is accessible to all steps in the workflow.\\n\",\n    \"\\n\",\n    \"To do this, we can define a state class that will be used to store the state of the workflow.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from pydantic import BaseModel\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.events import Event, StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class State(BaseModel):\\n\",\n    \"    counter: int = 0\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class IncrementAgainEvent(Event):\\n\",\n    \"    pass\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class StatefulWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(self, ctx: Context[State], ev: StartEvent) -> IncrementAgainEvent:\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.counter += 1\\n\",\n    \"\\n\",\n    \"        return IncrementAgainEvent()\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def step2(self, ctx: Context[State], ev: IncrementAgainEvent) -> StopEvent:\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.counter += 1\\n\",\n    \"\\n\",\n    \"        return StopEvent(result=state.counter)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = StatefulWorkflow()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"A few new things to note:\\n\",\n    \"- We've added a `Context` param to our step function. This is a special parameter that is used to store the state of the workflow. By reading type annotations, workflows will automatically pass the current context to the step function.\\n\",\n    \"- The context is annotated with our `State` class. This is a pydantic model that will be used to store the state of the workflow. The main requirement here is that the state is a pydantic model and has defaults for all fields.\\n\",\n    \"\\n\",\n    \"Let's run the workflow a few times and see what happens:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 21,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"2\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run()\\n\",\n    \"print(result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 22,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"2\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run()\\n\",\n    \"print(result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Each `.run()` call is actually creating a new `Context` object. So by default, each run will have a new state!\\n\",\n    \"\\n\",\n    \"If we wanted our counter to persist across runs, we can explicitly pass in a `Context` object to the `.run()` method.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 25,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"2\\n\",\n      \"4\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from workflows import Context\\n\",\n    \"\\n\",\n    \"ctx = Context(w)\\n\",\n    \"\\n\",\n    \"result = await w.run(ctx=ctx)\\n\",\n    \"print(result)\\n\",\n    \"\\n\",\n    \"result = await w.run(ctx=ctx)\\n\",\n    \"print(result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Serializing and deserializing state\\n\",\n    \"\\n\",\n    \"The entire `Context` object contains both the state of the workflow, as well as various pieces of information about the current workflow run. At any point, you can serialize the context to a JSON string and deserialize it back into a `Context` object.\\n\",\n    \"\\n\",\n    \"If your state is complex, you can customize the serialization and deserialization of the state by leveraging the `@model_serializer` and `@model_validator` decorators from pydantic!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 26,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"6\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"ctx_data = ctx.to_dict()\\n\",\n    \"restored_ctx = Context.from_dict(w, ctx_data)\\n\",\n    \"\\n\",\n    \"result = await w.run(ctx=restored_ctx)\\n\",\n    \"print(result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 4. Adding human-in-the-loop to your workflow\\n\",\n    \"\\n\",\n    \"Workflows are designed for the agentic use-cases, where having human-in-the-loop to review and approve actions and decisions is a key part of the workflow.\\n\",\n    \"\\n\",\n    \"To do this, we can utilize the prebuilt `InputRequiredEvent` and `HumanResponseEvent` events to make the user an actual step in the workflow!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 29,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"What is your name?\\n\",\n      \"Workflow completed! Got result: Logan\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import (\\n\",\n    \"    HumanResponseEvent,\\n\",\n    \"    InputRequiredEvent,\\n\",\n    \"    StartEvent,\\n\",\n    \"    StopEvent,\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class HITLRequiredEvent(InputRequiredEvent):\\n\",\n    \"    prompt: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ResponseEvent(HumanResponseEvent):\\n\",\n    \"    response: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class HITLWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(self, ev: StartEvent) -> HITLRequiredEvent:\\n\",\n    \"        return HITLRequiredEvent(\\n\",\n    \"            prompt=\\\"What is your name?\\\",\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def step2(self, ev: ResponseEvent) -> StopEvent:\\n\",\n    \"        return StopEvent(result=ev.response)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = HITLWorkflow()\\n\",\n    \"\\n\",\n    \"handler = w.run()\\n\",\n    \"async for ev in handler.stream_events():\\n\",\n    \"    if isinstance(ev, HITLRequiredEvent):\\n\",\n    \"        print(ev.prompt)\\n\",\n    \"        response = input(\\\"Enter your response: \\\")\\n\",\n    \"        handler.ctx.send_event(ResponseEvent(response=response))\\n\",\n    \"\\n\",\n    \"result = await handler\\n\",\n    \"print(\\\"Workflow completed! Got result:\\\", result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Let's break down what just happened:\\n\",\n    \"- We defined custom events that inherit from the prebuilt `InputRequiredEvent` and `HumanResponseEvent` events.\\n\",\n    \"- In our workflow, one step emits an `HITLRequiredEvent` and another step waits for a `ResponseEvent`.\\n\",\n    \"- The caller accesses the `handler` object to stream events from the workflow.\\n\",\n    \"- If the caller encounters an `HITLRequiredEvent`, it runs logic to prompt the user for input and send the `ResponseEvent` back to the workflow.\\n\",\n    \"- The workflow will recieve the `ResponseEvent` and continue executing.\\n\",\n    \"\\n\",\n    \"The `handler` object is a special object that allows you to interact with the workflow. \\n\",\n    \"\\n\",\n    \"It has a `ctx` attribute that is a `Context` object, which you can use to send events to the workflow. It also has a `stream_events` method that allows you to stream events from the workflow.\\n\",\n    \"\\n\",\n    \"Calling `await handler` will wait for the workflow to complete and return the final result.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Async Human-in-the-loop\\n\",\n    \"\\n\",\n    \"Sometimes, your application design will not support waiting for a human response. In these cases, you can serialize the `Context` object to a JSON string and deserialize it back into a `Context` object to resume the workflow once a human response is received.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 30,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Workflow completed! Got result: Logan\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"w = HITLWorkflow()\\n\",\n    \"\\n\",\n    \"handler = w.run()\\n\",\n    \"req_event = None\\n\",\n    \"ctx_data = None\\n\",\n    \"async for ev in handler.stream_events():\\n\",\n    \"    if isinstance(ev, HITLRequiredEvent):\\n\",\n    \"        # break the execution of the workflow\\n\",\n    \"        # and save the context data\\n\",\n    \"        req_event = ev\\n\",\n    \"        ctx_data = handler.ctx.to_dict()\\n\",\n    \"        await handler.cancel_run()\\n\",\n    \"        break\\n\",\n    \"\\n\",\n    \"# Some time later, we can resume the workflow\\n\",\n    \"# by deserializing the context data\\n\",\n    \"response = input(\\\"Enter your response: \\\")\\n\",\n    \"restored_ctx = Context.from_dict(w, ctx_data)\\n\",\n    \"\\n\",\n    \"handler = w.run(ctx=restored_ctx)\\n\",\n    \"handler.ctx.send_event(ResponseEvent(response=response))\\n\",\n    \"\\n\",\n    \"result = await handler\\n\",\n    \"print(\\\"Workflow completed! Got result:\\\", result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 5. Dynamic resource injection\\n\",\n    \"\\n\",\n    \"Often times, you'll find yourself building a workflow that requires dynamic resources like API keys, database connections, or other external services.\\n\",\n    \"\\n\",\n    \"To best use these resources, you can inject them into the workflow by adding a `Resource` object to your workflow steps.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"# Fake a database connection\\n\",\n    \"class DBConnection:\\n\",\n    \"    pass\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_db_connection(**kwargs) -> DBConnection:\\n\",\n    \"    print(\\\"Creating a new DB connection\\\", flush=True)\\n\",\n    \"    return DBConnection()\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(\\n\",\n    \"        self,\\n\",\n    \"        ev: StartEvent,\\n\",\n    \"        db_connection: Annotated[DBConnection, Resource(get_db_connection)],\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        print(\\\"Using DB connection\\\", db_connection, flush=True)\\n\",\n    \"        return StopEvent(result=db_connection)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"By default, resources are cached, so that the same resource is used throughout the run of a workflow.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Creating a new DB connection\\n\",\n      \"Using DB connection <__main__.DBConnection object at 0x1072ea960>\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Using DB connection <__main__.DBConnection object at 0x1072ea960>\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Notice that the second run did not create a new DB connection! This is because the `db_connection` resource is cached and reused between runs.\\n\",\n    \"\\n\",\n    \"We can turn this off by setting `cache=False` on the `Resource` object.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(\\n\",\n    \"        self,\\n\",\n    \"        ev: StartEvent,\\n\",\n    \"        db_connection: Annotated[\\n\",\n    \"            DBConnection, Resource(get_db_connection, cache=False)\\n\",\n    \"        ],\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        print(\\\"Using DB connection\\\", db_connection, flush=True)\\n\",\n    \"        return StopEvent(result=db_connection)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Creating a new DB connection\\n\",\n      \"Using DB connection <__main__.DBConnection object at 0x107380380>\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Creating a new DB connection\\n\",\n      \"Using DB connection <__main__.DBConnection object at 0x106ebe660>\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"result = await w.run()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 6. Integrating an observability layer\\n\",\n    \"\\n\",\n    \"Workflows are pre-instrumented to track traces and events. This means that we can easily integrate with an observability layer like OpenTelemetry or Arize Phoenix to track the workflow execution.\\n\",\n    \"\\n\",\n    \"Furthermore, if you use workflows with `llama-index`, you can also trace events from the `llama-index` layer per workflow step.\\n\",\n    \"\\n\",\n    \"You can see all options for observability in the [docs](https://docs.llamaindex.ai/en/stable/module_guides/observability/).\\n\",\n    \"\\n\",\n    \"Below, we'll show an example of how to integrate with [Arize Phoenix](https://arize.com/docs/phoenix/tracing/integrations-tracing/llamaindex).\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install llama-index-instrumentation\\n\",\n    \"%pip install llama-index-core llama-index-llms-openai\\n\",\n    \"%pip install arize-phoenix openinference-instrumentation-llama_index\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"🌍 To view the Phoenix app in your browser, visit http://localhost:6006/\\n\",\n      \"📖 For more information on how to use Phoenix, check out https://arize.com/docs/phoenix\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"<phoenix.session.session.ThreadSession at 0x312779d00>\"\n      ]\n     },\n     \"execution_count\": 10,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"# Launch a Phoenix server\\n\",\n    \"import phoenix as px\\n\",\n    \"\\n\",\n    \"px.launch_app()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# instrument your code\\n\",\n    \"from openinference.instrumentation.llama_index import LlamaIndexInstrumentor\\n\",\n    \"from phoenix.otel import register\\n\",\n    \"\\n\",\n    \"tracer_provider = register()\\n\",\n    \"LlamaIndexInstrumentor().instrument(tracer_provider=tracer_provider)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 16,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from llama_index.core.llms import ChatMessage\\n\",\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_llm(**kwargs):\\n\",\n    \"    return OpenAI(model=\\\"gpt-4.1-mini\\\", api_key=\\\"sk-...\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(\\n\",\n    \"        self, ev: StartEvent, llm: Annotated[OpenAI, Resource(get_llm)]\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        msg = ChatMessage(role=\\\"user\\\", content=ev.get(\\\"input\\\"))\\n\",\n    \"        response = await llm.achat([msg])\\n\",\n    \"        return StopEvent(result=response.message.content)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"w = MyWorkflow()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 17,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Hello! I'm doing well, thank you. How can I assist you today?\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"response = await w.run(input=\\\"Hello, how are you?\\\")\\n\",\n    \"print(response)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"If we check the dashboard at http://localhost:6006, we can see the trace!\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"![image.png](data:image/png;base64,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Oi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8ZXhpZjpVc2VyQ29tbWVudD5TY3JlZW5zaG90PC9leGlmOlVzZXJDb21tZW50PgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MjQwMjwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4xMTQwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0LzE8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NC8xPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KOM4NtQAAQABJREFUeAHs3XecXVW5P+CVQgIJpBFKgNARQuggJXSIIKEXKUpRqlJEERAriFQF/CkXFMSLSBFpiiK9X6TX0KQmEEoInZBAQkJ+827dm73PnJlMZpJJZnjWvcNZa++123POzj9+P+/q0rNnz2mpThs+fHi2deTIkXX22kSAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECuUDXvOOTAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFonIIjVOjdHESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoBAQxCoodAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINA6AUGs1rk5igABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAoWAIFZBoUOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHWCQhitc7NUQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECgEBLEKCh0CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0TkAQq3VujiJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAhIIhVUOgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgdQKCWK1zcxQBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQKAUGsgkKHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECrRMQxGqdm6MIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQCAhiFRQ6BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJ2AIFbr3BxFgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBQkAQq6DQIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQOsEBLFa5+YoAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIFAKCWAWFDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFonIIjVOjdHESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoBAQxCoodAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINA6AUGs1rk5igABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAoWAIFZBoUOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHWCQhitc7NUQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECgEBLEKCh0CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0TkAQq3VujiJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAhIIhVUOgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgdQKCWK1zcxQBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQKge5FT4cAgXYVWG+99dKXhg8vrnnu73+fxo4dW4x1CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEOo5Ahwhide3afOGu7t2rjzFlypRmv4FPP/202f12Ni8Q30duHpa1nrXj5s/Weff269cvfeUrX0l33313evLJJxs96I477JA223zzYvt9998viFVo6BAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEOpZANcE0B977vvvumw4++OCZdmeTJ09Ow4YNm2nn+7ydaMstt0wnnnhis48dQaxPPvkkjRkzJt1yyy3pwgsvTB9//HGzx3S2nUsvvXT6y1/+krp06ZIOOuigdMUVV6RTTjmlsz2m5yFAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEPivQPOlpuYAprzy0sy6lQjGaK0XmGuuuaZ7cFTM6tmzZ1p22WWzENLNN9+cNt544+keNysmzDvvvGngwIHZX58+fWbFJeqec4eGalfl39qXvvSluvNsJECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6BwCc3xFrM7B/Pl+irnnnjudfvrp6e5//SsdedRRKaqStVe77rrr0jzzzJNdbtKkSWn99ddvl0uPGjWqcp3XXnutMjYgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDoXAJzfBDr0ksvTU899VST6ssss0w67LDDiv2xBN4xxxxTjGs7H3zwQe0m4zYIvP/+++nYY48tzrDYYoulVVddNX3hC19I0e/WrVuxb1hDCOof//hHiuUN26uVq1KV+7P6+ldffXUaOnRo2nDDDVOEsixLOKvFnZ8AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgMHsF5vggVgSn7rrrriaV3nzzzUoQa+zYsc3Ob/JEdrRKYMyYMY28IzwXrW/fvuncc89NEZbL2/zzz5++/e1vp9/85jf5pk75+emnn6YTTjihUz6bhyJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGgs0LXxJlsIzByBqJa12267pQsuuKBywj333DMNHDiwss2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQEcWmOMrYs0q3I022igNHz68OP0ZZ5yRevXqlXbaaae03nrrpUUWWSRNmDAh7b777unDDz8s5kWnR48eaa+99korrbRSWnzxxdOAAQNSVO569tln08MPP5xuu+22FJW5Wtri+E022SStvfbaaciQIalPnz7ZtceNG5f++te/Zsv5tfRccd9bbbVV+uIXv5iWXnrpNG3atPTCCy+kRx55JN1+++3pueeea+mpZtq8M888M7NedNFFs3N27do1nX766WmfffaZ7jXCeIsttsiWOxw8eHAaP3585vzAAw+kW2+9Nb311luVcyy33HLZd5NvnHvuufNu9r0df/zxxfhvf/tb9n0VG0qdhRdeOH3ta19Lcb5YYrF3797pjTfeSE8//XSKa99xxx3Zd1Q6pNKN4w8++OBi24033tioclixs4WdqCwWIbb4XuP8c801V3YP8VuL38n111+fohKXRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0P4Cn9sg1vbbb5823njjQvyJJ55I3/ve91K3bt2KbfPNN18WtHrqqaeKbQceeGD6+te/noV6io0NnZgbQaNNN900HXHEEemUU05JV155ZXlK3X6c74ADDkhdunSp7I/zRdhmlVVWyc63xx57TDfctd9++6VvfvObjc4VywFGyOuggw5K11xzTTruuOMq12qPwbHHHpvOO++84lJDhw7NAmcRYKvXunfvnk477bS0wQYbVHZHSC13PvLII7Pl//7+978XcyJEN2LEiGJc2ynv+/jjjxoFsSJw9dOf/jRtttlmjRzjO1l22WXTtttumz7++OMU3s8880ztJbJxfG/la80zzzytDmL169cvnXzyyVm4rvZi4TFo0KC0+uqrp8MPPzzF7+Sdd96pnWZMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAwiwUsTfhf4KOPProSwsrdp06dmnfTWWedlSI4FRWxmmsRqvrBD36QjjnmmCanRdDonHPOyc5XG8KqPSgCQBHqikpI9Vrcz8UXX5y+9a1vNQoP1c7fZptt0mWXXZbKlaJq58yK8aOPPppeeumlyqnLQaXyjnjOqCBVG8Iqz4l+VNaK0NRPf/KT2l2tGofj1VdfnTbffPPpOobfhRdemFXratXFWnhQPOOll15aN4RVe4oI3EUoLap4aQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAu0rIIhVxzuW84tlCT/55JPsL6bEMnPrrLNOZXZUHoplCP/4xz+me++9N3300UeV/bvsskvaZJNNKtvywamnnprWXHPNfJh9xjUjsHTtP/+ZhZbKy8z17NkzC1vF0oO17ec//3lafvnlK5tjubpbbrklW0YvqjeVWwSdjj7qqPKmduk///zzlesMa6heVa+dffbZWbWsfF98Hy+++GLmEhWoyuG4mLNdQ3WzddddN5seywbe3rAEY/6XnyP/zLfH56233pZvzj7/93//N0X1qXIbPXp0tjTkn//85/T4449Xrh0hqRNOOCFb0rJ8zMzsxxKOAwcOrJxyzJgx6eabb86WInz11Vcr+yIgFr/HuDeNAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECg/QQ+t0sT1iOeOHFi+klDdaU77rij0e7ddtutsu3aa6/NqjGVN/bq1StdccUVacEFFyw2RwWtCP2UWwRrNtpoo/KmuksGRmDqoosuKipwzTXXXOmohgDVd7/73eLYqH4UFZzKLYJMESoqt7iP+MvbiK23Tqc0hMEmT56cb5rlny+88ELlXgc0VHCqbfvuu28leBRhuEMOOaTREoIRNlpppZWKw8Nl5513Tk8//XSKJQvzdtdddxXVv+JZy/vyOfE5YMCAtMIKKxSbIvx10kknpb/+9a/FtuhE4C2uHd9FtAg8xRKFZ555Zjae2f+pDf+dccYZ6ZJLLqlcZsstt8wCYXlltQjt7b777o3mVQ4yIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQmKkCyuaUOIcPH143hNW/f//Uu3fvYmZUrool8WpbBLkOPfTQyuZFF120Mo7BcccdV1n67u6778621U6MKlB77rlnZXNe+SnfeNppp+Xd7DOWy6sNYcWOc889N0U1qbzF0oixlGF7tvL147qx5GK5zTvvvJWwWOzbf//9G4WwYntUKCtX+lpiiSXSiiuuGLta1WqXSYzKZLUhrDhxPEMsA1luQ4cOLQ9nWn/hhRcuQnhx0rfffrtuuOqGG27IgnzlC9dWWyvv0ydAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEJj5Aipi/df0mmuuabI61Lvvvpti+b9u3bplsyMg1VSLfVOmTEkRdIoWS8XVttqQzCmnnFI7pRjH+caNG1dU2YpKTBFY+vDDD7OQTjnoFdf97W9/Wxxb24kAWCyxl7cIdf3617/Oh7P887nnnqtcIyo3ldtmm21WuMX2l156KT355JPlKUU/Qm+XX3552muvvYptEaR76qmnivGMdK6//voU58zbfffdl3cbfd50003p61//erG9dunAYkcbO+WlKeNU+W+q3mmjctpSSy1V7HrwwQeLvg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCsFxDE+q/xbbfd1qz21Vdf3ez+8s4I9PTp0yfblC8Xl+/v0aNHsaxdbHv99dfTa6+9lu+u+/m73/0uffnLX872xZJ5eUBnlVVWqcy/9957mwyTxcQIQsWxsZxetFiOb05qtc9zwQUXNHt7d955ZyWIFVWxWtveeuutdNVVV7Xo8FhisdzygF5528zoRwBv0qRJKQ+s9e3bN1122WXpxz/+cXr22Wcrl4h7KofDKjsNCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEZrmAINZ/iSPg1NIWS+DF0niDBw/OAle9evUqwk1xjtrwVfm8tcvYvfzyy+Xddft///vfU/zVtjXWWKOyaYMNNkjTC4yV7y0Pi1VOMgsHCy20UOXsETIqt+WWW648TN/5znfSfvvtV9lWHpSfJbYPGjSovLtN/W233TbtvPPOaYEFFsiWUIzKZvn18s82XaCFB0elru23376YvfTSS2fLE44fPz49/vjjKfbffPPNzQbwioN1CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEZpmAINYM0K622mrp+OOPT4ssssgMHFWdOmTIkMqGsWPHVsYzMqg9VxxbXqpweueKZQ7bsy222GKVy8XyiuVWe+8RFJuRsNjMqPAVSx0edNBBdZeULN9re/VjScwImK299tqVS84333xp2LBh2d/PfvazNHr06HTeeeelG264oTLPgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoH0E/rNGXftcq0NfJSoRxRKB9UJYU6dOTRMmTEjvv/9+9tfcg/bu3buy+4MPPqiMZ2QwM4JHM3K9ts5deeWVKqd4++23K+OoOtWW1tZKVTvttFM6/PDD64awPvnkkxRVqOI7bst31prni+prUR3sjTfeqHt4PPdSSy2VTjzxxHTOOedUqrPVPcBGAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBmS6gIlYLSS+44ILUvftnXBG8uvjii9NFF12UJk6cWDnLrbfe2mQlp1dffbUyt3a5vsrO6QxqK0qNGjUqPfLII9M56rPdtUsDfrZn1vQ23HCjyonvueeeynjy5MmVEFQssxght5a2F198saVTG81bfvnl0w9+8IPK9lg28vzzz0///Oc/06efflrsi9/BvffeW4zbo3PXXXelrbfeOi244IJphx12SJtuumlacsklU21VszXXXDNdd911KZZWDE+NAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgfQQ+Sxa1z/U65FViScJ55pmnuPeoirT99tun2iBUMaGZzhNPPFHZ25Yg1tNPP53WWWed4nwRDjr99NOL8ZzUidDQAgssULmla6+9tjJ+/fXXKwG2K664IsUztkeLaljliloRaDvggAPa49IzdI1x48alc889N/uLA1daaaV01FFHpaFDhxbnmX/++dM222yTrrrqqmKbDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwKwVEMRqge9mm21WmXXllVc2G8Lq06dPZX55EFWWpk2bVoR+ohLT9Fpcf6ON/lNNKo49+eSTs2pHjz76aOXQciirsmMOGJxxxhmVu3jyyScbLeP4wgsvpLLHiBEj2i2IFWG7cvv5z39eHlb6Xbu2z4qe8Z0vtthi2bWjItell15auY8YRLBvn332Sccff3wKr7xtueWWglg5hk8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQDsItE+ipB0eZFZeYtlll62c/uOPP66My4OBAweWh3X7H3zwQbF97rnnTptsskkxrtc55phjsgpHUeUolpzr0aNHNu3hhx+uTF966aXTGmusUdlWbxCBnfYKE8X1f/zjH6fFF1+8uJUIFX3ve98rxnln5MiReTf73GWXXYpnrewoDcJv+PDhpS2t68aSf+XW3He88847l6fOsv6PfvSjdMQRR2R/Rx55ZNpggw2avNZpp51W2bfMMstUxgYECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKzVkAQqwW+sUxduUUYql6LsFFUy5pe+8Mf/lCZ8rOf/SwNGDCgsi0fxLXK+yZMmFBU45o4cWJ6/PHH86nZ5y9/+ctUGyrKJ0T46pxzzsmqJ8WygE3Ny+e39TPuO551hx12qJzqggsuSG+99VZlWwz+9re/pXimvM0111zpd7/7XZOhsTj/Nddck0455ZR09tln1503efLk/HRZqKt79/pF4F566aViXnQOPPDAyjgfbLHFFlkwKh/H56wKtcVSjeW2//77l4eVfm1Iq/bYymQDAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBmS4giNUC0r/+9a+VWYMHD05/+ctfsuUCoyJThGCOO+64dNlll6XevXtX5kZIpzaoc8kll6RyVaw4Jq4RIZ+82lX//v3TT3/yk3TsscdWznfxxRdXxlFZKipM5a1v377p6quvTtttt11xrl69emWVtCIktuaaa2ZTo3LXb3/72/ywVn+GxbBhw7K/9ddfP+26667p6KOPTnGfN9xwQ1p11VUr537zzTfTWWedVdmWD6ZMmZJOOOGEfJh9rrLKKum6667LKn3ljhEgi+X44jn79euXzVt77bXT97///cqxMRgzZkxlW9xbtDhX+bu6+eabK/MiPHbSSSellVZaKc0777xp++23T2eeeWa2rUuXLpW55fNUdrRxcN5551XOEPcS32GY5y2e46tf/Wqj38kDDzyQT/FJgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQDgL1ywO1w4U70iWielNUGBo0aFBx27H02xlnnFGMm+ssueSS6cUXX6xMOfXUU7PQUR7qiTBPBH+iRbAqDx2VD4rKXOeee255U3rnnXfS+eefn/bbb79ie1SS+ulPf5r9TZo0KfXs2bPYl3emTp2afvCDH+TDVn9G8Os3v/lNi46/9ZZb0g8blttrrt14443pa1/7Who6dGgxbf7558+ee9q0aemTTz4pAmbFhIZOOJx++unlTVn/ySefrJxrp512SjvuuGMK99tvvz3Fkn/R/v73v6fDDjssdevWLRvHfyIYF3/Ta7MqiHXXXXel2vtfYoklstBefH8RXKv33UbVtNqqa9N7BvsJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTaJqAiVgv9dttttyyMNb3pEY75+OOPK9O23nrryjgGUS3qW9/6VoqgVG2rF8K66qor0wEHHFA7NRtHZasTTzwxC+bUTqgX1Ilrfve7303PPvts7fRZMh4/fny21N/RDRWrwmd67Rvf+EYWjKqdF+GpvGJYed+4cePS7rvvnsrLEOb7r7jiihShpXLLw29dS5WtokLZ3nvvXff7KB8b/ZgbobC8RfAtgnmzokXA7rnnnmt06giM1ftuI6gW1cIijKURIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0n0CHD2JF8KTcWhL0ifm1x9WGp8rnjP7EiROz5enuueeeRkGr2B/BnMceeyx9+ctfThH+KbdYUq5ee/DBB9M222yTnnjiibrnjGPef//9dPLJJzdUyzq53imKbbG04YgRI7IKSvUCSTExbK699tq06aabprvvvrs4dkY6TZ27fI4IPkUQKDyiStXmm2+eHn744fKUZvtREez444/Pgmdjx45tFKTKD47vJKqSxXNHRax6LSqRRZip9vvO5paCWDF+5pln0s4775xGjRpVNzAWfhGIGz58eHrhhRcql1tnnXUq49qA3SeTJ1f2x2ByzW+39piYE9fcY489sud8++23Y1Pd9p97uyr7bkePHl13jo0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKzTqBLQ1Wdz0r7lK4TYZNoI0eOLG3VzQX69++fBYzmnXfedN9996Wnn34639XqzzjXBhtskJZaaqn06quvZgGt2iUNW3ryfv36pWHDhqUBAwak1157LT300ENZqKulx89p8wYPHpzWW2+9bMnGCEFFyKslobDycwwZMiQzidDcK6+8kmLpvwhzNdWWbFhScpNNNsnmxNxwnN1twQUXTBHsC4+oiPX8889nv5OoCqYRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjMPgFBrNln78oECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHQSgQ6/NGEn+R48BgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHVhAEKsDf3lunQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBOUNAEGvO+B7cBQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHVhAEKsDf3lunQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBOUNAEGvO+B7cBQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHVhAEKsDf3lunQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBOUNAEGvO+B7cBQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHVhAEKsDf3lunQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBOUNAEGvO+B7cBQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHVhAEKsDf3lunQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBOUNAEGvO+B7cBQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHVhAEKsDf3lunQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBOUNAEGvO+B7cBQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHVige79+/Trw7bt1AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIzH4BFbFm/3fgDggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6OAC3d97770O/ghunwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABArNXQEWs2evv6gQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIdAIBQaxO8CV6BAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEZq+AINbs9Xd1AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQ6gYAgVif4Ej0CAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKzV0AQa/b6uzoBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAp1AQBCrE3yJHoEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdkrIIg1e/1dnQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBTiAgiNUJvkSPQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDA7BUQxJq9/q5OgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAnEBDE6gRfokcgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGD2CghizV5/VydAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoBMICGJ1gi/RIxAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgMHsFBLFmr7+rEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQCQQEsTrBl+gRCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCYvQKCWLPX39UJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOgEAoJYneBL9AgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECMxeAUGs2evv6gQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIdAKB7tN7hlVWWWV6U+wnQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDA51pARazP9dfv4QkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQmBkC062INXLkyJlxHecgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBApxVQEavTfrUejAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB9hIQxGovadchQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKDTCghiddqv1oMRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBeAoJY7SXtOgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIdFoBQaxO+9V6MAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE2ktAEKu9pF2HAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFOKyCI1Wm/Wg9GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEB7CQhitZe06xAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0GkFBLE67VfrwQgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaC8BQaz2knYdAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQ6rYAgVqf9aj0YAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLtJSCI1V7SrkOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQKcVEMTqtF+tByNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoL0EBLHaS9p1CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDotAKCWJ32q/VgBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0l4AgVntJuw4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAp1WQBCr0361HowAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgfYSEMRqL2nXIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECg0woIYnXar9aDESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQXgKCWO0l7ToECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHRaAUGsTvvVejACBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNpLQBCrvaRdhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBTisgiNVpv1oPRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAewl0b68LtfY6iyyySFpyySUbHd6tW7e08sorZ9snTpyYnn322UZzYsNjjz2WJkyYUHff52Vjz54909ChQzPHxRZdNE2aPDm9+eab6YEHHkhjxoz5vDB4zhkQ6N+/fxoyZEh2xGuvvZZGjx49A0ebSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBD4/Al0j5DTfPPNl9599900duzYNGXKlDlKYZedd06rr7HGdO9p4403rjvn3HPPTffcc0/dfZ19Y4TVvvrVr6awiX5t23bbbdN7772XTjrppCyYVbvf+PMrMHz48DRixIgM4JVXXkk/+clPPr8YnpwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0AKB7quttlplWgSx7rvvvso2g44n0KVLl3TKKaekgQMHNnvz/fr1SyeeeGL6+c9/3qg61rrrrpt222237PgIs1122WXNnmtGd84777zZdeO4CAGeeuqpM3oK8zuhwD777JPyf5fOP//8NHLkyE74lB6JAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6GwCjZYm7N69exo2bNgcs5zflVddlR6rE8RYYIEF0tZbb118H3/84x+Lfrnz+OOPl4efm/4hhxxSCWH9+9//Tv/617/Sk08+mQYNGpRWWWWVtNlmm6W55por+zv22GPTj3/84ywQlSPFvAhqRVtiiSXyzTPtc5555inO36NHj5l2Xifq2AJRpS//3cV7rhEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEOoJA94cffjhbljAqJw0ZMiTNaYGYV199NcVfbYuQUB7E+uSTT9Idd9xRO+VzPV511VWL57/55pvTxRdfXIxjGcqnnnoqxfaohDX33HNnSxd+7WtfS6effnoxT4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZYJdH/55ZezmePHj0/Rj3BTLGvXmVosgTdgwIDskd5888300UcfpaWXXjqtueaaKZ77+uuvrzxuVImK/cstt1wWUopqUk8//XSaOnVqZV5TgwizrbzyymnZZZdNo0ePThF2i7BYS1r//v3TGmuskd1vXHNGrpufP6oIRWWzvDW1pOBbb72VbrrpprTttttmU+N+ow0ePDj7DSy88MLZOP4T97X44otn49dff73u80SY7wtf+EI2L5wfe+yxFNeobXF/UQ2rXO2oZ8+exfkjKBbfS7R87rRp0xotnZifN+5tvvnmy4avvPJK+vTTT/NdxWfXrl3TMsssk/316dMnC6K1xrY4oU6LBFr6LsW8CFdG69u3b3HuRRZZJPtdNPf9x28ngofxzo4aNSpbyjDe8Xqt/G/BG2+8kSZNmpS9K1EhLn678Xt95plnmvyt1Z4z7i+WUYz7f+CBB9Jrr71WO6X4XceO/N/b2knxDAsttFC2Oe493h+NAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6FgCn6V1Gu47gkbPPfdctqRdBAM6S9t3333T6quvnj3O7bffntZdd90sYJU/Xx7EijDFXnvtldZff/0UwZ285ZW3XnzxxXTSSSc1GciKIMn3v//9SpAkP0cEi/7f//t/Kc5Rr0XI6Xvf+16KkFDeRowYkXWfffbZ9Itf/KLJ6+bz88+ocJW3CLBMmTIlHzb6vPfee9OXv/zlbPvkyZOzgNTxxx/faF48289+9rNs+5///Od04403FnOiktr+++9fhN3yHXvuuWd27d/97nfpoYceyjenOH/5HmNHt27divNHQCqeN1p57hFHHJFVb8t2lP7zwx/+sFiG8Ve/+lUWxCntTltttVXacccds7BMvj22RYvAywknnJA++OCDfJfPmSAwo+/SpptumvbYY49GV47lM+Mv2sEHH5yFKPNJEeY7+uijUyxlWNvee++9dOKJJzYKApb/LbjooouyJTojhFXbXnrppaw6XB4IrN0f1ePinuN3m7cddtghe0cjeHn22Wdnm+Pf0fy9iQ31fp+xfffdd0+bbLJJdLPfePzWNQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBjCXRdYYUVKssRRgjm//7v/zrWU0znbssVviLsUBsCisNjToSsNtxww0oIq3zqqLgTS/f17t27vDnrr7322tkyf+VqPuVJUbEpAkMrrrhieXPW33zzzdNxxx1XCWGVJ0WlnjPOOCMLSZW3N9UfM2ZMURUqnuvQQw9tampWwefAAw9M8Xf44Yc3+exNnSCeO8IwecWx2nlRmSuun4fKavfPjHH5+y3349y77LJL2nXXXSshrPI1o+JWhL6iCpg2cwRa8y6Vg48tuYsILv7617+uG8KK4/v165dOOeWUrMpV+Xzl30cEBeuFsGL+Eksskb7zne+UD836eWBw+PDhlRBWPjH2f/GLX0w/+tGPsn9TouLW2LFj891piy22KPrlTh4UjW33339/eZc+AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0EEEukcQK/5i6bwIDET1paaqwHSQZ2rRbcYSYfGXB0CiUk4srRctqkjdcsstKapnRX+jjTZKW265ZbYvglZRoecf//hHNo7/RNjogAMOKIIZH374Ybbc4eOPP56WX375tM0222QhqwhpRKWbCD3ly+dFQCsqAeUBkVgu7aqrrkrvv/9+FgqL6l1xXFTKiio85513XnHd5jrxPcb3Gi2WOjzttNPSBRdckOKemmsTJkzIAmlxP1EpKw+IROWo/NpRLShaVD066KCDitO98847KaplxdJu8dxf/epXsyUNY8J2222Xrr322mzuz3/+8xRLxIV3uEULj1NPPTXrz6xl2SIwl1czixO/8MIL6Zprrklxn7EsZewL26haFN9/uXJRdiP+0yqB1rxLt912W1Et7pBDDilCiREKveuuu7JKU+XlBiMkFb+/aBF2uuKKK9Lzzz+fIhgYYcuolhXfbfy+jjrqqCafI97vu+++Oz344INZwDLe1XxJzgheLrroounVV18tjt9tt90qSw3GcoT33HNP+vjjj9MGG2yQhg0bls2NZT733nvv7J2LZ8urfUWosrbFvwHlAGf+ntTOMyZAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTmbIFiacIINURVoPiLZb06c4tKOREWKrdyZZzLLrssC1Ll+y+99NIskJUv4bfqqqtWglgRsogwVrQIYUWFqDw0EmGvO++8MwtCRfgowiFrrbVWUfXmsMMOy7bFsa+88kr6yU9+Et2sxT1GoOob3/hGNo5QVoSpIjQ3vRbBq6iilS91OP/882chsAitRNWzCJI1tUxiLE8ZbejQoZUgVtxLuUXlnzzIFkGUI488MnOKORFseeyxx9JZZ52VBWYi7BTBq7feeiurwhVz3n333fjIWhxfe/58X2s/wzlvEWyLJQjzFt/LE088kVUpi20qYuUybf9szbsUv8v8+4+gXP67jdBfvj2/s6iu1r9//2wYx8XvLt67aKNHj04RfIp3PH6b8ZuLQOK///3vbH/tfyL8V/63IEJVZ555ZurVq1c2NSrY5UGs+DcyXyYxdl5++eVFuDDG8V7F9SOAGC1CjPG+RqgzAlxxP3GO+Pcj3o28famhulbe4v2wTGau4ZMAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHQsga4RHqj9H/7L1Vk61uNM/24j8FMOXuRHRKjiyiuvzP5uuummfHPxGWGqvEWVnHJbb731imFUs8pDWPnGCIvcfPPN+TCrsJUPonJO3n73u9/l3eIzrpsHliLEtdJKKxX7mutMnTo1C6hExZ5yi0DUaqutlgW+InCy0047lXfPUD8CW7lZBK6iulC5RWDstddeKzZFtaL2bFEVKW95BbJ8HJ9x/w8//HD2GdWy8jBdeY7+jAu05V1qydViKc+8XX/99UUIK98WFdUiCJi3jTfeOO9WPuv9WxC/kwjp5W2xxRbLu1mlrXgHo0Xwq17lqvi3I973aPHvaIQv410cNWpUti3+U7s84RdL70VU/9IIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBjinQPUJJeTAplpMbMmRIx3ySFt51U5VxagMQUXEnqiQttNBCWcWqPIARlyn3Yzz33HPHR7a8XixnWK/deOONRUAjD3pEoCtfkjDCG/GXL49YPkcES/IKQBEMeeSRR8q7m+xHEOrss89OgwYNSjvuuGOKSkURxMpbhES23XbbbAnEqKCVV/7J90/vM+4rlvrLWwSZ4v7iuWK5tWj5Mm/Rz52i3x4twjhRiSha+P3iF7/IKoHde++9RVWxCKNpM1egLe9SS+4kr5YVc2NZwXrvzJgxY7JlCmPOAgssEB+NWvwO6rVYojVf1jPekbwtscQSeTc9+uijRb+2E+/cIosskm2OsFe06667Lh166KFZv7w8YVTIin9jokWQMf6d0AgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGOKdC9fNsRyIrQQixj11nbG2+80eSjRVgnlhWLJcVqw1ZNHRRWeZhq8uTJjapC5cdFlayo3lNuedgjtkVA6pe//GV5d91+hKpmtL3++utZICuOi2pYW265ZVpuueWKZ+zXr19WIeu73/1uo2peLbnWhhtumLbbbru6gZiWHD+r5sQyb1FRbKONNsouEb/tfffdN1vqMaqMRRAnAjL5snaz6j4+j+dtzbvUEqeoclauXBZLC06v5SHG2nl55ara7fWqp8WccqiwXOmt9viRI0em+Cu3hx56KAv/RfAq7j/ewwhzxW8z//cj3tPaanrlc+gTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECc7ZA19rba++qRbXXn13jqOB08sknp7XWWqsIKMW9xLJiebWqevcWIaa8NRXsyPfXfuZVc2q3NzcuL7fX3Lym9kX4I8Ir3/zmN7NqQvm8CIIdcsgh+bDFn7vuumsWbipXJYogS1TjmlGPFl90Biaef/756U9/+lOaMGFCcVQEXwYMGJBGjBiRoiLW3nvvXezTabtAa9+llly5Ne9MObjVkms0NadXr17Frvfee6/ot7TzxOOPF1O/9KUvZf1hw4YV2+64446ir0OAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0PIHuK664YopQQYQMYumtWIorlsj6vLUf/vCHxbJ9sZzYFVdckW677bZsucGwiApZ5513XiOWWJ4vbzMaYvvggw/yQ7PA19VXX12Mm+o89dRTTe0qtkcFoPxexo0bl5272PnfzpQpU9Lvf//7FFW8Ntlkk2zrUkstVTut2fFKK62Uttpqq2JOhLwuv/zyVK4WdOSRR6ahQ4cWc2ZFJ0JVzbX4HuMvlpqMZ40lGsvBsU033TSrBHf66ac3dxr7WijQ2nepJad///33K9OuuuqqyrjeoLkqePXmN7WtXDlter+5euf4+z/+kVZfY41sV748Yb7cYYQXb7311nqH2UaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0EIHueSCgg9zvLLnNqJC00EILZeeOENoPfvCDLJzWkotFmCpCFF27dk09evTIAltRRasl7dlnny2mTZw4Mf2jIagxM9qxxx6b+vbtm53qkksuSTfddFOTp73hhhuycFJMmNFqW+VqPo83VPv59a9/3eg6TS0L12hiCzY0VdkoX9pteqcYM2ZMuvDCC7Np8axRFSxCWdEiLBbfYVPL0mWT/Ge6Am15l6Z78oYJsdxk/r7F/FjuM6qvtUeLpQNjSc9oiy222AxfcvTo0dkymBF2jd/yXnvtVVTfi99mhCM1AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoOMKNFqaMCokPfbYYx33iVpx51EJKg/zxPPXW3Ysr1xT7/QRoooW58iXHKudt/nmm6dYJi/+IugV7bnnniumxXJuUWGqqVZeArGpOfn2UaNG5d1sqcViUKdTDuK1NECWn2bJJZfMu+mZZ54p+uVOa5aSKx9fXlKwfL3ynKb6Z5xxRlbFLCqZlZ8z5n/00UfpV7/6VYrqZ9Hiu1t55ZWzvv+0XqCt71JLrlz+TXzta19r8pAIPEW4bma1F198sTjVaqutVvRrO//zP/9TvOt9+vSp7H7ggQeK8WabbVb0b7755qKvQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHRMga5REenBBx9Md999d7rmmmvStddem6Jyy+epRaWbvPXs2bOoepNvW3jhhdNRRx2VD4vQVr6hvKTYDjvskOaff/58V/YZ1Ze23XbbYtvtt9+e9aMCTlTCydtBBx2U6gWuYvm/CA19//vfLyro5MfU+4zvM28RQCpfO98enxES2WmnnYpN5WUWY2M58FLvvsrz11133eI8eWfvvffOu9lnLO9YbhGGylu412tvv/12sTmWEKxtBxxwQO2mYhz3F9eMv5133rnYnneiKtFcc82VD1Ms46i1TaCt71JcPQ/HRT+vVBf9vN111115N2244YYplletbVG5Kt6ZCOPVvo+1c1s6/r//+780adKkbHos/VkvBBa/0d69e2dzYhnF8vKjsbFe1bsIQP7rX//KjvEfAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoOMKdI8wzCuvvNJxn2Am3HkYRGgiX87vmGOOSSNHjkwR5Fl22WXT4osvXglA1QaK/va3v6Utt9wyRZgo/k488cQUlW+ef/75FCGuTTbZJEVwI1oso3bPPfcUd/2b3/wm/eIXv8jCXVHB55e//GUWynjhhRfSggsumC2dF9ePtsIKK6Tll18+PfXUU8Xx9ToR6ohrxr1Hi7DV8OHDs8BdPFPcY1SXigpc5eX+4jnK7YknniiGgwYNSrvvvnsWLPn3v/+dojpQhPfypf1iqbZ4jliiMKp7RQAs98xPUjv+8MMPs+XY4h7C9PDDD8+ebfz48enee+/NDnvkkUeKalZDhgxJRx99dIpt8WzxN2DAgPz0jT4j8JZXworPuL8IzYVBmEb1svy7jO+lHCJqdLLP8Yb4bg899NBmBWK5wHPOOSerNNaWdykuEr+t+K1H22ijjdK7776bvT+xDGG8q5dddlkWwMorXh155JHp0UcfTU8++WQWrIvfZLwnUQ0rfnMjRowolqTMTtrK/8Qz/vOf/yzCi/FORQWwuHZU0lt11VUrobAIbtW2eJZYXnHgwIHFrnjXY0lUjQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOjYAt079u3PvLv/05/+lA455JAsvBEBjuaWHotl7GLJvddeey27gQhRnH766VnVrKiwFEGnDTbYIPsr32FU0zn++OPLm7JQxoUXXpj23HPP7NoRStp4442zv8rEhkFU05leCCs/5qSTTkrHHXdcFjiKbVH9qrwUWj4v/7zuuuvS/fffnw+zz3i+qNoV9xTPHGGzaFE5LcIyETaLkEseFFtggQWavcbgwYOz48v/GTt2bIqgT7Qwj7+XXnqpCGJF+CYqguVLvEUYK/5a0iLwFteM46PF/e22226NDo3v78wzz2y03YbPBNZcc83PBk304h2KcF1b3qU49UMPPZT9rqIf79Kuu+4a3ew3kVdRi/ctKsRFwDF+m6uvvnr2l00s/SdCTvF+zawW72CEr+J60ZZZZpnsr/b8UbXryiuvrN2cje+8884izBUbbrzxxrrzbCRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQ6lkDXjnW7n91tBITyFpVqmmux9Ffempr78MMPpzCRxAsAAEAASURBVFNOOSVFNabaFhV+IthUXmZsrbXWqkx77rnn0hFHHJGFiMrXi0lxr6NGjUpRuScPb5UPvu2227JQSVRkqnd/7733Xjr11FPTVVddVT6s2X6Ei4499thsebY33nij7tyY8/LLL6ef/exnWZWhepOiilTt80TwJVrca1wj7MrfR74vKmb94Q9/yObGf+otMxfmZdeYl58/+tHiGhHOqm35km5hm7fae43qSX/84x+zqkr1bMP85JNPzip55efwmdLU0vvVUo/cvq3vUoT86v3Wy7+L0aNHp8MOOyw99thjjX6fcb8Revzzn/+cTjjhhMrt5/cYG+v9Hmq3l+fnJ4oqdpdccklWnSvfln/GbzmuW/7d5/vyz1gOttwieKYRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHV+gS0PFmbprYsWyW9Fiib7PW4sKUCuuuGK2bF0sw5dX4ZkRh6i+tPTSS2dBpxld8i6qbUWVqXfeeSdFRZ96YZAZuZd8bpw3rz4V4aVYoq8lLQIwyy23XLacWgTVnn766UbBqzhPLKUYz/zqq6+mMWPGtOTUxZxY+jAqDcUSb2EelZVq2zzzzJMtNRhVkiL4Fsu8zWhbsmFJxrjWuHHjsnBXbYBsRs9nfvMCbXmXorpcvIexBGFUTot3oanWr1+/bKnK+D6feeaZVr2zTZ27ue3588U7EksjtuT3FNW0vv3tb2enjaU8zzjjjOYuYR8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQQQQEsTrIF+U2CRDoHAInnnhitrRpPE1UuovgoUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0fIEOuzRhx6f3BAQIfN4ENt100yKEFZX2hLA+b78Az0uAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECnVmge2d+OM9GgACB2S0wePDgdOCBB6Y+ffpkf/n9/P3vf8+7PgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFOICCI1Qm+RI9AgMCcK7DsssumxRZbrHKDo0aNStdff31lmwEBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQsQUEsTr29+fuCRCYwwXef//9NHXq1NS1a9c0ceLE9MADD6QLLrhgDr9rt0eAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjMqECXnj17Tqt30PDhw7PNI0eOrLfbNgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBD4r0BXEgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQNgFBrLb5OZoAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJJEMuPgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAm0UEMRqI6DDCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgIIjlN0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIE2CghitRHQ4QQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBDE8hsgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAGwUEsdoI6HACBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgIYvkNECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoI0CglhtBHQ4AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEBLH8BggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBGAUGsNgI6nAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAoJYfgMECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBoo4AgVhsBHU6AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAFBLL8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQItFGgexuPdzgBAgQKgSXWGJziL9riqw9OLz8yptj30sNjUvxpBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHOKNCpg1h5KCQCIXkrB0PuPO/ufLNPAgRaKRDv2Yb7DSsCWOXT5KGs2Lbhfv/Z839/+M975/0rS+kTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHV2g0wWxmguFxJdVDYYMSyeud9ps/Q4HLjl/mn+J/qnvwn3S1E8+Te+P/SC98fybafy48TN0X126dinmT5s2LaWG/29pKx8bx0z7dAYObulFzOt0AtN715p64Aht5W12hrG6du+a5l98QHrzxbfy25kln93m6pYWXn7BNGiFhdOUSVPSm6Pe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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQH4KEMTKz8+Fs0IAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIECEiCIVUAfFqeKAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAAC+SlAECs/PxfOCgEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBApIgCBWAX1YnCoCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjkpwBBrPz8XDgrBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKCABglgF9GFxqggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJCfAgSx8vNz4awQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECggAQIYhXQh8WpIoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQH4KVCvttHr16lXaJqxHAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBCq1ABWxKvXHz8UjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBANgRKrYg1derUbByHPhBAAAEEEEAAAQQQQAABBBCosEDTpk3T9rF8+fK061mJAAIIIIAAAggggAACCCCAAAIIIIAAAggggECuBKiIlStZ+kUAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFKI0AQq9J81FwoAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5EqAIFauZOkXAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEKo0AQaxK81FzoQgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJArAYJYuZKlXwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEKg0AgSxKs1HzYUigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBArgQIYuVKln4RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECg0ggQxKo0HzUXigACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAArkSqNahQwfbsWNH2H+DBg2sTp064TwTCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAAC6QWq9e3bt8QWCmatX7++xHIWIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIlBRgaMKSJixBAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBMokQBCrTFxsjAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgiUFKj2ySefJC3t0aNH0jwzCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAAC6QWqzZ49O2mLNm3aWKNGjZKWMYMAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJBagKEJU9uwBgEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDISIAgVkZMbIQAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIpBaolnoVaxBAAAEEEEAAAQQQQACB7Ao0aNDAOnXqZM2aNbO6detmt/Mc97Z+/XpbtmyZzZs3z9asWZPToxWyk2B2p1VOPwg6RwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECgDAIEscqAxaYIIIAAAggggAACCCBQfoEePXpYly5dyt/BHt5TwTG9OnbsaHPnzrXp06fn5IwK3Ukou8sqJx8AnSKAAAIIIIAAAggggAACCCCAAAIIIIAAAgggUE4BgljlhGM3BBBAAAEEEEAAAQQQyFygf//+1rp162CHRYsWBZWlVDWpkJrCRark1bZt2yBQVqdOHZs8eXJWL6EYnASyO6yyCk9nCCCAAAIIIIAAAggggAACCCCAAAIIIIAA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Xno58aDYKeFh04UU+/TpE27nT9SoUcP2228/mzFjhr84mD7ssMOSlin4la5pWD+FwQ466CDTkIZ16tQJwlvTpk2zt99+2959913bvn17yi7atGljF110Ubh+3LhxQQWuk046yYYNG2aqQK6hF3/5y1/aW2+9FW6XyYTO56yzzkra9N5777Vly5aZC8FGq5/369fPfvGLX4T7TJw40Z577rlwviwTshg6dKgdfPDB1rNnT2vWrFlwbA0xqX7Hjx9v69evj+2yb9++JgPXHnzwQZs1Kz7M16BBA3vkkUeCSmZffPGFnXfeeSn77dGjh51++umuWxs7dqxNnTo1nE81ceihh9rw4cPD1X/4wx+Cz1oPLSq8p89R16K+161bZ/vss0/S7+/SjqPQoL5DQduxw2648UbbtGlTeLzo8a+//vrgeyUnnZd+Dtw5KDz4xhtv2MMPPxzun8sJfzjCuCpYZTm2q6z1wCWPEuQqCxzbIoAAAggggAACCCCAAAIIIIBA1gVyHsTScIOpnorU05O1atUKL2pH4obRihUrwnl/YubMmf4s0wgggAACCCCAAAIIIIAAAjECN998c1gZzF+t4JpCAy7Apnf9A7wLo8QFt9QXYSxf0axK1erWrNV+wcJirYalixt5xGG2z35DbdYnbyQD7Ka5VasWB1WxZC3z7du2VPjIkydPDgIobshBhU/iWqtWrcyv9qT9+vfvH2568sknxQaxDjzwwHAbTSj4lapdcsklQehHQyT6rWHDhrb//vsHoRzdH/n+979vCxYs8DcJp3W8Y445JpzfumWL3XDDDUnDI2qlwkxlCWJpyEGFrqpXrx72raCQAkQKX/nHDDf4dsJfp2EgyxPEUnDopptuSjq+uq9fv77tvffepuDRlsS1XnPNNfbaa69FT8EOOOCApHOsWbOmXXXVVSW204LRo0ebAnFq6nvUqFH2z3/+M5iP/uf88883P2yn36eZBLFOPPHEIBjn+vv444/tiiuuSBrWUdcmWw1lqXCW77hq1aq0xznjjDOCIJ/r/+577kn6zkSP/5e//CUIzPnfae2rc9B3X8vPPPPMIOS3aNEi123W3xWcci1bISz1pypbVNRysrwjgAACCCCAAAIIIIAAAggggMCeEMh5EEtPFuoV17p06WKPPrpriIOf//zn5bpJF9c3yxBAAAEEEEAAAQQQQACByibgD8/orl2BKw016AJYbrnetcwtV/WwuEpahLF8MbPGzTonL8jiXKs23ax5y872yUcvpq2E5B9SoaIevUbYxg2r7fM5k0rdT9t37T4kEQKpnghZ/Sfl9qrElEkIq6zH98893fSaVUtszeol1qBhy8A8W1WxFi9eHFT/0bGrVq0aW93qlFN2Vc3Sdg899FBQpUghKbVBg3YFSIIF3/6nbdu24az8FOCKNgW8/va3vyUFZ6LbuPkmTZoEVZcUSnriiSfc4pTvJyQCP3EtXVWt6PYK4jzwwANJIahvvvnGFPZRIKhdu3bRXbI6f+2115qCQ6U1hcR++9vf2vPPP2/ax29PPfWU/fCHPwwXRQNy4YrExHe/+11/1o4++uiUQSwNceuaHiR85pln3GyZ3n/yk5/Ebr9t27bY5dleKB+F09K11q1bmwJbJ598crrNKrTOH5LwzbvfKXdfrhKW66Aifbk+eEcAAQQQQAABBBBAAIH8FFCBl6ZNmwYPkqhidSG1zZs329q1a4MCNn4V41xcg6o/6yErVZf2HzTLxbFy0aceBlNF7nnz5tmaNYmS7Tls3bt3D+6HDhkyJDDL4aGy3rV89OCd7unmuqhRIX+nduf3yf+Qcx7E8g/GNAIIIIAAAggggAACCCCAQG4E4kJUCmCVZXhGt220OpbmXWArN2dfOL3Wb7wziJKtYQlr12lov79znO3bw6zat3+h77Cf2vKvzW689o/2/sQnY3EaNmpl//zXG9Zhb7O9vt1iR+L96cc32B9+dXSJfQ7oc7Rd/eufWutdWSHbYdfZyuVmP7n4ZzZnVnIQ4unxb1j9BmafzTa7YNSwoL/Dv3uxXffbnUOz/ejCW+yyqy/P+PglTqgMC2SerSDWhx98EAaxdAoami06zKCGC3RN4Zg333zTpkyZElZDUlipWuLD0rB/rrVv3z4pvLRkSfyYjgq3aNg/v+lGrIbO+/rrr4NqWOrLVe3S+89+9rPghpqG5StL041dVdzScHeZtMaNGwcPy/mVy3WN+vn/8ssvgy40fN/rr78eTCt4pmH0/ObWadk77yR/p/zt4qYvuOCCEiEsVb5SpXX57Lvvvta5c2fzb7YrOCW3P//5z2GXukmryuy6Oa+mdw11uGHDhnAbTagfVe3ym/qXeTS8pm1df9pen282bpwr0KXzUv+61t3RXAhLx1Ywcfbs2UFVsKitvofiu/PmAABAAElEQVSqzFWeqmalXUe0GlZp26daHw1haVhCGgIIIIAAAggggAACCBSngB4Yad68ecFenPu7Un9bLl261L766qucXEuPHj1MxXAKuSk8ppf+Zp87d25QPToX13P11VcH1aBz0ffu6FNhO73OPvtsu+OOO4Iq6bk4bqF/p3bX9ylqX3BBrJEjR4ZP7r344ovBja8jjjgiuCGqm4aqvqXlftMNUt1cVXl694tn2rRpNmHCBPvwww9L3GDz9/WnlfRTGvLggQOtY+JLvTrxNOicxA+/nhZNNaSiv7+mdaPy8MMPtwEDBgTXoTL2b7/9dlBmPnqjL7ov8wgggAACCCCAAAIIIJBaQEEhFyDKZLio1D0V5hp37e7syxrCcvspjKWXP8Shhi9U0MsFtdy2lfG9boNWWbvsNu32s7sfvyMREknuUsGqZonR0m6+8zK7/oqm9sYrdyVvkJi7/JrDSyzTfiecWscWzPuNjX3gp+H6IYedZ7+69fxwXoEtbatXk0RW5a5Hb7QrfnCLTZn0r3AbNypdIh8Stho1d53oH+66PFzuJlId360v67uGfuzZ+0jLpvkrr75qxxx7bHgqBx10UDitCYVw/HCObvipPf3002EQS+GmI488MimgMmLEiGA7958PEoGvaFNlJt2X8Nu4ceOCYeL8ZRqi7sEHH7TatWuHizXkYCaVonRf4c477wyqOvlBsbCjFBO6KTV27NikJ2XVl4ZGVBDKtdWrVwfDlWpeQyj6w/i9//774Tq3fabvuiF94YUXJm0+f/78oBKXnhp2TdvJRsMIunbWWWfZ3//+d9MTjq69O3Fi0ud81FFH2ZNPJocaNcRhdGhIff7aNho+0j0lv+k+TkWawleq5PXGG29UpJty7yurc889N3i62HXSqFGjwEj3vlzTPbiohVtXkfcOfdqHu5e3gpWGIPSraml4Q4YkDFmZQAABBBBAAAEEEECgqAT0d7qrUq1qzXrgSNWbC6npoZh69eqZ/vZSoEx/3+rv3mw2DTWvwJqahppXVSn/b+VsHiuXfekehap5KVehbIceroqrOl6Rc7j99tvtWO/+UEX6yod9L7roItMDVRdffHFWT6cYvlO74/sUh14lbmE+L1My8Yorrghep512mr2auImqIQ0PPfTQ4IamlvntvPPOC25s/epXv7LRo0eb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PWVhSwGWpsYtCZAACZAACZAACZAACZAACZAACZAACZAACZAACeRtAkXq1Klj7YF2ew4X6HnVylauJ/UuvEoq1W8jJctVlUKe/3LSzsk5ObJ/p+zesFhS5n0nB1JTcnI6jk0CJEACJEACJEACJEACJEACQRHAv/MQ+gmfaBrCM+FTqFDO/pssmvsUzFwzP5yjmuutGYaQwqtgSLItCZAACZAACZAACZAACZAACZAACZAACZAACZAACeQdAkWKFy+ebbUJCQnZyvJCQau+d0njrv2julQIvUqVq6Y+9dtfIWtmfinLfh4Z1TVwMhIgARIgARIgARIgARIgARJwI3DmzJmoC7D0OrT4C2KswoUL6+ICu6X4qsAeeu44CZAACZAACZAACZAACZAACZAACZAACZAACZBAASJQ5NixY9buao9YJ0+etMrySqLjzUOlRvNuub5cCMFKJFeROZ8/m+tr4QJIgARIgARIgARIgARIgAQKLgGE8NP/xstNChBkYR1FihTJzWVwbhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARLIcQJFNm3alOOT5PQE8IQVCyIsvZ9YC9ZEz1iaCLckQAIkQAIkQAIkQAIkQALRJABPWLEgwtL7jLVgTfSMpYlwSwIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkkB8JxOX1nSpbuV7UwxEGwgyesbA2GgmQAAmQAAmQAAmQAAmQAAlEkwBET/BCFWumPWPF2rq4HhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARKIFIE8L8Sqd+FVkWIR8XFieW0R31kOSAIkQAIkQAIkQAIkQAIkEBMEYlGEpcHE8tr0GrklARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggVAJ5HkhVqX6bULd9xzvF8try/Gd5wQkQAIkQAIkQAIkQAIkQAK5QiCWxU6xvLZcOViclARIgARIgARIgARIgARIgARIgARIgARIgARIgARIIF8RKJLX96ZkuaoxuwuxvLbcgNaoUSNp166dNfWECRPk0KFDVj7QRFxcnLRo0ULatGkjTZo0keLFi0tGRoakp6dLamqqbN26VdasWSM7duwIdEi2IwESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIGwCOR5IVYhKRQygJRls1TfKZ8NU1udr9eqi9Rr1VWV9R74ZMjjh7M2PWmNGjWkYcOGOiuLFi2SgwcPWnmdKF26tPTs2VOaNm0qS5YskSlTpsjp06d1tbWtXbu21KtXz8ovXLgwJDGUNUAQiWuuuUa6ds3kim6rV6+WxYsXBzGCSJkyZeT9998X7K8/mzFjhrz88sv+mrGeBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABMImkOeFWKEQgOAK4istvHKOgXJdN3n0MLl40FMSjiDLOX4w+RtuuEF69epldRk/frx8+OGHVl4nBgwYIFdeeaXKdu7cWY4ePSozZ87U1db24Ycflvr161v5YcOGyezZs618LCeSkpJk1EcfSTGPB6xAbP369YE0YxsSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCJtAgRNiQWA18pE+Fjjt/apuyy6qDPkpn72o0inLZipBFsRYuSXImjNnjk2I1bx5c2vtZqJ169ZmVrp26eIqxKpWrZqt3fz58235WM5ceuml2URYGzZskC1btqjwhO3bt5fChQvH8i5wbX4IVG9dVYqVLiYbpm/009K9ulBcIandoaZUbFReSlcuJUf3HZXU1Xtl46zN7h1cStEvqWYZSSyXKNuX7JTDu4+4tGIRCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACdgJFCghFgRWEFTBILjqPfAptbUjEcv7FbxgQbilvWfpvtH0joXQgefOnZNChTJDMCJUoZtVrVrVVty0WTNbHpmiRYsqwZKuOHTokJw6dUpnY37bpk0b2xrffvtt+eWXX6yyW265RW6++WYrz0TeIFCqYklpeU0zqd+tjsQXi5e0LekhCbFKVSopl73QW0pWKGnb8aaXNpaOf28n3//rZ5+iKszf/ra2UrxMMav/7BHzZM0v9KxmAWGCBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEjAK4EicXFxcvbsWa8N8kuFKcIKJtRgpsesScpLlvaMBe9ZKI+GnT59Wg4ePChly5ZV0yUmJkpCQoKcPHnSmr5p06bZPEGhvbPdBRdcYPVBIiUlxZaP9UzNmjWtJUKcZoqwrAom8gSBuMJx0viSBtKsX2MpU6V0RNZ85Wt9lTctDHZ492HZvyldkmsnKc9Y8LJ1xat95cvB4+Xs6ez3u/OubyFtbz7Pto4zp854PGpl2MqYIQESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAFvBIo0aNBACX1MYQ88JeU3096sghFhmQy0FyyMg9CGd70+KWpiLAimTG9QEFTNnj3bWl7Pnj2ttE7Ag1bnzp3lt99+00XStm1bK43EggULbHkzA89bmKd06dKydOlSWb58uUAU5mYlS5aUSpUqWVVYb5EiRaRjx44CkdiSJUt8zmV19CTq1q1ref9C+c6dO6VKlSqqDCI00+rVq6eyx44dU+3MukDSge5juXLlLCEcPIht3brVNjxYt2vXTrFCmMT16+0elOLj48UUke3atUsyMrwLfALlWblyZSlRooRPBmabo0c9YfpSU621Y01YG2zfvn3qPgBhZqtWrQShLg8cOCArVqyQtWvXWn0ilShTrbR0vCNLGAgxKOYO1ep2qW2JsFZPWie/j8wKuYl5ml7aSHm6qt+1jqz7zS5A7DCknTS7vLGa+tTxU/L7iPmyY9kuOXbgeKjLYT8SIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIECSKBI4cKFJTk52bbr3gQ3tkZ5KANvWLDMcIRPhrzyzFCFM61whfVaTQp5rGA6QjBlCrEgqDKFWOedZ/fko8d2CrEaN84Um+j6mTNn6qS1/fvf/y5XXnmlzcPWDTfcoOohQHr00UeVYMfq4Encf//90rVrV6to+PDhqkyHU4Qga9CgQVa9t8TQoUOV+EvXw+vVCy+8IE8//bQusrYY+91331V5nK/9+vWz6vwlgt3HZ599ViBY1HbttdfahFRdunSRJ554QlertZjX0GWXXSZ33nmnVT9q1Cj5+uuvrbwzESjP//73v6LFaRBS4rg5bcSIEVKsWGaovePHj8vVV19tNXnvvfes9O8eYd+evXvlqquusgnh0ABiuKeeesom4rI6hpnYt2G/rPhhjWyctUlu+uAaSUy2i+0CHb6VJ7QhDIKuuR8stHWb++FCaXxxA4krEieNPFtTiFXME4ZQi7DgReu7R36WE4dP2Pr7ysQV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69ev193V9tSpU4Jwhtqz2dKlS5VXMt3IuW5n2EndDlucC0OHDrWY6Tr0GT9+vHTp0kUQWlKZwU+301vnnLrc3AbSxmwfbPrL28f77fL9v35WYQednq1UR8/pMX/UYoE3Kni3KlmxhBzYdlCO7s/wOy68XuGD0IRlqpWWvev3y6ljWV6wnAPsXr1Xxt3pCc9ZSKRc7SSJT0yQ9C0HGI7QCYp5EiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEihABAp5QsBlqVuMHdcikeTkZKM0e9JNiJK9Vc6V3PjSjKAGNz1boSMEWvVadVVjQGClBVcb/5ylxFo6jwbwqoX2wdi4Jy4KpnnMtIVXJYS9gyBrxYoVkp6e6YkoZhYYgYXk9j4mJSWpMIJOr1ah7FpiYqI0bdpUsIUwb//+/aEMwz4kQAIkQAIkQAIkQAL5gACE/7Fs8OIajvn6oQLG5d/C4dBlXxIgARIgARIgARIgARIgARIgARIgARIgARIgARIggXAIFDghloblFGTpcrctxFeBeM9y65tXhVhu+8IyEiABEiABEiABEiABEiCB2CdAIRZ/lBD7ZylXSAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAL5k0CBCU3oPHzwfoUPBFkwhCrU3q+01yuIr2A6rzL8HwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAk4COR5IdY5OSeFPP+FahBjwfQ21HHc+mFtNBIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggfxPIC6v7+KR/TtjdhdieW0xC40LIwESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIE8SCDPC7F2b1gcs9hjeW0xC40LIwESIAESIAESIAESIAESCItAXFzs/jMvltcWFnR2JgESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEPgTwfmjBl3ndSv/0VMXkwsTYaCZAACZAACZAACZAACZAACUSTAMROZ8+ejeaUAc9FIVbAqNiQBEiABAIiULRoUSlVqpQkJiZKsWLFJD4+XgoXLhxQ3zNnzsipU6fk+PHjkpGRIYcP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LFyD0Em/gEbYubfeeku93K9SpYr06NFDLrroItuL74oVKypxji/GCF2JF+P4OEVmgawXIkwtrsPasEZTcGCKBzGXWRfI+GijX9w7w2wG2j+/tcO9wuQazP4hhB/uw2bI02D6x0JbtzCouB5gzvNLnzuRXDfOeYSt0wYvSo8++qjO+tzqawSNEKbw22+/Vec3PJX17NlT6tSpY+vfq1cv4XPShiTkzNKlSwX3EC0OwlbfuzAozh1TrIW/a1BmCkwR9nXUqFEhryHSHc3zaeHChZa3O3j+wjMB4itz/eedd57fJYT7TDAZO//mw+T6GYZ0MAJKtNemr2s+EzQRbkmABEiABEiABEiABEhAxK8Qa/r06QWKE375RCMBEiABEiABEiABEiABEiABEohNAubL1thcYWysatWqVdK5c2drMQg/2KdPH1tYpX/+85+2F4JojFA5NJHu3btbYhOTh5u3CIQa0mGudFt4ucltIdbp06eVpxB45dGGc+CXX36xxBHwFAWhlGn+XsQ6BSfOsE8IN2Wam6cgs96ZdnpsctY78xBdmEIsiEKSkpJsXr369etn6wavHW7h1GyNAsg8+eSTYopQNmzYIMOGDRN42/r888/lxx9/VOIsPRRewONF/Jw5c3RRti3OMTAFh1Beau/bt88mHoEocN68eWoeCB1M70U65Fe2Rfgp0MfI7Xrw0zVfVruFJYSIzumNBjvvvF5wTlx66aUyfvx4r2y8edMyO+hjYpblVBqiElOMBHGSaRDL6HMLQknznIMY0bRQBK9OHhCXmn8bwPOfW8jHjh07yrXXXqu81x06dEjdJxBa1DSEGcU1BE9M+DzzzDO268m83s1+TIdGYN26ddm8YOmR9uzZk02giPPJ9HCIYwoBndPr1W233SYIZWiKgvW4kdw6n4cINWg+c3Fd//DDD+p6wTWD0MWmt01cGxBC+RLdhvtMgMhTi92c87Vr186GI9T3Ivr+w2eCDSczJEACJEACJEACJEACBZyAXyFWAefD3ScBEiABEiABEiABEiABEiABEiCBPEfgk08+kb/97W9KzKEX//zzzws8AyF8GoRG8NZjGl5mr1y50iwqsGmnsAAg7r//fktc4ATzxhtv2Dx5wBtZLHi5wY/rLrnkEmu5eAn7yssvy+QpU9T6IMzSnix0o6+++kon1RYvcU2hU6tWrZSnGLw4hsAH3p3MF8/dunUTeDqBEAwCE+f4pjcQTIAX6KbHrLJlyyqxBF7Cb968WbRnHduijMzYsWMFgihteKn90ksvKdEhPOXgWDpDMs6dO1c3D2sL4aIpzIB3MAggZ8+ercbt4bnOnObmqcxsk5GRIWCAF9uhhA2EKMF8uX7vvffKokWL1PFAGEnT+8maNWvMqQNO65fuWGtBN/Bs3LixDQOO23333Wcr0xmINt9//32bCLZLly6WEMstpOh1112nrlcInPS55e+60fPlxPahhx4SXEPwfgehX61atWzTmOc4ruOqVata9Ti3H/DcS5f88YcK4+rmrc5q7EkEwmPcuHFKUGreW+688065wCMyWbxkifL+CG+A+jhBLAmPWQghp0Uuek4IlCHewX0Pgs5KlSrpKrUNJmyrrWMByEBkiJB8vgzn8H/+8x91P0K7SZMmeRVimWEJ9Zg//fST3HDDDTqrni+vvPKKTJs2TSACxrnYrm1bqfCX4A/HGaFjI2X+nocIRajPM8x52WWXyZ9//inw5oV7hTN0McS2vkRYGCPcZ8KCBQvUcxBj4RqB2BDMsB48H0zT9xezLJA0nwmBUGIbEiABEiABEiABEiCBgkaAQqyCdsS5vyRAAiRAAiRAAiRAAiRAAiRAAvmeADyxjBw5Uh544AFrX/ECDt5A8HEzU8ziVl9QyiA6qly5sm13IXrQHl5sFX9l8KIToiZtYA2vWN98840uypUtBAoQ3JleePCC+uabb3ZdD14WL1u2zFa3Y8cO24tliK5uueUW1QbeTCC6gIccbZjr8ccf19lsW7z8BR+8kIfBexvEXdpQd/nll6vszz//7FeIhZfvWIfpWQdrNL2O6LGxxYvvTz/91CwKOY2X/9dff70lbsLaBw8erESQSGNfTYPow01YYrbRnrrMY2bW+0vDExte3GtPLTiXJ0yYoIQPZlgq8IeAMBTTa9NrDWWM/NIH15dTbOi8hsx9hUAR15QpPoRAEmIliGFxfjrFQRAD3X777eoYaqGEv+sG4rucMojJIB7Dx82++OILqxjh2Zzh185v3VrwCcQC4QGe4OJcj7d54FHp6quvFqxziUeoBa9K2nAc4NUIx8np6RBttHc53Z5bO4FmzZrZC1xyuH/o5+mKFSts4Ql1c9yffv31V521tngmQEhuimtx/UFw6yagxrMJAlmIBiNh/p6HWLMpxMK+Dh06VOCFUT/7zHUEsi59n9X3XbN/IGk870w2SOOch5cs7SkL40Bk6MY8kDn02vRaA+nDNiRAAiRAAiRAAiRAAiSQ3wnYvxHK73vL/SMBEiABEiABEiABEiABEiABEiCBAkJg9OjRtlCE3nYbLzzfeecdwQtRmohbmDEIrXwZwv05rWvXrs6iqOdxbF977TUl7PA3OUL8vfXWW9mazZw5M1uZLsCL5TFjxiivWLoskK35sh5CDbykDseGDx+uRCz+xoD3LoQig8giEobx4GkOYhHTIAxwirCwj/DU5c8wJjwqoT9CioZijz32mM3LCl62myIsjPntt98KPIYFa1gT1oY1Yq0F3Xr37p0NAbz8+LJZs2Zlq4YwSBs86PizSFw3/uYIpR5hN02PWMiHGu5Mzx8ID3iBhLfHQGzLli3y5ZdfqqYffPCB8hjn7OcmwoLHSH/H1jkO8/4JmGEudWsIVuF9ys3gzQnPq0AMwq1AxE6BjIU2/p6H8JKIZyKevabhmQBxrmkQLTlDY5r1Oh3uMwEsP/74Yz2c2kKoa4qw4JUrVGEunwk2tMyQAAmQAAmQAAmQAAmQgEWAQiwLBRMkQAIkQAIkQAIkQAIkQAIkQAIkED4Bp6jE+ULOOYOzvZtIxCwz03os5xi6/P/+7/8EYaTS0tKyvRjEizd4d7jxxhsj5iFIz5uXt209YY2c5u/l+4EDBwQf0+DlpnTp0maRSjvDELkdO13mbOt2LjnbOPMQRSCsIkQKTsEQFoSXvL/99psK1eXcB9QjBBleLDvHRR3WCS8+CHWEl73O9UGsA5Gfs7xDhw7orgxeOJ577jlXUY/m4OzvXAvmeeqpp2TixImuojNcM/CahdB8znB8zrH0nHp9bluzD8InQvgEzvBk5DSsHQIOeAmDx7FADNcrLFQhFvbx4Ycfll27dmWbDuIGhGYbMWJEtrpACvSa9BoD6ZNf20Bc4fSeB77+wmnCk5rznG7evLmFCccGgg5nGzPv77pxe064lWFSc1zkzfPbeT3g3JoxY0Y28STuLRD3QdjkNIgV3cQ2WM/48eNl3759ti7OOQPhgTVD1PL2228rgaFznzAB7hMQKP/73/+27fN///tf5b0QTM1914uC9ybcA19//XVdxK2HgBvjQMA4+7k9X7XnN7fx4KESIQcRhtXtnos+EP+9+OKLYobadR5b5zrQz9nGmff3PMQYU6dOVaJmPBPdrjlcK3jmIoyj2zMZYzhN32/1/ddZ7y8P728QLLuFk0X40CFDhgi8KYZiek16jaGMwT4kQAIkQAIkQAIkQAIkkB8JFKpfv77n3x32X2lgR7Ub3UB+dZQfwODLOli/fv3yw+5wH0iABEiABEiABEiABEiABEggXxIoV66cz/3CCzqaOwF4sUGYnho1aihRium1xL0HS/MjAYTlQhg/vGBGeDOEsQzUELarVq1aSoSBkIAQLpgGjx+NGjUShHfE+RXs9QjhGtaWkJAg27dvD1i4ZK4BaXizwbmOEIUQYB06dMjZJMfy8DyF79TgbWTTpk0Be25xLggcEO4JXqt0GC9nm0Dy8Hyix8J3fG4v4gMZB21w7kBgCPEDuNJylgCupwYNGqgwbDhuuF7dvJBF6roJdm9q1qyphGg4F9xEnM7xcF17vodW9wf0cd4/nO2d+UB56H7VqlVTzzusDfejQO912C/0xf0LQstA++l5uY0uAfxtg+skKSlJCYLh8cztXUekV+Xveajnw9+tOO8h0sQzIdTwffo+Hu4zITk5WT2jsA6IhJ1iM73uQLZ8JgRCiW1IgARIgARIgARIgAQKKgEKsf468hRiFdRLgPtNAiRAAiRAAiRAAiRAAiSQlwhQiJWXjhbXSgIkECoBvOCuW7eu6g6PJYF6Tgl1Pn/9ICyrWLGiagZRSzjiMH9zsZ4ESIAESMBOgM8EOw/mSIAESIAESIAESIAESCDWCcTBVS++0HF+Yn3hXB8JkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJ5EcCEDrhuzoYPL3A60tuGebGGmBYE0VYuXUkOC8JkEBBJcBnQkE98txvEiABEiABEiABEiCBvEog977FyavEuG4SIAESIAESIAESIAESIAESIAESIAESIAESyGECqampKjQhvFHBG2BuiLEwJ+bGGhASC2uikQAJkAAJRJ8AnwnRZ84ZSYAESIAESIAESIAESCBUAhRihUqO/UiABEiABEiABEiABEiABEiABEiABEiABEggBwns3LlTeaBKSEiQ8uXLK0FUDk5nGxriK8yJueGNBWuhkQAJkAAJ5B4BPhNyjz1nJgESIAESIAESIAESIIFgCFCIFQwttiUBEiABEiABEiABEiABEiABEiABEiABEiCBKBE4deqUbNu2zfKMVbFiRSlZsmSOz445MJf2hIU1YC00EiABEiCB3CPAZ0LusefMJEACJEACJEACJEACJBAMgSLBNGZbEiABEiABEiABEiABEiABEiABEiABEiABEiCB6BHAi/ctW7ZI5cqVlTiqTJkykpiYKEePHlWfSK6kRIkSgg8EWLA9e/YwHGEkAXMsEiABEgiTAJ8JYQJkdxIgARIgARIgARIgARKIAgEKsaIAmVOQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQDgEUlNTVYjAqlWrSrFixaRs2bJSunRpOX78uJw8eVJ9zpw5I2fPng1omri4OClcuLAKPYjwgxgTZTCMqUNgBTQYG5EACZAACUSVAJ8JUcXNyUiABEiABEiABEiABEggKAIUYgWFi41JgARIgARIgARIgARIgARIgARIgARIgARIIHcIHDlyRNatWydJSUmSnJysvFfBOxY+kTB42UpLS5P09PRIDMcxSIAESIAEcpAAnwk5CJdDkwAJkAAJkAAJkAAJkEAYBCjECgMeu5IACZAACZAACZAACZAACZAACZAACZAACZBAtAlAKIVP0aJFpVSpUkqIBY9WCCkIL1eBGLxnIcQVvF9lZGTI4cOH5cSJE4F0ZRsSIAESIIEYIsBnQgwdDC6FBEiABEiABEiABEiABDwEKMTiaUACJEACJEACJEACJEACJEACJEACJEACJEACeZAAhFMUT+XBA8clkwAJkEAOEOAzIQegckgSIAESIAESIAESIAESCIEAhVghQIulLuWrN5QWPQZIjaYdpEzFmlLI819O2jk5Jwf3bJVtq+bK8t++kH3b1+XkdBybBEiABEiABEiABEiABEggTALnznn+iv/rE+ZQQXUvVMjzr5O/PkF1ZGMSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESyKMEKMTKowcOy+50wyPSps+QqO4BhF5lK9ZSnxbdbpLFkz6U3796Papr4GQkQAIkQAIkQAIkQAIkQAKBETh79qwSYQXWOrKttPgLYqy4uLjIDs7RSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCAGCfDbcMdBwUuCvGB973k76iIsNy4QgmEtNBIgARIgARIgARIgARIggdgikJsiLJMEBFlYC40ESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAE8jsBCrEcR7h48eKOktjLwhNWg7Z9YmZhWAvWRCMBEiABEiABEiABEiABEogNArEiwtI0KMbSJLglARIgARIgARIgARIgARIgARIgARIgARIgARIgARLIzwQoxHIc3XLlyjlKYitbvnrDmPCE5aQCz1hYG40ESIAESIAESIAESIAESCB3CeiQgLm7iuyzx+q6sq+UJSRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQGgEKsRzcqlWr5iiJrWyLHgNia0HGamJ5bcYymSQBEiABEiABEiABEiCBfE0AgqdYtVheW6wy47pIgARIgARIgARIgARIgARIgARIgARIgARIgARIgATyDgEKsRzHqn79+o6S2MrWaNohthZkrCaW12Ysk0kSIAESIAESIAESIAESyNcEYlnsFMtry9cnBXeOBEiABEiABEiABEiABEiABEiABEiABEiABEiABEggKgSKRGWWPDRJ8+bNY3q1ZSrWjNn1xfLaYhZaLiwsKSlJrr/+emvm2bNny6pVq6x8MIkGDRpIp06dpHXr1oKwnsePH5eDBw7Izl27ZPPmzWrcNWvWyNmzZ4MZlm1JgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIIM8RoBDLcciaNWumBCX79+931MRGtpAUCnkhKctmqb5TPhumtjpfr1UXqdeqqyrrPfDJkMcPZ23mpAkJCdKrVy9p2rSpVKhQQebPny9bt26VRYsWmc28piE06tChg+BYli9fXvWHGChUsREmKl26tHTu3Nmac/369YKPm1166aXStWtXWbdunfz000+SmpqarVnZsmWlY8eOVjnWt3HjRiufk4nzzz9f7rjjDmuKKlWqyNChQ618IIm4uDgZMWKEtGnTxm9zeD1o166d33ZsEDsELr74Ytm0aZPXczx2VsqVkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEDsEKAQy+VYdOvWTcaPH+9SkzeLILiC+EoLr5x7gXJdN3n0MLl40FMSjiDLOX6w+SZNmshzzz1ndevZs6ecOXNGeV46ffq0Ve4t8eijj0rv3r2tavSHKGrAgAFWWbAJeEoz17Rhwwa56aabsg0D8ZhuBzFZo0aN5LHHHsvWrn///jJ48GCrfMqUKfLEE09Y+VhPfPHFFxJoGM8VK1bE+u5wfR4CrVq1UuckRHPx8fEy4fvv5bnnnycbEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBAAlQiOUC6pJLLsk3QiwIrEY+0sfaS+39qm7LLqoM+Smf/X/27gNOqup84/hL772tIB0BUZpBpKlExYJYiYIKauyiEiOWqP8YG2qiJlFjiV0waozYUESxICgiNnrvCEtZei/Cf5+7nrtnZmf77DK7+zv5jLede+6935klsPvsex4I1hdNmxAEshTGSoRAVnjTqStlypSx888/3xQAyq6pGlW82+TJk02VnUqVSqtIduihh8a8xOmnnx6xv1OnThHbbkNT+fntyy+/9DcTer1hw4YZQlgrV64MKo7JqEWLFhHH9+zZk9DPUxA3J6MRI0aEQ7///vv2+OOPh9uJslK/fv2gOprCitWqVTuot1W5ZmXrdFZ7m/b+DNu+cUee7qV241rWqmcLa9guKfhaTVm6waZ/ONM2r94Sc7wKVcpbx35ZT0e7Z8demzp6Rszz2YkAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggIAvQBDL10hdnzFjhrVv3940vZymlSvKTQErBarUFLjqM/jOYBn9TK76lZYKbrnqWe5cdzz6vMLeHjBgQLZBrBNOOMEqVKgQ91vbv3+/bdiwIZi2UoNXrFjRatSoYZs3b464Vrdu3SK2a9eubZpqMTqMpLCS38aPH+9vJvT6qaemB/t0o5O+/tqG/uEP4T0rhKTgUUluCjVp+knXWrVs6VYTYqnP7quvvmqalvJgtlKlS1m7k9raMRd2sUPaNAhuZflPP+cpiNXr993s+KvSpw91z3XcFT3s2ze+t08fG+92hcsGrRtYnxtPCLdjrShcSBArlgz7EEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSiBUpH7yjp26+//npAoKnjKleuXGQ5/BCWphq85pGxMUNY0Q+owJb66hw1hbHctIXRfQt7u1GjRqYKPlm1Sy65JKvD+To2a9asiPOPP/74iG1tRFfKUgUthcP8Vrp0aatevXq4a8uWLbZr165wO9FXunTpEnGLI0aOjNhmI/EFatWqdVBDWPVa1LWz7+tnt315o519z+lhCCuvcqpq5UJY+3/Zb8t+XG6LJi8xrasdM7CLdT6rQ4bhazasEe5T4CrWa9+e7KdDDQdhBQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECgRAtQESvq7VdFLFcV64orrkjI6cSibjnmpqtmpUBVXipauXM0jqY2zGmQK+bNxHHn1Vdfbffdd1/MEVWlql27djGPuZ2qBJSUlOQ2bc2aNbZp06Zw262oqpM/VduCBQvsq6++Mn/aw+7du0dUfurcuXMwhaIbwy379OljY8eOdZumIJOb4lA758+fHx6LXlFgplevXnbYYYfZzJkz7evU6lPbt2+P7hZsK+Clfq79/HNqZaHUvpoesXfv3sGzuqCh65PZsnHjxhFBxHXr1lmlSpWsatWqGcJmCiy2adMmCLFk9SyZXSunz6hrK4zn2pIlSzJUGjvyyCOtWbNmQfWySZMmua7hsmVqZaqyZdP+2Fu/fr2lpKSEx7JakW3btm2tQ4cOps+G3guNv23btojTVAWrQYMGEe+DOtRP3ScjNX2WVGEtuukamrLymGOOCSqtafzFixdHdwu2FeRz1az27dtnixYtCvYfccQRwWd069at9vnnn9uqVatinu/vVAhwyrff2qLUa/3+97/3DxXYet/bT7ZDj2wYjq+wU9nyef+/o97Xpk21unf3Xnv2wpdt06q0SnVV61ax69+5ysqULWO9r+llP703PbymVmokpQUiFcB6oMejEcfYQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAILcCef/Jd26vVIT6K6yi6QkVoFm4cGGRm6JQ1bDU0qYjvCPP8mlTFU4Ipyts2TE9TJTnQfN54kknnZRpEOviiy+OCDjFupTCdap25tqcOXNs8ODBbjNcvvXWW8GUgm7HWWedZZ9++qndcUe6Z3To67TTTnPdI5YKB/nt2GPTQiNu3zfffONWw6WqaN1zzz1B+Cnc+euKpkO86aabbNq0aRGHFLb629/+Fu7T51j37Sq7KWySkyDW+eefb7feems4jlaeffZZk6/CbtHt73//e7jrwgsvzBBOCg9GreT2GQcNGmR6/1x78MEHbdSoUW4zeL9efvnlcHvYsGH25ZdfhtsKSf33v/8Nt+V3+eWXh9uZrQwcONCuu+66mO/Fzp077f7777ePP/44OP2B4cOta2qQKrq1atXK/vOf/wS7L7vsMps+PT0QpGCYDBXs8wN6N954o/3yyy/25JNP2ogRIyKG1OdQXwuuafvee+8NQ2bar/MVxLvyyiszhP02btwY2Lzxxhv23XffBcNEV25zYxfUcs+OPTZj7Gz79vXvLSl1isBzh5+Rp0sdcniSVa1dJTj3h7emhiEs7diWst0mv/qd9by0m1WuWdlUiWvd4vTwXc1D0oJYe3fuzdO1NWatRjVs7aIU27srb2Pk6cKchAACCCCAQD4F9IsBrsrpM888k8/RLBjr+++/z/c4DIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQFEXKF3UH6Ag7l8VsVzg5tprr7Vu3boVxGUKbExXDavP4LTpBfNzITeGpic8WFMUrlixInyEKlWq2NFHHx1u+ytnn312uKmwkkIs0W1k1DR6rVu3NlUi8puqEpUvXz7cpYpZK1euNE0h6FejUuUjv6mSkWt79uxxq1anTp2IAJOqKvlt3Lhx/qbdmxrAUqBKFahiNVX1ev75523IkCGxDof78jK9pqZbvOWWW8IxtPLJJ58EQayInfncyMszfvjhhxFXVaUwv5155pn+pvmfBx048cQTI477Ia2IA96GAlg333xzpu+F3qPhqeGrP/zhD95ZOV9VdS359ujRIyKE5UYoU6aMDR061B5/7DG3K+bygQceiAhhuU76enn11VeDkJfbp6W+PhRUcyEs/1hhrL/xx1H28ImP29iHP7WNP2esSJebe6jXvE7YferoGeG6W5n92Ty3ak06HRqua6Va/WrB9o5NOyL2Z7VRqnQpO+6KHsG0in/8aIhd+vxFdusXf7Bhn1xvva+O/ExmNQ7HEEAAAQQQOJgCqjLrXtdcc02+buW5554zvX766acw3JWvATkZAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIEiLEBFrEzePIWxVD1IYZY777wzCFtMnjw5k96Js9uvhqWKWPltGkOvgxXC0v3/8MMPwZR0LjClqkjRAZImTZpY/fr1w8f96KOPbMCAAeG2W1m7dq0lJyeH07op6KLKQgrDuHbeeee51WD52WefhduaAs4FqVTJSFPlKaSle3NTxamzPjuXXHJJeJ6u8cEHHwTbmjrPNQW2/Onj1K/v6ae7w8FSU8dpGj09X7ly5YJ9qpykykpffPGFqapXTpoqYmXVNK3dww8/HBEImjp1ahhKnDJlSjAtn6o7+U2eLqCmqf5iVc3y++f1GRXIUwUqF1CLrkjWt29f/zLBNH/+jp6pYSe/vf/++/5mhnV5R0/VN2/ePFu+fHkwzaA+c66pqpqqcX2balQ7NXin6RYVwPObquup6TPo2j/+8Q/TNIOuacpCHde0mApRudajZ88grBVrukXXR+/vhg0bgkCWwnqu6bN51113WWYV21y/wlzu3rY7bper2TD9WTcnb8kw7oblG8N9mqrQb9V/DWJtWbstCGmpulb5yuVt5cxV9vP0lbYnRqWss+/tZ+1OTJtmUmPJXV+PFatVDCpv1WlWx0bd/p5/GdYRQAABBBBIaAEFstRyWxlLFbV0rquspTG0ngiVsfT3NVrRFvD/rl20n4S7RwABBBBAAAEEEEAAAQQQQAABBBBAAIGSJhBZCqikPX02z/vaa6+FU7kpjBUd9Mjm9IN6uGXH4+J2fTfWuJHD4zZmbgYqnxo+0m/Yu9apU6cM1X/83+RXMEK/le9P8+bO1XL06NH+pp177rkR2127do3Yfumll8Lt6DCem85N1bBcUEyd//e//wXhKXeiq8akkJIfsIn+IZGrxObOU1hIlZ80xaCmrvMd1EeVkLJqCklpikNNN+hP6xd9jiozqcqW/wzLli2zq666Kuyq6RA1TV/0lIia+k779VIQKLuWn2ecPXt2OHzt2rUj7vfwww8Pj2lFzo0bNw73tUsNmrmmilCqdJZVO+WUUyIOa1rDiy66yG6//fbgMxNdUat///72yiuvBA6qYuW3SV9/HRqtXr06OKRAWsuWLcNuCpmpile/fv1Mlck09aLfot38Y/rM6/q6Z33W9F5pn2v16tULglxuuzgtazaqGTyOnnffnn0ZHs3fV+XXKQxdp0o10qbaVKWswU8PtJOG9g6qXV3wz9/ZsHE3WPvT2rmuwVKhLxfC2rRqkz1x9rP2QI9H7blBL5urqtW292FW45D0cFjEAGwggAACCCCQIAL//ve/I+5EgSr9/TmnTX/3Vn8/hKUAVm7DXDm9Hv0QQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECgqAgSxsnmn/DCWpilUwKJy5crZnHXwDi+aNiHuF2/RIf+VtfJzUwov+T8YUhUrBYv8dtxx6cEzVa1yFZr8Pm79P//5T0RIxVW40nEFkvxqQqpO5IIzOv7pp59qETYX2jrt1FPDfbq2zlE1Kdfat28frGoKOr+p2pdrClz51ZEUorr33nvd4WCp0JMCO64paKR7zqydccYZQfBs8eLFNn369JjdVGFKFbxctS11UgUuVYNThaZ4tvw+o1+dTEE759m5c+eI+3f37KqiKWDmV6iKDpO5/v4yKSnJ34yoZKUDTzzxhMnVvWJNhRkxQNRGdDBOgTm/OtqoUaNswYIF4Vm6Hz8oFx5IXdHXhx/qmzBhQobA4e9+9zv/lGKzXq5iWmHHA/vTg2fRD+dCaeUrp1WUc8crVK3gVoOlH9oqXaa0nXlXX2vXp23Yp9lv0qugjX34M9uyJq0C19pFKfb6H94K+7mwVriDFQQQQAABBBJMQKEp/f3Jr16lUFVOphdUCMtV0XKPpWCX/p5KQwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECgpAsQxMrBJ0BhLFeNpk+fPqYfNCRqdSw3hWCfwXfk4Mly18WNnbuz8t+7UmrwTT8k2rp1aziYC9hoR+/evSOmwxsxYoRVrVo17Bu9oqCUwlqulS9f3lygSlWd/KYpDv2m0M3evXvDXa1btw7Wj/rNb8J9bqrAjz/+ONxXs2bNIMDngkPugB/s6tixo9sdLBWOitXGedMo6rgqhMVqfoAs1nG3T5WZ/Cpd8lHQTdMmxrvl9xk//PDDiFs64be/DbbP80JGu3enT3vnAnq/SX1//AppY8eOjRgn1kZ0xavrr7/enn76aTv55JODimxLly4NnGSll6ph5aY1aNAg7K6gkD5bLVq0iHj54Sp1Puyww8Jz/BU/9Of2R1ek8KfOdH1K+nL+xNTQ5sYdtuDrRfZInyfsr8f/0x7s9Xf79o3vQ5q+t/WxUqVLBdspS9eH+4+7sodVqZ0eyl09f609ctLjwTj++eEJrCCAAAIIIJCAAgpPRVfHUsDbrzbr37aOxQphRf+9wz+HdQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEESpIAQawcvtszZswwVRdSOEahGlXHevbZZ4PpwPxKOzkcrsC7xTM01bLjwa+IJTA/2NSoUSOrX79+4HjppZcGS/1H4aExY8ZYtWrVwn2xVt56K716jY4PHDAg6KYp4fymUFd0W7lyZbhL0+OVLVvW/FCNC1eNHz8+ovKWQnyuMpYGULUpf6rBNm3ahONqxT/mH5gWVdnKH9PvN3fuXH8zx+sKvWnqvoJo+X1GhcRUrcu1jr+G0LqmTg3p2qOPPupWTeEjBe3cFJI6oNDT559/HvbJbGXKlCm2ZMmS8LCCXEcffXQwHeSkSZNMUxXqs6eKbXlpflhQY7/55psZXm5KSzd+u3aRU+W5/bGWqubmVzTTZ7U4Nm8GxuwfL6po1tt3vG//7PuUvXnzO7Z7W1qAb/8v++3Tx8bbom/S3vsKVSpYUpu00NzKWcm24eeNwXUatjvEbvxwiF339pV2yrATrW6zOrZ7+55gnP374ltJLvsHowcCCCCAAAJ5F1CIKjqMpbCVH8ZStSyFsPypCHVFBbkIYeXdnjMRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEip8AQaxcvqduqkIFshTyUBDj5ZdftoceesgGDRpk3bp1M00Xp+kL/Qo8ubxMnrsXRGhq3MgHgvspiLFz8qAu6KLgm9+uuuqqIARzxBFHhLu/mjgxWM8uiPX222+bP5Vcl9SAja7jT/O3YsWKmIGkH3/8Mbye3mNNMee/1wqCqSkEo+kFXVOoRgEy19atW+dWg2WtWrUitv1z/QNr1qzxN63Br4G0iJ2pG1vyGKZSGC2zKlvR18jtdjye0Q+oybNu3bpBOFL3ovf03XfftZSUlODW9L6ceeaZwdQ77l5VKWzfvn1uM8vlRRddZApduantXGdNEdiyZUtTlSwFBHMTkNIYCnP6nxk3bnZLf+rK7PrquP8Zd19HOTmvKPXZtWVncLuaSjBWUzUrZ71t/fZYXWLumzRySrg/qXVoLTitAABAAElEQVRa6FPTH75wyUibPmZW+JmoeUgN6/K7znb167+369+5ymodWjM8jxUEEEAAAQSKikBWYaxYISwF9xXC8qc2LCrPyn0igAACCCCAAAIIIIAAAggggAACCCCAAAIIIFCQAmULcvDiOrbCWH674IILTGEgPxDkH8/JuqptxbMtnj7R4h2catnxuHjeYo7HUsUptQ0bNgQVipo3bx5sq8KUQkkuZKGdz6ROG6mmKkhZNYWkZs6caW6qPAXn/vjHP0aM9d5778Uc4osvvrBzzz03PHbhhReG66rWtGPHjnD7u+++s7POOivYVrjJD8OoyprfFMw6/PDDw12HHnqobdq0Kdx2K36YS/tWrlrlDsVt+dhjj5l84z09YTye8YMPPjBNp6imz8aNN94YPvfChQuDANyECRPC90jTiCoc6dq3337rVrNd6vmHDh1qSUlJplBWr169TO+L/5nTtI6alvC0004LA2DZDbxt27YMXdyUlhkOeDtmz57tbWW/6r521HPnzrTAUvZnFa0eW9elh6vKVSxne3elTx2qJ/GnD9y0KueV3jauTP/aq5FUPUTZs2OPjb7vIxvz0CfWsntzO+Lkw61V6rJ85fKmfte8cZk9dd4Ltjk559cKB2cFAQQQQACBgyigMJaCVap85ZoqYxXFqQj79evnHoFlERXo0KFDEb1zbhsBBBBAAAEEEEAAAQQQQAABBBBAAAEESrpA7BIiJV0lh8+vQJZeClHdcccdwbSFCte4Vw6HiWu3PoPvDMZbNG1C3MaN51h5uSm/GpEfglMA5rLLLguH1FRsixcvDrb9oEzYIWpl5MiREXv69+8fbuuab7zxRrjtryjI49+TQlyuTZ061a0GS4WGXPP7aZ/CQn6Lnkqwa9eu/uFwPbpaVXSgK+yYixVVdtqyZUt4hmz/8Y9/hNvxWonHM6pClT/l3qmnnhrenpu+0n/vNHVjhQoVwj7vv/9+uJ7TFVXR0pSH55xzjvXs2dOGDx8eEWzS5+28887L6XBBRS4/GLV371675JJLbPDgwVm+FOzLaWvSpElEYMxVCcvp+UWlnx94atalSYbbbnh4Urhv48/p4arjruhhQ0dfY1e+ekl43F+p1ahmuLlhRdp0hPVa1LUmnRtb/ZZ17Ze9v9j8CQvtnf8bbQ+f+LiN//dXQX9V5up4enqVvnAQVhBAAAEEECgCAgpide7cOdNKV5rCkKkIi8AbyS0igAACCCCAAAIIIIAAAggggAACCCCAAAIIHDQBKmLFif5ghq9iPcKiaRNNr/xWxXLj6Bp9Bt8R61KFuk9Vqm699VYrV65ccF231MY777yTq3sZP358UPEpVvWsRYsW2a5du2KOpxCQKl9pSrzo9tFHH0Xs0jR6mh6uTJkyEfu18fnnn0fs86c81IEBAwbYiy++GNFHU+KdcMIJEfuiz4s4mIONefPm2eTJk4PKTy+99FIY3jnmmGNM1aTcVIs5GCrbLtH3mpdnlL+mjWzatGmG640aNSrYp0De9u3bTYEyP5SnKQmnT5+e4bxYO7788svgfB1TNbaTTz456KYqWfqsffPNN+YH7VQt6+mnn441VFC5K/qAqrk1a9Ys2K3P8bXXXmtPPvlkdDc7OnXaTE2D6IfLojtpOskpU9Kn0tPxIUOGRHRbvmxZxHZx2Zj35QI748+nBY/T5bzOtuCrRRGP1uW8o8LtVXNWh+tbU7ZZtbpVg5cCXEu/Xx4e00rXAennJc9Nmw70jD+faoe0TbL9v+y3v/b+p+3ftz88Z9KIb6331b2C7Qa/TmUYHmQFAQQQQACBIiagaQevueaaiGpYRWkqwpz+fa+IvS3cLgIIIIAAAggggAACCCCAAAIIIIAAAggggEAREChdBO6RW8yFgIJXLnw1buTwXJwZu6sb4+SL0yptxe5VeHsVwlEAJrppv6aHy21TAClWe+utt2LtDvfNmjUrXHcrqpIVXeVKx5bFCMCo+lR00EsVCDR1n2t16tSxp556yhS+UlNg7L///W9EdSdVmMpvpaP58+cH42uqxrfffjtYd/+56667rHbt2m4z38t4PWOsz4CCTQpfuaZrRTcF7PymcJWeWcEqhc785qqraZ8M/Okote+4447TImyqmuWa7sVvLVu1Ct9Ht9+f9kf7Lr300ogKb3rfVSFLn4Gbb745Q7DKjaOlAm1umk1tazpMN32jttWef+GFtJUi+t/KNSvbhY/9zgY/PdCqN0ifKnD39j22en6ad4uuzaz7oLRKcqpMdezl3a350WmBPQW0dm5On55x7ufzw8p2Ax4911w1rbTzelib4w8LpDb+vNHWLU4J1md9MjdYqs+Fj51nlapXDLbLlCtj/e48NVjXf1ZMXxmus4IAAggggEBRFVDlK1XA0qsohbCKqjf3jQACCCCAAAIIIIAAAggggAACCCCAAAIIIFA8BKiIVTzex4in0PSEi6adGlTEGjfygTxXstK5qoillgjVsNxD6odB0SEYVSRTpaLcNlWcih5Loa7oQFL0uApcqQqR31atWhUxZZ479vXXX1uLFi3cZrB04aeInakbd955pz377LPhbk1PqKkQt23bZlWrVo2o7qTg12233Rb2jcfKgw8+GHjUq1cvGK5s2bLBD99yM+1edvcRj2dUcGrgwIERl4oOwSlMF/0eqcqVawo63XPPPWF1NYXOxo4dG76H//vf/0zTGrqm6UevuOIKS05ONvk0bNjQHQqWH374Ybi9adMm2717dxiaU6hOnwNVR3vggQeCKmOaRvHiiy+2Nm3aBOepcpeqWF199dXB+129evWI91tTF+qzoapesdoLqUEr9zUQXeVt4cKF4bSdsc4tCvs6nnGkNU8NWql1H3S0ffzoZ8G6/vPWn963If+73BSQOuG64+y3Q46NsFMFq08fHx/218rOLbtszIOf2Ol3nGJly5e1i544Pwhm+RXU9DX23t1jwvN+fGeadUiddrB+y3rW9KjGdtPH19sv+1Ir3pVNr3i3Y9MO+3FU5BSl4QCsIIAAAgggUMQEmIawiL1h3C4CCCCAAAIIIIAAAggggAACCCCAAAIIIIDAQRegItZBfwvifwOqiHXNI2ODgT8ZMdwUqMpt0zk6Vy1RqmG5Z9BUepoa0G/PP/+8v5njdVWB8qso6URVu1IYK6v2xRdfZDicWXWt0aNHZ+gbq6KTOmnqPoVtFABxTcGQatWqRQZLUu9PgZ6VK+NfeUdT5PnXb968eTBtnruf/C7j8YyqbKWgk99ef/11fzOonLZ3796Ife+++264rZCZXq5pXeEn1zQlox+K0/769esHlaeiQ1iffPJJhqkmVa3Mb5p+sGLFilapUqVw9/XXXx9MsxjuSF3RNJY1atSIeL/1ftx0002ZhrDc+QpgRYewdu7cGUzn6fok4tL/vB3I5Gtv5czk8HO57IflEY+xOXmzvXzla7Z+2YZgvx+mWr98gz3Z/3nbsGJjxDnamDp6ho26/T3bvnFHzPOePv8FWzkrOTxv76699vzFI2zm2Nm2Z0da8NMPYanq1nODX7E9OyM/d+EArCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIFGuB9BRCsX7MkvdwCmMpQKUwlV6Lpk1IrWp1ZzhtYWYiqoCl6QhdJSyNUdjVsKIr/vghDXff77//vv3+978PNhWkig42RQdwYo3hxlq7dq0pbORadKDH7feXmlpQ161SpUq4W/cUq2mKO786kvqMGzcuVtdgn8I/EydOtEceeSQI/vihEgXEVqxYYUOHDs0Qwop225dafSlWi+4XHTpbunSpadq8q666Kjz9sssuM7mo0lN0/+zGi2Wf12cMbyh1RVXFXMUqvRfLl0eGc9R39uzZ4ZR96qP32jVVj9LnpkePHsGuKVOmBM/njmup+9S41113XfBeKCTlt82bNwcVw958801/d7CuylavvvqqtUqdljCztnHjRjvnnHMC60GDBlnlypUjuspu+vTpQaU0f+rDiE6pG5q+UOf7QTKdq3u//PLLMzxX9Pnajn4fM/v8xDo3v/vmfjHfhnd/JMthlv+0wh456YnUqlelbNfWyBCeTkyes9qeGfiiVaxWweq1qGv79+1PnbJwrf2yN/bXgbvY3PELTC+dV79VfduzfbetWbjODuxPD0O6vlpq/3v3pFXJ0jmHtE2yLWu3BkGvzM7xz2cdAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQKD4CpRKrbQT86fNbro2hQBKQnNVi84444yEftyhL0ZW2cnuZv3KVuqrgFbLjscFpylg5QJXi6dPDMJablsdVFVL/XPTHr+sbW66J0TfUaNGWdOmTYN7UYCre/fuCXFf7iY0dZ2CYgoeKdRVHNvBfMbatWubpilMSUnJllZTTLZt2zYIdM2ZMydDNbVYA2hKyd/85jdBVbMlS5YEFddi9dM+VbNyfbOaTvChhx6yk046KRxGUxoqSKZgYJcuXYLgn7ajQ3PhCawggAACCCCAQLEW0HTIidyiw+25vVdN+5xVi66em1VfjiGAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEE8BKmLFUzMBx1LYSi8XyFLQyoWt3NSD0bet8FVOqmdFn1cUtzXFXJMmTcJb/+mnn8L1RFnRVIx6Fed2MJ9xw4a06exy4qsgXG7DcNu2bbMvv/wyJ8Obq9KVo84xOqnqV06vFeN0diGAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAPgQIYuUDryid6geydN+aqtAFslzVK4Wv1Nx2sFFM/6PKRu3atbNhw4aZP/WfpuSjIYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCQWwGCWLkVO8j9D9gBK5X6v7w2BbLU3DKv48Q6T/dWFNopp5xiw4cPz3CrK1eutESsiJXhRtmBAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggkHACpRPujrihLAU2r12e5fGDeTCR7813qVChgr8ZrO/bt8+GDBmSYT87EEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDIiQBBrJwoJVCfFbO/SaC7ibyVRL43/07Xr19vCl6p7dq1y2bOnGn9+vUzVcSiIVAUBObMmWOrV68OXykpKUXhtrlHBBBAAAEEECgkAX/q7UK6ZI4vk8j3luOHoCMCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAApkIMDVhJjCJunvG569Z+94DE/L2dG9FoX399dfWrVu3onCr3CMCMQVeeeUV04uGAAIIIIAAAgjEElDY6cCBxJw2nCBWrHeMfQgggAACCCCAAAIIIIAAAggggAACCCCAAALFRYCKWEXsnUz5eb79MPb5hLtr3ZPujYYAAggggAACCCCAAAIHV0Bhp0QMPCXqfR3cd4urI4AAAggggAACCCCAAAIIIIAAAggggAACCBQnAYJYRfDd/PrNR2zB92MT5s51L7onGgIIIIAAAggggAACCCSGQOnSpRMqjKUQlu6JhgACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAcRbgO+FF9N396KkbE6Iyliph6V5oCCCAAAIIIIAAAgggkFgCiRLGIoSVWJ8L7gYBBBBAAAEEEEAAAQQQQAABBBBAAAEEEECg4ATKFtzQjFzQAqpCNW/S+9b+hAutcbvuVqN+E0udhKRAL3vADtjmtcttxexvbMbnrzEdYYFqMzgCCCCAAAIIIIAAAvkTUBjrwIHUv8X/+srfaLk7201FqCUNAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGSIFC2QYMGwTflS8LDFsdnTPl5vn0x4u7i+Gg8EwIIIIAAAggggAACCMRBwAWi4jAUQyCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEAWAkxNmAUOhxBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBnAiU3bBhQ0760QcBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCATASpiZQLDbgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgpwIEsXIqRT8EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIBMBgliZwLAbAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEMipAEGsnEoVs36jR482vWgIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCQfwGCWPk3ZAQEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAo4QIEsUr4B4DHRwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgfwLEMTKvyEjIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQAkXIIhVwj8APD4CCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjkX4AgVv4NGQEBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQRKuABBrBL+AeDxEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIP8CBLHyb8gICCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggUMIFCGKV8A8Aj48AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAL5FyCIlX9DRkAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIESLlC2hD8/j48AAggggAACCCCAAAIIIIBAoQuUK1fOGjRoYHXq1LEaNWpY5cqVTftoCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAtkL7N2713bs2GGbN2+29evX25o1a0z7DnYjiHWw3wGujwACCCCAAAIIIIAAAgggUGIEqlevbs2bN7cmTZqUmGfmQRFAAAEEEEAAAQQQQAABBBBAAAEEEEAAgXgL6Bdb9Uuuernvty5fvtyWLFliW7ZsifflcjweQawcU9ERAQQQQAABBBBAAAEEEEAAgbwLtGvXzlq2bBkOsGnTJtNr69attnPnTtu3b194jBUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBzAXKli1rlSpVsmrVqlnNmjWDlwJZei1atMhmz56d+ckFeIQgVgHiMjQCCCCAAAIIIIAAAggggAAC+kZA586dg9/Mksbq1astOTk5CF+hgwACCCCAAAIIIIAAAggggAACCCCAAAIIIJB7Af1iq37JVa9Vq1YFoaxDDjnEkpKSgl+IrVu3rv3000/B8dyPnvczCGLl3Y4zEUAAAQQQQAABBBBAAAEEEMhSoHbt2ta1a1dTmext27YFZbH1jQEaAggggAACCCCAAAIIIIAAAggggAACCCCAQPwENOvA4sWLbd26dda8efPgF2N79uxpU6ZMsQ0bNsTvQtmMVDqb4xxGAAEEEEAAAQQQQAABBBBAAIE8CKgSlgthpaSk2PTp0wv9t6/ycNucggACCCCAAAIIIIAAAggggAACCCCAAAIIFFkB/SKsvher78nqF2T1PVp9r7awGkGswpLmOggggAACCCCAAAIIIIAAAiVKQNMR6h/6+gf//PnzS9Sz87AIIIAAAggggAACCCCAAAIIIIAAAggggMDBFND3ZF0YS9+rLaxGEKuwpLkOAggggAACCCCAAAIIIIBAiRFo165dUPpa0xESwioxbzsPigACCCCAAAIIIIAAAggggAACCCCAAAIJJKDvzep7tDVq1DB9z7YwGkGswlDmGggggAACCCCAAAIIIIAAAiVGoHr16tayZcvgeZcsWVJinpsHRQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEk3AfY9W37PV924LuhHEKmhhxkcAAQQQQAABBBBAAAEEEChRAs2bNw+ed/Xq1bZ169YS9ew8LAIIIIAAAggggAACCCCAAAIIIIAAAgggkEgC+h6tvler5r53W5D3RxCrIHUZGwEEEEAAAQQQQAABBBBAoEQJlCtXzpo0aRI8c3Jycol6dh4WAQQQQAABBBBAAAEEEEAAAQQQQAABBBBIRAH3vVp971bfwy3IRhCrIHUZGwEEEEAAAQQQQAABBBBAoEQJNGjQIHjeTZs22c6dO0vUs/OwCCCAAAIIIIAAAggggAACCCCAAAIIIIBAIgroe7X6nq2a+x5uQd0nQayCkmVcBBBAAAEEEEAAAQQQQACBEidQp06d4JndP+pLHAAPjAACCCCAAAIIIIAAAggggAACCCCAAAIIJKCA+56t+x5uQd0iQayCkmVcBBBAAAEEEEAAAQQQQACBEidQo0aN4Jm3bt1a4p6dB0YAAQQQQAABBBBAAAEEEEAAAQQQQAABBBJVwH3P1n0Pt6DukyBWQckyLgIIIIAAAggggAACCCCAQIkTqFy5cvDMTEtY4t56HhgBBBBAAAEEEEAAAQQQQAABBBBAAAEEEljAfc/WfQ+3oG6VIFZByTIuAggggAACCCCAAAIIIIBAiRMoV65c8Mz79u0rcc/OAyOAAAIIIIAAAggggAACCCCAAAIIIIAAAokq4L5n676HW1D3WbagBmZcBApCoHVSZbugRz3r1qq6NalToSAukWHM5et32+SFW+z1Sets/uodGY6zAwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABglh8BoqMwLDTD7XLjksq9PtV4KtJnXp2/jH17MUJq+3RD38u9HvggggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKJLUAQK7HfnwK/u1KlStmBAwcK/Dr5vcDfB7W0U9rXyu8w+T5fQbBGtSrYTa8uyvdYDIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCEQLVK5c2ZKSkqxmzZpWsWLF6MMJvb1r1y7btGmTrV692nbsKHmzjpVO6HeHmyswgRkzZgRjV6pUqcCuEa+BVQkrEUJY7nl0L7onGgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEE+Bpk2bWqdOnYIgVlELYclB96wQmZ5Bz1LSGhWxSto7HvW8derUSegEYuukygdlOsIopgybqow1+ocNNn91yUtvZsBgBwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjkW6BNmzamHIfaypUrLSUlxbZv357vcQtzgCpVqljdunWtUaNGwUvBrHnz5hXmLRzUa1ER66DyH/yL64OfyO2CHvUS9vYS+d4SFo0bQwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIEMAqoepRCWpvabNm2aLVu2rMiFsPRQCo7p3vUMehY9U0mqjEUQK8NHu2TsmDlzZvCgrVq1SugH7taqelzu76s5KXb6/RPtwVFz4zKeBonXvcXthhgIAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBIqcQOXKlYPqUbpxVY8qalWwYoHrGVwlLBUJ0jOWhEYQqyS8yzGeccaMGcHeI488MsbRxNnVpE6FfN+Mwld975toE2enpAax5lj1C9+JSyArHveW74djgGIjcNlll9lVV10VvI477rhi81w8CAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCA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AQSKlwAVsYrX+1ksn+bRD3+2RrUq2CntayXE8308Y6PpnmgIIIAAAggggAACCCCAQHEWcNWu/GpYmopQISwtaWkC1atXty1bIqeu79SpUwaeZcuXB/u2bt2a4VisaQtr1crbv4FLl+Z37jIAswMBBBBAAAEEEEAAAQQQQAABBBBAAIFCEuC7c4UEzWXyJ3DTq4sSojKWKmHpXmgIIIAAAggggAACCCCAQEkQUBhLUxW68JXWCWFFvvOPPfaYlS2b/ntu9957r1WsWDGyU+rWxIkTg3379++3vXv3RhzXFIRXXHFFuK9q1ar2+OOPh9uZrezYkbFSc/ny5a1u3bqZncJ+BBBAAAEEEEAAAQQQQAABBBBAAAEEEChAgfTvFBbgRRgagXgIqArV6B822AU96lm3VtWtSZ0K8Rg22zGWr99tkxdusdcnrWM6wmy16IAAAggggAACCCCAAALFTUDBK8JXmb+rhx56qI0ZM8Y2btxoClApCBXd5syZY2vXrg13b968OUNYasCAAXbKKadYmTJlrFq1amHfrFZ2795tu3btyhD8evXVV4P7UejroosuymoIjiGAAAIIIIAAAggggAACCCCAAAIIIIBAHAUIYsURk6EKXmD+6h12z9vLCv5CXAEBBBBAAAEEEEAAAQQQQACBHAqoolXt2rUz7f2vf/0r4thbb71l11xzTcQ+bdSsWTPDvux2JCcnW/PmzSO6KcylqlgKYtEQQAABBBBAAAEEEEAAAQQQQAABBBBAoPAEmJqw8Ky5EgIIIIAAAggggAACCCCAAAIIlCCBAwcOmEJY8+fPj3jqUaNGRVTIijj464bOzUl77bXXLKd9czIefRBAAAEEEEAAAQQQQAABBBBAAAEEEEAg7wIEsfJux5kIIIAAAggggAACCCCAAAIIIHCQBfbt25fhDvbu3RvuixVSilelKI396KOP2s6dO8PruZWtW7faXXfdZe+9957bFbG89NJLbeLEiRH73MaWLVts2LBhlpKS4nZluhw/frzdfffd5j+z6xzr2d0xlggggAACCCCAAAIIIIAAAggggAACCCAQfwGmJoy/KSMigAACCCCAAAIIIIAAAggggEAhCdxwww1ZXkkhqT59+mTZxx08+eST3WqOl2PHjjW9atSoYb169bLt27fbpEmTbM+ePVmOoeDUvffea2XLlrW2bdtamzZtTNMMfvfdd2Go6oILLshyDHdQ1+vbt28wHWGXLl2sSpUqQcWt77//3nVhiQACCCCAAAIIIIAAAggggAACCCBQDAWmTZsW8VQXX3yxjRgxImJffjc6duxogwcPzu8wJeZ8glgl5q3mQRFAAAEEEEAAAQQQQAABBBBAoKAENm/ebB9++GGuh1dFr5kzZwavXJ8cdYIqaCkURkMAAQQQQAABBBBAAAEEEEAAAQQQKDkCCmMpLKWmwFRBh6biHfQqbu8UUxMWt3eU50EAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAoEQI333xzoT3nyJEjC+1aRfVCBLGK6jvHfSOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgiUeIE+ffpYQYakVHVLgS+qYWX/UWNqwuyN6IEAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQMIKKCSll5umMF43qhAWLecCBLFybkVPBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQSVoDg1MF9awhiHVx/ro4AAggggAACCCCAAAIIIIAAAkVEYMKECVapUqXwbnfs2BGus4IAAggggAACCCCAAAIIIIAAAggggAACCJTdvn07CggggAACCCCAAAIIIIAAAggggAAC2Qjcf//92fTgMAIIIIAAAggggAACCCCAAAIIIIAAAgiUZIHSJfnheXYEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIB4CBLHiocgYCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggUKIFCGKV6Lefh0cAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIF4CBDEiociYyCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggkAeBXbt2BWdVqVIlD2cn9inumdwzJvbd5v/uCGLl35AREEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDIk8CmTZuC8+rWrZun8xP5JPdM7hkT+V7jcW8EseKhyBgIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCORBYPXq1cFZjRo1MldBKg/DJNwpehY9k5p7xoS7yTjfEEGsOIMyHAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACORXYsWOHrVy5Mujepk2bYhHGUghLz6KmZ9MzloRWtiQ8JM+IAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggECiCixbtswqVqxoderUsY4dOwbhpZSUFNu+fXui3nLM+1IAS9MRukpY69evNz1bSWkEsUrKO81zIoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCSswLx586xp06ZBiElBJhdmStgbzubGVAmrJIWwxEEQK5sPBYcRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECgMAQWX1q1bZ0lJSVazZs2gSlZhXPf/2bsTOJ2q/4Hj3xmDse/7buw7kS2SspSUvYiiVEqppBT9K4kIJS2/LK2KklJKCyVZQpR9Lfu+M3Yzxv9+77hn7vPMM2OWZ8wz5nN+r3HPPefcc89932dGP76+x1/3OHfunBw/flz279+fbrYjdNsRiOXWoI4AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQCoKnDlzRrZu3ZqKK+DWSRUITuqFXIcAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBAtQCAWnwQEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIJkCBGIlE5DLEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAECsfgMIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALJFCAQK5mAXI4AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEIjFZwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQSKYAgVjJBORyBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQ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EEAAAQQQQAABBBBAAAEEEEAAAQQQQAABTwEyYnl6cIYAAggggAACCCCAAAIIIIAAAqksMGbkq3IxMtKsInuOHFLY2p6xevWapo0KAggggAACCCCAAAIIIIAAAggggAACCCAQaAJkxAq0N8J6EEAAAQQQQAABBBBAAAEEEEjnAtu2bpGundpKuw5dpEzZMMmXP7/kyJlL9u/bm85leHwEEEAAAQQQQAABBBBAAAEEEEAAAQQQCGQBArEC+e2wNgQQQAABBBBAAAEEEEAAAQTSqYBmxPp62pR0+vQ8NgIIIIAAAggggAACCCCAAAIIIIAAAgikRQG2JkyLb401I4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEAJEIgVUK+DxSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBaFCAQKy2+NdaMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACASVAIFZAvQ4WgwACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAmlRgECstPjWWDMCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggElACBWAH1OlgMAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIpEUBArHS4ltjzQgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBBQAgRiBdTrYDEIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCQFgUIxEqLb401I4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEAJEIgVUK+DxSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBaFCAQKy2+NdaMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACASUQElCrYTEIIIAAAggggAACCCCAAAIIIIAAAgggkCiBrFmzSpGixaRQ4SKSMWMm+eP3XxN1PYMRQAABBBBAAAEEEEAAAQQQQAABBPwjQCCWfxyZ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mQMCBAgQIECAAAECBAgQqCkgEKumhyMCBAgQIECAAAECBAgQIECAAIFNEEj/oX7F8uVZC2/NmbMJLbmUQHUIXHLR+dUxUbMsK4E5c94sfJenQMHnn5sQN15/dbzx+mtlNQ+DJUCAAAECBAgQIECAQGsLCMRqbXH9ESBAgAABAgQIECBAgAABAgQqWGDhwgXxyY99uIJnaGoECBCofIFbb74x0ksiQIAAAQIECBAgQIAAgaYJtG9adbUJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoLaAQKzaIo4JECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQRAGBWE0EU50AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQK1BQRi1RZxTIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSYKCMRqIpjqBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqC0gEKu2iGMCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAg0UUAgVhPBVCdAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtAYFYtUUcEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoIkCHZpYX3UCBAgQIECAAAECBAgQIECAQMUJ7LP/AbFZ+83itdemx/RpU2PlihUVN0cTIkCAAAECBAgQIECAAAECBAgQIECgZQUEYrWsr9YJECBAgAABAgQIECBAgACBMhA48+xzol37doWRrlq5Mv5xx1/jql/+vFAmQ4AAAQIECBAgQIAAAQIECBAgQIAAgfUJ2JpwfTrOESBAgAABAgQIECBAgAABAlUp0KFjxzjsI0fGBw77SFXO36QJECBAgAABAgQIECBAgAABAgQIEGi6gECsppu5ggABAgQIECBAgAABAgQIEKgwgeOPPTJOPen4uOTC82LunDmF2R3zyeMKeRkCBAgQIECAAAECBAgQIECAAAECBAisT8DWhOvTcY4AAQIECBAgQIAAAQIECJSwwGmnnRb77LNPYYQXXHBBTJw4sXAs03iBJUuWRHrNfOP1mDZ1Svzk8iuzi3v06NH4RtQkQIAAAQIECBAgQIAAAQIECBAgQKCqBQRiVfXtN3kCBAgQIECAAAECBAgQKFeBkSNHxqGHHlpj+FtssUWNYwcbJ5ACsWLt2oh27SJtUSgRIECAAAECBAgQIECAAAECBAgQIECgMQK2JmyMkjoECBAgQIAAAQIECBAgQKDEBM4555wSG1FlDWfNmjWVNSGzIUCAAAECBAgQIECAAAECBAgQIECgxQUEYrU4sQ4IECBAgAABAgQIECBAgEDzCpx88snRp0+f5m1UawQIECBAgAABAgQIECBAgAABAgQIECCwSQICsTaJz8UECBAgQIAAAQIECBAgQKB1BQYMGBBHHHFE1um0adNi1apVrTsAvREgQIDETchUAABAAElEQVQAAQIECBAgQIAAAQIECBAgQIBAvQICseplUUiAAAECBAgQIECAAAECBEpT4Nxzz4127drF2rVr47zzzivNQVbYqDp37lxhMzIdAgQIECBAgAABAgQIECBAgAABAgRaQkAgVkuoapMAAQIECBAgQIAAAQIECLSAwOGHHx6DBw/OWr733ntjxowZLdCLJpPAypUrCxDbbDu8kJchQIAAAQIECBAgQIAAAQIECBAgQIBAQwICsRqSUU6AAAECBAgQIECAAAECBEpIoEePHnHiiSdmI1q+fHn85Cc/KaHRVd5QFi5cUJjUUUcfW8jLECBAgAABAgQIECBAgAABAgQIECBAoCEBgVgNySgnQIAAAQIECBAgQIAAAQIlJPDtb387OnTokI3oiiuuiFWrVpXQ6CpvKH+87ZbCpN6x977xjXPOjd333Cv69Nk8UlCcRIAAAQIECBAgQIAAAQIECBAgQIAAgdoCbz/BrV3qmAABAgQIECBAgAABAgQIECgZgb333jt23XXXbDxpO8J//OMfJTO2Sh3I3/9ye3Tp0iWO/NgnokfPnpGCsdIrn8449aSYNnVK/tA7AQIECBAgQIAAAQIECBAgQIAAAQIEwopYPgQECBAgQIAAAQIECBAgQKCEBdIqWGeddVY2wrVr18b5559fwqOtrKGNe+apeG3G9HpXH2vffrPKmqzZECBAgAABAgQIECBAgAABAgQIECCwyQJWxNpkQg0QIECAAAECBAgQIECAAIGWEzjjjDOia9euWQf3339/TJ06teU603JBYIcRI+PCSy4rHC9bujTu//c9Menll2LVypUxfbr7UMCRIUCAAAECBAgQIECAAAECBAgQIEAgExCI5YNAgAABAgQIECBAgAABAgRKVGD77beP973vfdnoVqxYET/60Y9KdKSVN6xTT/taYVKLFi6MUz73yVi+fHmhTIYAAQIECBAgQIAAAQIECBAgQIAAAQK1BQRi1RZxTIAAAQIECBAgQIAAAQIESkTgs5/9bGEk8+fPj6985SuF43xms83WbZF31FFHxT777BNLliyJyy+/PF/F+0YI9Ou3ZeGqG669ShBWQUOGAAECBAgQIECAAAECBAgQIECAAIGGBARiNSSjnAABAgQIECBAgAABAgQItLFA+/btCyPYcsstC6tjFQprZUaOHBnptXbtWoFYtWyaeti5S5fCJY8/8lAhL0OAAAECBAgQIECAAAECBAgQIECAAIGGBNY90W2ohnICBAgQIECAAAECBAgQIECAQJUJtGvXrjDjefPmFvIyBAgQIECAAAECBAgQIECAAAECBAgQaEjAilgNySgnQIAAAQIECBAgQIAAAQJtLPDDH/4wBgwYsN5RpDr57Ql/+9vfxpNPPhkrVqxY7zVOEiBAgAABAgQIECBAgAABAgQIECBAgEDzCwjEan5TLRIgQIAAAQIECBAgQIAAgWYRmDdvXqTX+lLahjCfpk6dGi+88EL+0PsmCLRrZxHxTeBzKQECBAgQIECAAAECBAgQIECAAIGqFPBUsSpvu0kTIECAAAECBAgQIECAAAECDQls1qFDtGv/n60JiwLdGqqvnAABAgQIECBAgAABAgQIECBAgAABAklAIJbPAQECBAgQIECAAAECBAgQIECgSOD4z51UOFq6dGkhL0OAAAECBAgQIECAAAECBAgQIECAAIH1CdiacH06zhEgQIAAAQIECBAgQIAAgRIXKN6acPXq1SU+2tId3v9c/NPo02fz6NW7d3Tp2rUw0EkvvVjIyxAgQIAAAQIECBAgQIAAAQIECBAgQGB9AgKx1qfjHAECBAgQIECAAAECBAgQKHGBI488ssRHWB7D22HHkeu2I/zPkJflVsO65KLzy2MCRkmAAAECBAgQIECAAAECBAgQIECAQJsLCMRq81tgAAQIECBAgAABAgQIECBAgEBbC0x59ZXo3LlzLFiwIOa+NSeeeuKxuPuuO9p6WPonQIAAAQIECBAgQIAAAQIECBAgQKCMBARildHNMlQCBAgQIECAAAECBAgQIECgZQS+fvp/tUzDWiVAgAABAgQIECBAgAABAgQIECBAoGoE2lfNTE2UAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLSQgEKuFYDVLgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgED1CAjEqp57baYECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECLSQgECsFoLVLAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC1SMgEKt67rWZEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQQgICsVoIVrMECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFSPgECs6rnXZkqAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQAsJCMRqIVjNEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQPQICsarnXpspAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQItJCAQq4VgNUuAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQPUICMSqnnttpgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQItJBAhxZqV7MECBAgQIAAAQIECBAgQIAAgQ0K7LTzLtF/wMCYOfP1mDFtWixcuGCD16hAgAABAgQIECBAgAABAgQIECBAgACBUhQQiFWKd8WYCBAgQIAAAQIECBAgQIBAlQic/rWzo//AgYXZrl2zNsaPfTouuei8WLRoUaFchgABAgQIECBAgAABAgQIECBAgAABAqUuYGvCUr9DxkeAAAECBAgQIECAAAECBCpYYG2srTG7du3bxeg99oyvn31OjXIHBAgQIECAAAECBAgQIECAAAECBAgQKHUBK2KV+h0yPgIECBAgQIAAAQIECBAgUMECXzn1pOjZs1cM2npwHH3sp2OX0btns03BWO3atYu1a2sGalUwhakRIECAAAECBAgQIECAAAECBAgQIFDmAgKxyvwGGj4BAgQIECBAgAABAgQIVLZA79694/LLL2/UJB977LG49NJLG1W3VCqtXLEi3pozO3s9O+6ZuPn//hYdOnbMhjd4yNCYNnVKqQzVOAgQIECAAAECBAgQIECAAAECBAgQILBeAYFY6+VxkgABAgQIECBAgAABAgQItK1A165do0+fPo0axDbbbNOoeqVcacGC+bFF337ZEIcMHSYQq5RvlrERIECAAAECBAgQIECAAAECBAgQIFBDQCBWDQ4HBAgQIECAAAECBAgQIECgtAXWt1Xf4sWLS3vwjRjdsqXLCrW6duteyMsQIECAAAECBAgQIECAAAECBAgQIECg1AUEYpX6HTI+AgQIECBAgAABAgQIECDwH4GFCxfGscceW9Eea2NtRc/P5AgQIECAAAECBAgQIECAAAECBAgQqFyB9pU7NTMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA6wgIxGodZ70QIECAAAECBAgQIECAAAECTRTo1KlTE69QnQABAgQIECBAgAABAgQIECBAgAABAm0nIBCr7ez1TIAAAQIECBAgQIAAAQIECNQSWJTbfjGfBg8Zms96J0CAAAECBAgQIECAAAECBAgQIECAQMkLdCj5ERogAQIECBAgQIAAAQIECBAgkAn06NEjfvOb30SvXr1i1apVMWvWrJgyZUrcdNNN8dprr1WE0sw3Xo8RO+2czWW//d8dv/7FZRUxL5MgQIAAAQIECBAgQIAAAQIECBAgQKDyBayIVfn32AwJECBAgAABAgQIECBAoEIE2rVrFwMGDIiuXbtGz549Y/jw4XHggQfGr371qzjllFMqYpa/++21EWvXZnPp1adP/PSK38T+B7w3+ufm3bNnr2jf3qOMirjRJkGAAAECBAgQIECAAAECBAgQIECgAgU8vazAm2pKBAgQIECAAAECBAgQIFC5AmtzQUrpVZxSgNYRRxwRZ599dnFxWebTiljnf+9b8ebMmVlA1taDh8TXzv5W/OKqG+Kam26LTx9/QlnOy6AJECBAgAABAgQIECBAgAABAgQIEKh8AVsTVv49NkMCBAgQIECAAAECBAgQKGOBZcuWxdy5c+Nf//pX/PGPf4w5c+Zks0krYh1zzDFx1FFHRQrESund7353/OEPf4iXXnopOy7XP15+8YWY/MrL0SM3x67dutWYhhWxanA4IECAAAECBAgQIECAAAECBAgQIECghAQEYpXQzTAUAgQIECBAgAABAgQIECBQW2DevHnxmc98pnZxLFy4MK666qq455574rLLLisEY5122pfj9NO/Uqd+uRR07tw5fn3tTdGxU6dsyGvXrI2HH7wvXnhuQixevDjGj3umXKZinAQIECBAgAABAgQIECBAgAABAgQIVJmAQKwqu+GmS4AAAQIECBAgQIAAAQKVJTB58uR44IEH4oADDsgmNmDAwLKe4LGf+VwhCCu3B2N89csnx7SpU8p6TgZPgAABAgQIECBAgAABAgQIECBAgEB1CLSvjmmaJQECBAgQIECAAAECBAgQqFyBRx99tDC57t27F/LlmNlxx5GFYT/z9JOCsAoaMgQIECBAgAABAgQIECBAgAABAgQIlLqAQKxSv0PGR4AAAQIECBAgQIAAAQIENiAwa9asQo127dpFhw7luwB2z969C3OZMH5sIS9DgAABAgQIECBAgAABAgQIECBAgACBUhcQiFXqd8j4CBAgQIAAAQIECBAgQIDABgRGjBhRqLFy5cpYtWpV4bicM/Pnzy/n4Rs7AQIECBAgQIAAAQIECBAgQIAAAQJVJiAQq8puuOkSIECAAAECBAgQIECAQOUJHHrooYVJzZ07t5CXIUCAAAECBAgQIECAAAECBAgQIECAAIHWExCI1XrWeiJAgAABAgQIECBAgAABAk0W+Pa3vx277rprg9edeuqpsfXWWxfOP/XUU4V8OWZqbKu4dm05TsGYCRAgQIAAAQIECBAgQIAAAQIECBCoUoEOVTpv0yZAgAABAgQIECBAgAABAmUhsO+++0Z6LVy4MF566aWYMWNGzJs3L4YNGxa777579OrVqzCPVOdnP/tZ4bgcM927dS8Me+HCBYW8DAECBAgQIECAAAECBAgQIECAAAECBEpdQCBWqd8h4yNAgAABAgQIECBAgAABAjmBnj17xp577pm96gNZtWpVfO9734u1ZbyK1E477xI9igLLJj47rr6pKiNAgAABAgQIECBAgAABAgQIECBAgEBJCgjEKsnbYlAECBAgQIAAAQIECBAgQOBtgalTp8aQIUOiXbt2DZI8/vjjcdFFF8XSpUsbrFOqJ7542tdi1K6jo0ePHtGzV+/CMJcsWhSLci+JAAECBAgQIECAAAECBAgQIECAAAEC5SIgEKtc7pRxEiBAgAABAgQIECBAgEBVCnzxi1/MgrBGjx6dbUe45ZZbRufOneO13BaFEyZOjEmTJsWaNWvK1mb0bntE/4EDa4x/zerVcdlPflijzAEBAgQIECBAgAABAgQIECBAgAABAgRKXUAgVqnfIeMjQIAAAQIECBAgQIAAgaoXSNsNjh07NntVGsZzE5/NAskWLVoY8+fNjVdeeTluu+WmWLliRaVN1XwIECBAgAABAgQIECBAgAABAgQIEKhwAYFYFX6DTY8AAQIECBAgQIAAAQIECJSywE9/dFEpD8/YCBAgQIAAAQIECBAgQIAAAQIECBAg0GiB9o2uqSIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1CsgEKteFoUECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBovIBArMZbqUmAAAECBAgQIECAAAECBNYrsHLlyux8hw4d1lvPSQIECBAgQIAAAQIECBAgQIAAAQIECBBoPYH8M9v8M9yW6lkgVkvJapcAAQIECBAgQIAAAQIEqk5gyZIl2Zy7du1adXM3YQIECBAgQIAAAQIECBAgQIAAAQIECJSqQP6Zbf4ZbkuNUyBWS8lqlwABAgQIECBAgAABAgSqTmD+/PnZnPv06VN1czdhAgQIECBAgAABAgQIECBAgAABAgQIlKpA/plt/hluS41TIFZLyWqXAAECBAgQIECAAAECBKpOYM6cOdmcBw4cWHVzN2ECBAgQIECAAAECBAgQIECAAAECBAiUqkD//v2zoeWf4bbUOAVitZSsdgkQIECAAAECBAgQIECg6gRmzpyZzXno0KGRX+q66hBMmAABAgQIECBAgAABAgQIECBAgAABAiUkkJ7VDho0KBtR/hluSw1PIFZLyWqXAAECBAgQIECAAAECBKpOYOXKlTF16tRs3sOGDau6+ZswAQIECBAgQIAAAQIECBAgQIAAAQIESk0g/6w2PbtNz3BbMgnEakldbRMgQIAAAQIECBAgQIBA1QlMnjw5m/OoUaOiZ8+eVTd/EyZAgAABAgQIECBAgAABAgQIECBAgECpCKRntDvssEM2nPyz25Ycm0CsltTVNgECBAgQIECAAAECBAhUncCCBQti0qRJ2bx32223qpu/CRMgQIAAAQIECBAgQIAAAQIECBAgQKBUBHbZZZdsKOmZbXp229JJIFZLC2ufAAECBAgQIECAAAECBKpOYOLEiTF//vwYMGBAjB49uurmb8IECBAgQIAAAQIECBAgQIAAAQIECBBoa4Fdd901+vXrlz2rTc9sWyMJxGoNZX0QIECAAAECBAgQIECAQNUJPP3007Fy5cps2WvBWFV3+02YAAECBAgQIECAAAECBAgQIECAAIE2FEhBWNtss032jDY9q22tJBCrtaT1Q4AAAQIECBAgQIAAAQJVJbBw4cJ47LHHCsFY73rXu6Jnz55VZWCyBAgQIECAAAECBAgQIECAAAECBAgQaE2B9Ax23333LQRhpWe06Vlta6XNOnTo8L36Ottuu+2y4pkzZ9Z3WhkBAgQIECBAgAABAgQIEGh1gW7duq23z6VLl673fGufTONJ/67efPPNo2/fvjF8+PBYu3ZtLF68OFatWtXaw2nR/jbbbLMWbV/jBAgQIECAAAECBAgQIECAAAECBAgQaEiga9eusf3228eYMWMiPUeeP39+9kPZ9N6aqUNrdqYvAgQIECBAgAABAgQIECBQbQLp11b33Xdf7Lzzzlkg1qhRoyK9pk6dGm+88UbMmzcvUsBWpQVmVdt9Nl8CBAgQIECAAAECBAgQIECAAAECBFpPILfwVKTgqz59+kT//v1j0KBBhc4nTZoUEydOLBy3ZkYgVmtq64sAAQIECBAgQIAAAQIEqlYg/cN/+vTpse2228bQoUMLr6oFMXECBAgQIECAAAECBAgQIECAAAECBAg0o0D68evkyZNjwYIFzdhq05oSiNU0L7UJECBAgAABAgQIECBAgMBGC6QHAGPHjs1+jTVgwIBsu8LevXtnS2V37Nhxo9t1IQECBAgQIECAAAECBAgQIECAAAECBKpJYOXKlbFkyZJsC8I5c+bEzJkzI5W1dRKI1dZ3QP8ECBAgQIAAAQIECBAgUHUC6YFAWh0rvSQCBAgQIECAAAECBAgQIECAAAECBAgQqAyB9pUxDbMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA2wkIxGo7ez0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAhAgKxKuRGmgYBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAm0nIBCr7ez1TIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAhQgIxKqQG2kaBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0nYBArLaz1zMBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhUiIBCrQm6kaRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0HYCArHazl7PBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhUiIBArAq5kaZBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDbCQjEajt7PRMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCECArEq5EaaBgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECbScgEKvt7PVMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECFCAjEqpAbaRoECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECLSdgECstrPXMwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFSIgEKtCbqRpECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQdgICsdrOXs8ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFSIgECsCrmRpkGAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQNsJCMRqO3s9EyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQIQICsSrkRpoGAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJtJyAQq+3s9UyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQIUICMSqkBtpGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQItJ2AQKy2s9czAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVIiAQq0JupGkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINB2AgKx2s5ezwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVIiAQKwKuZGmQYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA2wkIxGo7ez0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAhAgKxKuRGmgYBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAm0nIBCr7ez1TIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAhQgIxKqQG2kaBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0nYBArLaz1zMBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhUiIBCrQm6kaRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0HYCHdquaz0TIECAAAECBAgQIECAAIHqFGjfuUN0H94/ug7tG53794qOfbpG+y4dqxPDrAkQIECAAAECBAgQIECAAAECBAgQINBEgTXLVsbKeUtj+awFsXTqnFg8aVasWb6qia00f3WBWM1vqkUCBAgQIECAAAECBAgQIFCvQOcte0bvMdtEr9GD6z2vkAABAgQIECBAgAABAgQIECBAgAABAgQ2LJB+2Np5YHr1KjxvXTBuesx/8tVY/ubCDTfQQjUEYrUQrGYJECBAgAABAgQIECBAgECxQL/3jow+79y2ULRo+uxYOHV2LJk1L5bPWxyrl60onJMhQIAAAQIECBAgQIAAAQIECBAgQIAAgYYFNuvSKTr36R7d+veJnkP7RY/B/bKArPQj2HmPTY7Z9z7f8MUteEYgVgviapoAAQIECBAgQIAAAQIECHTKrYI14LDR0XlArwzjrQlTY/b43K+ycsFXEgECBAgQIECAAAECBAgQIECAAAECBAg0XSD9sHXJG+k1N2aPm5wFZfXbdZvYYtTQ7AexXYf1jZl/GxcrWnl1LIFYTb+XriBAgAABAgQIECBAgAABAo0S6Dp489jqo2MiLZO99M358doDz8WSmXMbda1KBAgQIECAAAECBAgQIECAAAECBAgQINA4gfTD1xn3T4i5L74Wg961U3Qd0DsGf3LveP0PT8bS6a33TLZ944arFgECBAgQIECAAAECBAgQINAUgbQSVj4Ia/7Lr8fLtz0kCKspgOoSIECAAAECBAgQIECAAAECBAgQIECgiQLph7DpWWx6Jpt+IJue0aZnta2VBGK1lrR+CBAgQIAAAQIECBAgQKCqBNJ2hOkf+ukf/FP/+UxVzd1kCRAgQIAAAQIECBAgQIAAAQIECBAg0JYC6ZlsPhgrPattrSQQq7Wk9UOAAAECBAgQIECAAAECVSPQ770jo/OAXtl2hIKwqua2mygBAgQIECBAgAABAgQIECBAgAABAiUkkJ7NLn1zfvasNj2zbY0kEKs1lPVBgAABAgQIECBAgAABAlUj0Dm3zHWfd26bzfe1B56rmnmbKAECBAgQIECAAAECBAgQIECAAAECBEpNIP+MNj2zTc9uWzoJxGppYe0TIECAAAECBAgQIECAQFUJ9B6zTTbftyZMjSUz51bV3E2WAAECBAgQIECAAAECBAgQIECAAAECpSSQntGmZ7Up5Z/dtuT4BGK1pK62CRAgQIAAAQIECBAgQKCqBNp37hC9Rg/O5jx7/KtVNXeTJUCAAAECBAgQIECAAAECBAgQIECAQCkK5J/Vpme36RluSyaBWC2pq20CBAgQIECAAAECBAgQqCqB7sP7Z/NdNH12LJ+3uKrmbrIECBAgQIAAAQIECBAgQIAAAQIECBAoRYH0rDY9s00p/wy3pcYpEKulZLVLgAABAgQIECBAgAABAlUn0HVo32zOC6e+/Y/6qgMwYQIECBAgQIAAAQIECBAgQIAAAQIECJSgQP6Zbf4ZbksNUSBWS8lqlwABAgQIECBAgAABAgSqTqBz/17ZnJfMmld1czdhAgQIECBAgAABAgQIECBAgAABAgQIlKpA/plt/hluS41TIFZLyWqXAAECBAgQIECAAAECBKpOoGOfrtmcbUtYdbfehAkQIECAAAECBAgQIECAAAECBAgQKGGB/DPb/DPclhqqQKyWktUuAQIECBAgQIAAAQIECFSdQPsuHbM5r162ourmbsIECBAgQIAAAQIECBAgQIAAAQIECBAoVYH8M9v8M9yWGqdArJaS1S4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlUjIBCram61iRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0FICHVqqYe0SIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFSFejTvVcMH7RNDNi8X/To2r1Uh2lcBAiUiMCipYtj5tzZMem1V2Pe4gUtOqpy/n5qTacWvQkb2bhArI2EcxkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlKfA6O12jpFDhpfn4I2aAIE2EUgBm+k1fNCweH7apBj3ysQWGUe5fz+1llOL4DdDowKxmgFREwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQHgL77rxXDNlyq2ywM2bMiNmzZ8fixYvLY/BGSYBAmwl07949+vXrF1tvvXUWyNm9S7d4eOITzTqeSvh+ag2nZkVv5sYEYjUzqOYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoDQF0kozKQhr2bJl8cILLwjAKs3bZFQESlIgBWymVwreHDFiRPZdsjj3ndJcK2NVyvdTSzuV5IejaFDti/KyBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgIgX6dO9V2I5QEFZF3mKTItAqAinQKH2HpJS2OE3fLZuaKvH7qSWcNtW5Na4XiNUayvogQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgTYVGD5om6z/tB1hChCQCBAgsLEC6TskfZeklP9u2di2ituotO+n5nbaFOPWulYgVmtJ64cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE2kxgwOb9sr7TtmISAQIENlUg/12S/27ZlPbybeTb3JS2Su3a/Jzycyy18TX3eARiNbeo9ggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECg5AR6dO2ejclqWCV3awyIQFkK5L9L8t8tmzKJfBv5NjelrVK7Nj+n/BxLbXzNPR6BWM0tqj0CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBKpOQCBW1d1yEyZAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoLkFBGI1t6j2CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoOgGB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bH31lNOKT8e4556NT536uRpl78z5fePLX69R9sasmXHQJz4U5/73d6JPr941zuUPUpBT/rq77rs7vvrds+OkT38+PvT+Q/NVsveeuUCvU447ITp26Fgo3y0XZJUCyVJg2fGnnxwTX3yucC5lzjr1q7HHLjWXnu+e23ril9dfVaNeff399e474uzzvx3f+eo3a9QtPthphxG5AK0RWdHst+bEHf/6R/FpeQIECBAgQIAAAQIECBBookAKrLrwwgujT58+hSs/8YlPZPlnn302zjrrrMgHZKU6P//5z6N9+/bZ+TvvvDNSwElxSqvWXHLJJYWia665Jo455phI2xIWp8022yx+8YtfZEUp8Cv1IxGoBIGGgrDyc8sHXeUDlfLv+fJ8vXJ+zwedCZxqm7v49jd02/StVwIECBAgQIAAAQIECBAgQKCNBJ791xNx9Y9/Gek9BWRtakpt5Ns79XOnbGpzjbo+repUezWstWvXxo9+9bMGr1+zdk3849931zk/dOvBdcrauuBLn/9CjSCs4vGkrRZ/d8W12QpVxeXyBAgQIECAAAECBAgQIFA+AocffngWDFUchFU8+l122SVuuumm6N69e1Y8d+7cuPHGGwtVDjnkkBg0aFDhOGXOOeecwvHMmTPjt7/9beFYhkClC9Re/Slt05dfBat47ino6vrrry8UpQDGSktp7sVzbOr8kltqo5IC1JpqsLH1rYi1sXKuI0CAAAECBAgQIECAAAECZSpQO/AqBVBdfu2v4vJrfrVRM0qBV6d+dl3wVcpvbFtNGcAeu+5ep/ripUti7ry5dcqLC+596P5Iq14Vp75b9C0+LIt8+3bt44JvfDf+ef89ZTFegyRAgAABAgQIECBAgACBdQK9e/eOU089tfADoxkzZkRaveqtt96KD3zgA/H+978/0qpVKUgr1fvhD3+YXXzttbkf5Rx0ULbNYPpx0rnnnhsnnXRSdu7EE08srKyVfqj0jW98Iys//fTTo1evXjFgwIA4++yzs7K03X0Kskjp9ddfz979QaDcBVLwUAo+SoFV6b2+IKz8HPMBRqlufvu9/LlKeU9zzM+zUuZUDvMQiFUOd8kYCRAgQIAAAQIECBAgQIBAMwqk7QjTqzggKx9I1dQAqtpBWGmYKairNdIuI3au082ChQvqlNUueGXK5NpFkVaYSoFNacWs5kjf/eF5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fliUT4ccckjstNNO+cP4zne+E9dee2289dZbWVkK0Dr//PML5/OZxYsX57PRpUuXQl6GQKUIpC0G00pY+dRQMFZxEFaqa2vCvJj35hAQiNUcitogQIAAAQIECBAgQIAAAQIVKJACrtJWhWkFrHwQVnovpXT2BefE6tWrawypfbv2cfVPfhnHffxTkVaQSmn4sO3imlzZgfu/p0bddPDg4w/Hi6+8VCgfO6HuryC36j8w9thlt0Kd1N4njvh44bihzMJ6VqoaMmhwpDFuKKWH7AcdcGChWurzi8efXDjOZya88Fw+m5vHy4V8PvP+A94XPbr3yB/GER/4cL3BaIUK/8ksXbqsdlHsuevudcoUECBAgAABAgQIECBAgEDTBVLwVH6V5549e2YrWn3tq1/NthU84YQT4oorroiTT37734CjR4+O9Eqpe/fuccYZ61aCvv/+++Oll97+N+33v//9wkBSoFbaorA4LViwIFIQWErp35znnXdeHHXUUXHggev+7VlcX55AOQpcd9116w3Gqh2ElbYkFIhVjne6dMe8Lmy2dMdoZAQIECBAgAABAgQIECBAgEAbCaTAq1ILviqmWLBwQVxzyw1x4ic/W1ycBTqd/aWvRXqlQK36VsFKF6RzZ5//7RrXvprbpjA9DK+9HeF1P70yZr45K/rkto3o0rlxvxy+7+EHItY9H8/66dmjZzz6t3/H/NwD8ElTXokv/PdpNfovPvjJuf8by5Yvy209uDj6br5FnTGluhdc+r+FSya8MLGQz2e6dukaD95+d7w5Z3Zs2bdfgxb5+vn3J8Y9FfvutXf+MHt/5x57xQO5ttKD+9/d/vv45fVX1TjvgAABAgQIECBAgAABAgQaJzBz5sy47LLL4stf/nK0b98+22bwg4cdFulVO914443x1FNPZcUXXHBBYUvCFStWxIUXXlioPnHixHj44Ydj3333zcrSFoUPPPBALFq0bjv76dOnx7bbbpud32effSK9Xn755bjnnnsK7cgQKHeBFIyVUloRq/g9BTQWb18oCCvj8UczC2z455fN3KHmCBAgQIAAAQIECBAgQIAAAQLNKXDpr38ez730QoNNNhSElYKtLvzZJTGvnu38nhj79gPu4kZTYNbA/gMaHYSVrn191huxctXbvzYubisFR6W20upYG0op6CttjVg7MCxdl1bySsFc+fSvB++LRUvWbTWRL08Gqb+GLPL1it9TW/WlPr16ZwFdgwZsVd9pZQQIECBAgAABAgQIEKhagVWrVhXmvmbNmkK+ocyf//znOP7442Pq1KlRX/05c+bEWWedFVdffXXWxJgxY2LUqFGF5i699NJIwVjFKQVq5Ve9SlsUpm0Pi9PXvva1mDdvXnFRvf/erFHBAYEyFKhvZSxBWGV4I8twyFbEKsObZsgECBAgQIAAAQIECBAgQIDAOoE1a9fE0ad8Ok797Cnxxc+e3KgHyCn46vNnnBIvTZ60rqGi3Ne+d3bce9ud6w1cWrFyRXTq2Knoqvqzf7rzr/GxDx1Z78n0q+eNTa/NfD2OP73mVoXJ4pyLvh8//v66VbLqa7++Fb9q10tBXlNyq4MNGzy09qnsuL7AsHorKiRAgAABAgQIECBAgECVCLz++utx8MEHN2m2aWWsE088Mbtm2LBhMXz48HjzzTfjueeei+LArlThySef3GD7y5cvj8PqWVUrP6i0OtbRRx8dQ4YMiREjRkSqP27cuPxp7wQqSqD2ylj5yVkJKy/hvSUENv5pX0uMRpsECBAgQIAAAQIECBAgQIAAgY0UuPzaX8XHTvpUtjrWkqVL6rSSgpTmzJ0Tf7nrb/Huow5uMAgrXTh3/rw47DNH1VhtKt9gCmKaPPXVeP/Rh2VbGObL03vqo3b67sXnxw233VTvuVWr1/1auvZ1d977z2wutdtMD+IfffrxOPRTR+S2LFy3vUT++rvuuyfb7jBt21g7pWv/9dC/IwWa1U61H/Cn8x/57Mdj/PMTalfNjvO/sK73pEICBAgQIECAAAECBAgQaLLAlClTsi0Cx48fXycIq8mNbeCCadOmxT//+c+4//77Y/78+Ruo7TSB8hUoXhlr7NixIQirfO9luYzciljlcqeMkwABAgQIECBAgAABAgQIENigQFrFKa2OlVLnTp1i/3fsG4MHbR2PPf1EPP/yixu8vrjCjDdeiyM+d0z06N4j9tx199hu2LYxdsK4ePrZsYVqux74jkJ+fZm0BWJ67bDt8NhnzN6xOheAlYK5Hs2Nq6G0YsXywlzGjN4jRo3YKR587JF6g8Nqt/Hg4w/HfocfGAO27B97jd4zNu+zeTzw6IPxam6Fq3za5X175bMNvqetMT75xc9GWrlrr932zI1h51i4aGFMeGHiereDbLBBJwgQIECAAAECBAgQIECAAAECrSyQgrHyq2O1ctct3l0KLitOabvT5p5r2tLxuOOOK+5Gfj0CArHWg+MUAQIECBAgQIAAAQIECBAgUL4Cy1esiHse/PcmTyCtOnXfIw9kr01tLG2F2NB2iOtr+8lxT0d6NTXNfHNW/PXuO5p6WZ36KSArBbOll0SAAAECBAgQIECAAAECBAgQIFA6AikYKwVLpZQCplo6aKq5A71KR7J5RmJrwuZx1AoBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBVhVI2y22Vrr++utbq6uy7UcgVtneOgMnQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCodoGDDz44WjJIKq26lQK+rIa14U+arQk3bKQGAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZIVSEFS6ZXfprC5BpqCsKTGCwjEaryVmgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRKVkDgVNveGoFYbeuvdwIECBAgQIAAAQIECBAgQIBA/PO+e6JXj541JO5+4N81jh0QIECAAAECBAgQIECAAAECBAgQIFDaAgKxSvv+0Zbj8QAAQABJREFUGB0BAgQIECBAgAABAgQIECBQBQJ35QKx0ksiQIAAAQIECBAgQIAAAQIECBAgQKB8BdqX79CNnAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAqUhIBCrNO6DURAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUMYCArHK+OYZOgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECpSEgEKs07oNRECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVKHAoqWLs1l379694mafn1N+jhU3wVoTEohVC8QhAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFSeQD4IIB8UUHkzNCMCBFpTIP9dkv9u2ZS+Z86dnV3er1+/TWmmJK/Nzyk/x5IcZDMOSiBWM2JqigABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRKUyAfBJAPCijNURoVAQLlIpD/Lsl/t2zKuCe99mp2+dZbbx35AK9Naa9Urk1zSXNKKT/HUhlbS41DIFZLyWqXAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBEpGIB8EUGmBDiUDbCAEqkiguQOM5i1eEM9Pm5QJjhgxoiKCsZJRmktKaW5pjtWQBGJVw102RwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFS5QCUGOlT5LTV9Am0i0FIBRuNemRjT3nw9unTpErvttlsMGzasLAOykk8ae5pDmkuaU5pbtaQO1TJR8yRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB6hZIwQDdu3SLIVtulQUJzJgxI2bPnh2LFy+ubhizJ0BggwIpwChtR5jfaq8lAowenvhELN5u5xg5ZHjWT76vDQ6uRCuklbCqKQgr3QaBWCX6YTQsAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGh+gUoLdGh+IS0SILAhgZYMMEqBS1NnTo/hg7aJAZv3ix5du29oOCV1ftHSxTFz7uxI28FWy3aExTdAIFaxhjwBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVLxAuQc6VPwNMkECJSjQmgFGKYDpyZfGlaCCIW1IQCDWhoScJ0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGKExDoUHG31IQIECDQ5gLt23wEBkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEyFxCIVeY30PAJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGh7AYFYbX8PjIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgTIXEIhV5jfQ8AkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaHsBgVhtfw+MgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBMhcQiFXmN9DwCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBoewGBWG1/D4yAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEyFxCIVeY30PAJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGh7AYFYbX8PjIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgTIXEIhV5jfQ8AkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaHsBgVhtfw+MgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBMhcQiPX/2bsLMEmqawHAl8UluLu7uxM0EAge3DXwIGgIkABBggUnLE4WCRISCMHdCe7uTvDFfXl1aqmamt6emd2dmZ6Z3v/yDV1dduv+JV3bdfrcPr4DbT4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAj0vIBCr5/eBLSBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoI8LCMTq4zvQ5hMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0PMCArF6fh/YAgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE+rjAKH18+20+AQIECBAgQIAAAQIECBDodQLz/Ga1XrdNNogAAQIECBAgQIAAAQIECBAgQIAAAQIEuldARqzu9bV2AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRGAAEZsUaAnayJBAgQIECAAAECBAgQINBYgSdOu7axFaqNAAECBAgQIECAAAECBAgQIECAAAECBNoVaERPBjJitbsLTCRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDHAgKxOjYyBwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNoVEIjVLo+JBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6FhAIFbHRuYgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAuwICsdrlMZEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIdCwjE6tjIHAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGhXQCBWuzwmEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoGMBgVgdG5mDAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC7QoIxGqXx0QCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0LCAQq2MjcxAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBdAYFY7fKYSIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgY4FBGJ1bGQOAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQItCsgEKtdHhMJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQsYBArI6NzEGAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIF2BQRitctjIgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBDoWEIjVsZE5CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0K6AQKx2eUwkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAxwICsTo2MgcBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTaFRCI1S6PiQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOhYQCBWx0bmIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQLsCArHa5TGRAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECHQsIxOrYyBwECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBoV0AgVrs8JhIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBjAYFYHRuZgwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAu0KCMRql8dEAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIdCwgEKtjI3MQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgXQGBWO3ymEiAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGOBQRidWxkDgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECLQrIBCrXR4TCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0LGAQKyOjcxBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBdgUEYrXLYyIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQ6FhCI1bGROQgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINCugECsdnlMJECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQMcCArE6NjIHAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE2hUYpd2pJhIgQIAAAQIECBAgQIAAAQIEOilw6D4HpMUWXLTuWvY7/MD00OOP1J1WHdkV66iuz3Brgd233yWtsNRy+cg33n4z/d/+e7SeoQ+/W2e1NdOm626Yt+DcS/+errzhmoa0ZqrJp0yH7nNgXtejTz2eTjq7f0PqVUmLwD/OuCCNP+54LSN+Gho0aFBadZO1hhhfOyL24d9OOL12dP7+hZdfbKrzpG4jKyPnnWPudOCe++Vj7rj37j59PM803YzpqD8emrfluZdeSH848k+VlhokQIAAgeEROPHQY9LPxh4nffbF52m3A/YenlVYhkDDBM498cw09lhjpU8/+yxts+dvGlZvZyvqN1K/NPWUU6V33383ffPtt51dneUJECBAoBsFBGJ1I65VEyBAgAABAgQIECBAgACBnhK4sP+ANPUUU+bVH37SMem6W2+ouyk7bbF92nidX+fTHnzskbTnn35fd77OjJxvznnTlJNNUXcV0041zVAFYnXFOupuQCdHnnbUSWnOWWfP13Lm3wek8/95YSfX2PHi1114RRprzDE7njGb46VXX0lb77Fjh/MuvuAiacbpZsjnm2KyyTucvy/NsOj8C6XZZ54t3+SF512wYYFY0009bVp0gYXzeqfKzkWBWI0/amadYeY0yijD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PfWO5a46X6t1D++xUV1H9fO0u65V1foMEyBAgEDnBEYaffTR63zUpbTSSivla3788ZYvTjpXlaUJECBAgAABAgQIECBAgEDnBCaaaKJ2V/Dhhx+2O727J868z2p5FU+cdm13V9Vj649fWC+6wMJ5l4TxsOfl117NuvZ6KkV2n75Uppp8yjwLwEzTz5CefO6ZPKiirYCQvtSuEXlb4xfvkd0hjs9nsm6XomuSyOoxrKWZjo3oNmjUzKXoKm/6LFvaVT91XRmBHsuvv+qw8vTI/JF1Y45ZZ8+7lJki697m9bfezLvdjG5Q6wUS9chGdlBpdAF29nGnlnPFQ+LVNl27z107ywZ0YiCyYcV5Om+W+eqLL79MN95+U6tglU6suk8vevd/bi4zF0U2rFPPO3OY2hMZEOMaOG/WZXBk+osuCqtBQB2tLD7fl8qyqcydZfh78LFH0i1339at3TF2tD09PT0+UxaZb8HsurNwHvhy78MPpCeefrJ8eN/T2zc89cdnwKILLpJmmm7G/NoT928ddSftfB0e6baX6devX1pwnvnzv+hiLLKitZWtru21pHwfzjPHXHlXZWOPNVZ6OetO9Nks62UEkAxtie2IrJnTZF1pRgDri6++nJ7Mlh+W68bQ1tXWfKOPNlpaLOuuc/pppk1TTT5V1pXgwPT8yy+kh7PP9754Xz7RBBOm2/51fRn0vkYWKDusnp29lk871TRZd4RLpbhfujELeKsGx7e1H3rr+K64Dsd9fWQyjWzIEYw2NEHOhUdXna+xvq44Nort8kqAAAECnReY5zeDv7998eju+/5WIFbn95M1ECBAgAABAgQIECBAgECDBARiNQhaNQQINKXAb7fduewa7onsAfzGWQY4pTECh+5zYKsuee59+P4UXUkqBEIguiiLbpSiRJaORVZdergCSfMV+B8BAgQI9IjAEfsdXHazGJnFVtnoVz2yHSrtfQKOjd63T2wRAQIjtkAjArF0TThiH2NaT4AAAQIECBAgQIAAAQIECBAg0CQCc846R9pv173T+OOON0SLpppiyrTlBpuW4y+98rJy2ED3C0QGqKJENqw/HPmn4q3XEVxg0oknTUfuf0ipEF1LDU82v3IFBggQIECg4QILzbtAWmPlX5b1RneACoEQcGw4DggQIDBiCowyYjZbqwkQIECAAAECBAgQIECAAAECBAg0l8D8c82TNl13o7TJOhum1956Pb3w8ovp8y++yLr7mTItPP+CKbr4i/K/995Nl11zRXM1vhe3JtynmGzycgvvvO/u9O7775XvDYx4AnFOnvznY/OukqaYtOXYiCC9w44/csQD0WICBAj0QYEIvNpmo83TZJNMWnYtG8344ssv0oWX/6MPtsgmd5WAY6OrJK2HAAECfVdAIFbf3Xe2nAABAgQIECBAgAABAgQIECBAgMAQAiONNFKafurp8r/aiQM//STttO9va0d7340CY405Znry2afTKKOMkgb98EP6w1EHd2NtVt0XBOKh/awzztJqUyMIa78jDkqvvvl6q/HeECBAgEDvFFhw7vmGuJZ/8+03acMdN++dG2yrGibg2GgYtYoIECDQawUEYvXaXWPDCBAgQIAAAQIECBAgQIAAAQIECAy9wO333pUWzLrGiYc/E44/YR74E0t/9/136cOPPkp33HdXlm3nqDTox0FDv1Jzdlrg8ywzxiY7b9Xp9VhB8wh89vln6etvvs4b9PU336Snn38mnXfphemu++9pnkZqCQECBJpc4H/vv1teywd+8kl65MlH08nnnJZef+uNJm+55nUk4NjoSMh0AgQINL/ASKOPPvqP9Zq50kor5aMff/zxepONI0CAAAECBAgQIECAAAECDReYaKKJ2q3zww8/bHd6d0+ceZ/V8iqeOO3a7q7K+gkQIDBUAtEtnsCroaIyEwECBAgQIECAAAECBAgQINDkAvP8ZvD3ty8e3X3f3/ZrckPNI0CAAAECBAgQIECAAAECBAgQIDDCCgjCGmF3vYYTIECAAAECBAgQIECAAAECPSAgEKsH0FVJgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBzCQjEaq79qTUECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECPSAgECsHkBXJQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECzSUgEKu59qfWECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQAwICsXoAXZUECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECDSXgECs5tqfWkOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQA8ICMTqAXRVEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQXAICsZprf2oNAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQI9ICAQqwfQVUmAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQHMJCMRqrv2pNQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI9ICAQKweQFclAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLNJSAQq7n2p9YQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINADAgKxegBdlQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQINJeAQKzm2p9aQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBADwgIxOoBdFUSIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBcAgKxmmt/ag0BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAj0gIBCrB9BVSYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAcwkIxGqu/ak1BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAj0gIBArB5AVyUBAgQIECBAgAABAgQINKfAoK+/yxs28hijNWcDtYoAAQIECBAgQIAAAQIECBAgQIAAAQJ9UKD4zrb4Dre7miAQq7tkrZcAAQIECBAgQIAAAQIERjiB7wZ+lbd59PHHHuHarsEECBAgQIAAAQIECBAgQIAAAQIECBDorQLFd7bFd7jdtZ0CsbpL1noJECBAgAABAgQIECBAYIQT+Oa9T/M2jzXp+CNc2zWYAAECBAgQIECAAAECBAgQIECAAAECvVWg+M62+A63u7ZTIFZ3yVovAQIECBAgQIAAAQIECIxwAl+9/mHe5p9NO/EI13YNJkCAAAECBAgQIECAAAECBAgQIECAQG8VKL6zLb7D7a7tFIjVXbLWS4AAAQIECBAgQIAAAQIjnMAXL72Xt3mcqSdORarrEQ5BgwkQIECAAAECBAgQIECAAAECBAgQINCLBOK72vjONkrxHW53bZ5ArO6StV4CBAgQIECAAAECBAgQGOEEBn3zffr08Tfzdk88z/QjXPs1mAABAgQIECBAgAABAgQIECBAgAABAr1NoPiuNr67je9wu7MIxOpOXesmQIAAAQIECBAgQIAAgRFO4JOHXs3bPOFc06axJptghGu/BhMgQIAAAQIECBAgQIAAAQIECBAgQKC3CMR3tPFdbZTiu9vu3DaBWN2pa90ECBAgQIAAAQIECBAgMMIJfPP+Z2ng/a/k7Z5y6TlGuPZrMAECBAgQIECAAAECBAgQIECAAAECBHqLQPEdbXxnG9/ddncRiNXdwtZPgAABAgQIECBAgAABAiOcwAe3PZu+effTNOYk46VpV5p/hGu/BhMgQIAAAQIECBAgQIAAAQIECBAgQKCnBeK72fiONr6rje9sG1EEYjVCWR0ECBAgQIAAAQIECBAgMMIJvHvN42nQ19+l8WaeQjDWCLf3NZgAAQIECBAgQIAAAQIECBAgQIAAgZ4UiCCs+G42vqON72obVQRiNUpaPQQIECBAgAABAgQIECAwQgl8m6W5fueyh8pgrJnXWzKNNdkEI5SBxhIgQIAAAQIECBAgQIAAAQIECBAgQKCRAvEdbHwXWwRhxXe08V1to8oojapIPQQIECBAgAABAgQIECBAYEQT+OrNj9ObF92XJvvlvGnMycZLM62zeProqdfTB0+8mr4Z+MWIxqG9BAgQIECAAAECBAgQIECAAAECBAgQ6BaB0ccfO008z/Rpwrmmzdcf3RFGJqxGBmFFxQKxumX3WikBAgQIECBAgAABAgQIEBgsEP/Qf+Pcu9PEP589jb/oDPkXAfFlwOdvfpA+e/2D9OV7A/OgrB++/hYZAQIECBAgQIAAAQIECBAgQIAAAQIECAyFwMhjjJYi+GqsScdPP5t24jTO1BOXSw28/5X0wW3Plu8bOSAQq5Ha6iJAgAABAgQIECBAgACBEVYg/uH/2VNvpfEWmj6NO+/U+RcD1S8HRlgYDSdAgAABAgQIECBAgAABAgQIECBAgEAXCHz6+Jvpk4ey3gga2BVh7WYLxKoV8Z4AAQIECBAgQIAAAQIECHSTQHwB8N51T6QPbn0mjT3TpGnMaSdKo086bhp1/DFTvzFG7aZarZYAAQIECBAgQIAAAQIECBAgQIAAAQLNJTDo6+/SdwO/St+892n66vUP0xcvvZcGffN9jzdSIFaP7wIbQIAAAQIECBAgQIAAAQIjmkB8IfDZ02/nfyNa27WXAAECBAgQIECAAAECBAgQIECAAAECzSrQr1kbpl0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBolIBArEZJq4cAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaYVEIjVtLtWwwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaJSAQKxGSauHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGmFRCI1bS7VsMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiUgECsRkmrhwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBphUQiNW0u1bDCBAgQIAAAQIECBAgQIAAgf9v145pAABgGIbxZ10WOSojqOa9IUCAAAECBAgQIECAAAECBAgQIECAAIFKQIhVSdshQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQOBWQIh1+1qHESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQCQixKmk7BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCgixbl/rMAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKgEhViVthwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBWwEh1u1rHUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQCUgxKqk7RAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgcCsgxLp9rcMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKgEhFiVtB0CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBG4FhFi3r3UYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVgBCrkrZDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgBDr9rUOI0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgEhBiVdJ2CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBC4FRBi3b7WYQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVAJCrEraDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECtwJCrNvXOowAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUpAiFVJ2yFAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBA4FZAiHX7WocRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAJCLEqaTsECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKCLFuX+swAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQqASFWJW2HAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFbASHW7WsdRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJSDEqqTtECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBwKyDEun2twwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqASEWJW0HQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEbgWEWLevdRgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABApWAEKuStkOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AEOv2tQ4jQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKASEGJV0nYIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIELgVEGLdvtZhBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhUAkKsStoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQK3AgOz8Hfdyz6uMAAAAABJRU5ErkJggg==)\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.3\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/k8s-otel/Dockerfile",
    "content": "# syntax=docker/dockerfile:1\nFROM python:3.13-slim\n\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv\n\nWORKDIR /src\n\n# 1) Copy only dependency metadata — cached until lock/pyproject files change\nCOPY pyproject.toml uv.lock ./\nCOPY packages/llama-index-workflows/pyproject.toml packages/llama-index-workflows/pyproject.toml\nCOPY packages/llama-index-utils-workflow/pyproject.toml packages/llama-index-utils-workflow/pyproject.toml\nCOPY packages/llama-agents-server/pyproject.toml packages/llama-agents-server/pyproject.toml\nCOPY packages/llama-agents-client/pyproject.toml packages/llama-agents-client/pyproject.toml\nCOPY packages/llama-agents-dbos/pyproject.toml packages/llama-agents-dbos/pyproject.toml\nCOPY packages/llama-agents-integration-tests/pyproject.toml packages/llama-agents-integration-tests/pyproject.toml\nCOPY examples/k8s-otel/pyproject.toml examples/k8s-otel/pyproject.toml\n\n# 2) Install all dependencies (cached unless lock/pyproject files change)\nRUN --mount=type=cache,target=/root/.cache/uv \\\n    uv sync --no-editable --package k8s-otel-example --no-install-workspace\n\n# 3) Copy source and install workspace packages only (fast — deps already present)\nCOPY . .\nRUN --mount=type=cache,target=/root/.cache/uv \\\n    uv sync --no-editable --package k8s-otel-example\n\nENV PATH=\"/src/.venv/bin:$PATH\"\n\nENTRYPOINT [\"python\", \"examples/k8s-otel/app.py\"]\n"
  },
  {
    "path": "examples/k8s-otel/README.md",
    "content": "# K8s + OpenTelemetry Example\n\nA self-contained example deploying LlamaIndex Workflows on Kubernetes with distributed tracing via OpenTelemetry and Arize Phoenix.\n\n## What's Inside\n\n- **Counter workflow** — counts to 20 with 1s delays, emitting stream events\n- **Greeter workflow** — human-in-the-loop (HITL) with idle release: asks for a name, waits, then greets\n- **DBOS runtime** — durable execution across 2 replicas sharing Postgres\n- **OpenTelemetry** — traces exported via OTLP to Phoenix\n- **structlog** — structured logs with trace context (`run_id`, span tags)\n\n## Architecture\n\n```\n┌──────────────┐     ┌──────────────┐\n│  app-0       │     │  app-1       │\n│  (replica)   │     │  (replica)   │\n└──────┬───────┘     └──────┬───────┘\n       │  OTLP gRPC         │\n       ▼                    ▼\n┌──────────────────────────────────┐\n│         Phoenix (traces UI)      │\n│         localhost:6006           │\n└──────────────────────────────────┘\n       │\n       │  SQL\n       ▼\n┌──────────────────────────────────┐\n│         Postgres                 │\n│         (shared state)           │\n└──────────────────────────────────┘\n```\n\n## Prerequisites\n\n- Docker\n- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation)\n- [Tilt](https://docs.tilt.dev/install.html)\n- kubectl\n\n## Quick Start\n\n```bash\n# Create a kind cluster (one-time setup)\nkind create cluster --config examples/k8s-otel/kind-config.yaml\n\n# Deploy\ncd examples/k8s-otel\ntilt up\n```\n\nTilt opens a browser UI showing all resources. The Tiltfile is pinned to the `kind-llama-k8s-otel` context so it won't accidentally deploy elsewhere.\n\nCtrl-C stops Tilt but **leaves all resources running** — your Postgres data and Phoenix traces persist. Run `tilt up` again to reconnect.\n\nTo tear down app resources (Postgres data is preserved):\n\n```bash\ntilt down\n```\n\nTo destroy the cluster entirely:\n\n```bash\nkind delete cluster --name llama-k8s-otel\n```\n\n## Manual Alternative\n\n```bash\n# Build from repo root\ndocker build -f examples/k8s-otel/Dockerfile -t k8s-otel-app .\n\n# Deploy\nkubectl apply -k examples/k8s-otel/k8s/\n\n# Port-forward\nkubectl port-forward -n llama-k8s-otel svc/app 8080:8080 &\nkubectl port-forward -n llama-k8s-otel svc/phoenix 6006:6006 &\n```\n\n## Interacting with the App\n\n### Counter workflow\n\n```bash\n# Start the counter without waiting (returns handler_id)\ncurl -s -X POST http://localhost:8080/workflows/counter/run-nowait \\\n  -H 'Content-Type: application/json' -d '{}'\n\n# Check result (after ~20s)\ncurl -s http://localhost:8080/results/<handler_id>\n\n# Or run synchronously (blocks until done)\ncurl -s -X POST http://localhost:8080/workflows/counter/run \\\n  -H 'Content-Type: application/json' -d '{}'\n```\n\n### Greeter workflow (HITL)\n\n```bash\n# Start — returns a handler_id\ncurl -s -X POST http://localhost:8080/workflows/greeter/run-nowait \\\n  -H 'Content-Type: application/json' -d '{}'\n# {\"handler_id\": \"abc123\", ...}\n\n# Send user input\ncurl -s -X POST http://localhost:8080/events/<handler_id> \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"event\": {\"type\": \"UserInput\", \"value\": {\"response\": \"Alice\"}}}'\n\n# Get result\ncurl -s http://localhost:8080/results/<handler_id>\n```\n\n### View Traces\n\nOpen [http://localhost:6006](http://localhost:6006) in your browser to see traces in Phoenix.\n\n## Notes\n\n- **Postgres not ready on first deploy**: DBOS retries connections. App pods may restart once — that's normal.\n- **Phoenix not ready**: The OTLP exporter silently drops spans if Phoenix isn't up yet. No app impact.\n- **Idle release across replicas**: If a pod dies while a greeter workflow is idle-released, another replica picks it up via DBOS recovery. This is the intended distributed behavior.\n- **Data persistence**: Postgres uses a PVC with `tilt.dev/down-policy: keep`, so data survives `tilt down`. Only deleting the kind cluster destroys data.\n"
  },
  {
    "path": "examples/k8s-otel/Tiltfile",
    "content": "allow_k8s_contexts('kind-llama-k8s-otel')\n\nk8s_yaml(kustomize('./k8s'))\n\ndocker_build(\n    'k8s-otel-app',\n    context='../..',\n    dockerfile='./Dockerfile',\n    only=[\n        'packages/',\n        'examples/k8s-otel/',\n        'pyproject.toml',\n        'uv.lock',\n    ],\n)\n\nk8s_resource(\n    'postgres',\n    labels=['infra'],\n    objects=[\n        'postgres-init:configmap',\n        'pgdata:persistentvolumeclaim',\n    ],\n)\n\nk8s_resource(\n    'phoenix',\n    labels=['observability'],\n    port_forwards='6006:6006',\n    resource_deps=['postgres'],\n)\n\nk8s_resource(\n    'jaeger',\n    labels=['observability'],\n    port_forwards=[\n        '16686:16686',  # Jaeger UI\n        '4317:4317',    # OTLP gRPC (for verify_spans.py)\n    ],\n)\n\nk8s_resource(\n    'otel-collector',\n    labels=['observability'],\n    objects=['otel-collector-config:configmap'],\n    resource_deps=['phoenix', 'jaeger'],\n)\n\nk8s_resource(\n    'app',\n    labels=['app'],\n    port_forwards='8080:8080',\n    resource_deps=['postgres'],\n)\n"
  },
  {
    "path": "examples/k8s-otel/app.py",
    "content": "\"\"\"\nK8s OTEL Example — Counter + Greeter workflows with DBOS, OpenTelemetry, and structlog.\n\nServes via FastAPI with the WorkflowServer mounted at /api and custom endpoints at the\ntop level. FastAPI is instrumented with OpenTelemetry.\n\nEnv vars:\n  POSTGRES_DSN              — Postgres connection string\n  OTEL_EXPORTER_OTLP_ENDPOINT — OTLP gRPC endpoint (e.g. http://phoenix:4317)\n  SERVER_PORT               — HTTP port (default 8080)\n  IDLE_TIMEOUT              — Seconds before idle release (default 30)\n  EXECUTOR_POOL_SIZE        — Number of executor slots (e.g., \"2\")\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport logging.config\nimport os\nimport signal\nimport time\nfrom contextlib import asynccontextmanager\nfrom typing import Any, AsyncGenerator, MutableMapping\n\nimport structlog\nimport uvicorn\nfrom dbos import DBOS\nfrom fastapi import FastAPI\nfrom fastapi.responses import RedirectResponse\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import WorkflowServer\nfrom llama_index.observability.otel import LlamaIndexOpenTelemetry\nfrom llama_index_instrumentation import get_dispatcher\nfrom llama_index_instrumentation.dispatcher import active_instrument_tags\nfrom opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter\nfrom opentelemetry.instrumentation.fastapi import FastAPIInstrumentor\nfrom pydantic import Field\nfrom starlette.types import ASGIApp, Receive, Scope, Send\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\n\n# ---------------------------------------------------------------------------\n# Config from env\n# ---------------------------------------------------------------------------\nPOSTGRES_DSN = os.environ.get(\n    \"POSTGRES_DSN\", \"postgresql://workflows:workflows@localhost:5432/workflows\"\n)\nOTEL_ENDPOINT = os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"http://localhost:4317\")\nSERVER_PORT = int(os.environ.get(\"SERVER_PORT\", \"8080\"))\nIDLE_TIMEOUT = float(os.environ.get(\"IDLE_TIMEOUT\", \"30\"))\nEXECUTOR_POOL_SIZE = int(os.environ.get(\"EXECUTOR_POOL_SIZE\", \"2\"))\n\n# ---------------------------------------------------------------------------\n# Logging helpers\n# ---------------------------------------------------------------------------\n\nlog = structlog.get_logger(\"app\")\naccess_logger = logging.getLogger(\"access\")\n\n\ndef _merge_instrument_tags(\n    _logger: logging.Logger,\n    _method_name: str,\n    event_dict: MutableMapping[str, Any],\n) -> MutableMapping[str, Any]:\n    ctx = active_instrument_tags.get()\n    if ctx:\n        for k, v in ctx.items():\n            event_dict.setdefault(k, v)\n    return event_dict\n\n\ndef _drop_uvicorn_color_message(\n    _logger: logging.Logger,\n    _method_name: str,\n    event_dict: MutableMapping[str, Any],\n) -> MutableMapping[str, Any]:\n    event_dict.pop(\"color_message\", None)\n    return event_dict\n\n\ndef setup_logging() -> None:\n    \"\"\"Configure all Python logging (stdlib + structlog) to emit JSON.\n\n    Must be called AFTER DBOS() init so we can clear its custom handlers.\n    \"\"\"\n    shared_processors: list[structlog.types.Processor] = [\n        structlog.contextvars.merge_contextvars,\n        structlog.processors.add_log_level,\n        structlog.processors.TimeStamper(fmt=\"iso\", utc=True),\n        _merge_instrument_tags,\n    ]\n\n    # structlog loggers → wrap into a stdlib LogRecord for the formatter\n    structlog.configure(\n        processors=[\n            *shared_processors,\n            structlog.stdlib.ProcessorFormatter.wrap_for_formatter,\n        ],\n        logger_factory=structlog.stdlib.LoggerFactory(),\n        wrapper_class=structlog.stdlib.BoundLogger,\n        cache_logger_on_first_use=True,\n    )\n\n    formatter = structlog.stdlib.ProcessorFormatter(\n        processors=[\n            structlog.stdlib.ProcessorFormatter.remove_processors_meta,\n            structlog.processors.JSONRenderer(),\n        ],\n        foreign_pre_chain=[\n            structlog.stdlib.add_logger_name,\n            *shared_processors,\n            _drop_uvicorn_color_message,\n            structlog.stdlib.ExtraAdder(),\n        ],\n    )\n\n    handler = logging.StreamHandler()\n    handler.setFormatter(formatter)\n\n    root = logging.getLogger()\n    root.handlers.clear()\n    root.addHandler(handler)\n    root.setLevel(logging.INFO)\n\n    # Clear DBOS's private handler so its logs propagate to root\n    dbos_logger = logging.getLogger(\"dbos\")\n    dbos_logger.handlers.clear()\n    dbos_logger.propagate = True\n\n    # Suppress uvicorn's default access logger (we have our own middleware)\n    logging.getLogger(\"uvicorn.access\").setLevel(logging.WARNING)\n\n\n# ---------------------------------------------------------------------------\n# OTEL setup\n# ---------------------------------------------------------------------------\notel_exporter = OTLPSpanExporter(endpoint=OTEL_ENDPOINT, insecure=True)\ninstrumentor = LlamaIndexOpenTelemetry(\n    span_exporter=otel_exporter,\n    service_name_or_resource=\"k8s-otel-example\",\n    span_processor=\"simple\",\n)\ninstrumentor.start_registering()\n\n# ---------------------------------------------------------------------------\n# DBOS setup — must be called at module level before DBOSRuntime()\n# ---------------------------------------------------------------------------\nDBOS(\n    config={\n        \"name\": \"k8s-otel-example\",\n        \"system_database_url\": POSTGRES_DSN,\n        \"run_admin_server\": False,\n    }\n)\n\n# Must come AFTER DBOS() init — DBOS adds its own handlers in __init__\nsetup_logging()\n\n# ---------------------------------------------------------------------------\n# Counter Workflow\n# ---------------------------------------------------------------------------\n\n\nclass Tick(Event):\n    count: int = Field(description=\"Current count\")\n\n\nclass WaitDone(Event):\n    count: int = Field(description=\"Current count after waiting\")\n\n\nclass CounterResult(StopEvent):\n    final_count: int = Field(description=\"Final counter value\")\n\n\nclass CounterWorkflow(Workflow):\n    \"\"\"Counts to 20 with 1s delays, emitting Tick stream events.\"\"\"\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> WaitDone:\n        log.info(\"counter.start\")\n        return WaitDone(count=0)\n\n    @step\n    async def tick(self, ctx: Context, ev: WaitDone) -> Tick | CounterResult:\n        count = ev.count + 1\n        await ctx.store.set(\"count\", count)\n        ctx.write_event_to_stream(Tick(count=count))\n        log.info(\"counter.tick\", count=count)\n        if count >= 20:\n            return CounterResult(final_count=count)\n        return Tick(count=count)\n\n    @step\n    async def wait(self, ctx: Context, ev: Tick) -> WaitDone:\n        await asyncio.sleep(1.0)\n        return WaitDone(count=ev.count)\n\n\n# ---------------------------------------------------------------------------\n# Greeter Workflow (HITL with idle release)\n# ---------------------------------------------------------------------------\n\n\nclass AskName(InputRequiredEvent):\n    prompt: str = Field(default=\"What is your name?\")\n\n\nclass UserInput(HumanResponseEvent):\n    response: str = Field(default=\"\")\n\n\nclass GreeterWorkflow(Workflow):\n    \"\"\"Ask for a name, wait (idle-releases), then greet.\"\"\"\n\n    @step\n    async def ask(self, ctx: Context, ev: StartEvent) -> AskName:\n        log.info(\"greeter.ask\")\n        return AskName()\n\n    @step\n    async def greet(self, ctx: Context, ev: UserInput) -> StopEvent:\n        greeting = f\"Hello, {ev.response}!\"\n        log.info(\"greeter.greet\", greeting=greeting)\n        return StopEvent(result={\"greeting\": greeting})\n\n\n# ---------------------------------------------------------------------------\n# Workflow Server\n# ---------------------------------------------------------------------------\n\nruntime = DBOSRuntime(\n    _experimental_executor_lease={\n        \"pool_size\": EXECUTOR_POOL_SIZE,\n    },\n)\n\nworkflow_server = WorkflowServer(\n    workflow_store=runtime.create_workflow_store(),\n    runtime=runtime.build_server_runtime(idle_timeout=IDLE_TIMEOUT),\n)\nworkflow_server.add_workflow(\"counter\", CounterWorkflow(runtime=runtime))\nworkflow_server.add_workflow(\"greeter\", GreeterWorkflow(runtime=runtime))\n\n# ---------------------------------------------------------------------------\n# FastAPI app with WorkflowServer mounted\n# ---------------------------------------------------------------------------\n\n\ndef _flush_and_shutdown() -> None:\n    \"\"\"Flush open spans and shut down the OTel pipeline synchronously.\"\"\"\n    get_dispatcher().shutdown()\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:\n    # Register signal handler so spans flush on SIGTERM even if uvicorn\n    # is still waiting for connections to drain.\n    loop = asyncio.get_running_loop()\n    loop.add_signal_handler(signal.SIGTERM, _flush_and_shutdown)\n    await workflow_server.start()\n    try:\n        yield\n    finally:\n        _flush_and_shutdown()\n        await workflow_server.stop()\n\n\nHEALTH_PATHS = {\"/health\"}\n\n\nclass AccessLogMiddleware:\n    \"\"\"Raw ASGI middleware so it covers mounted sub-apps (e.g. /api).\"\"\"\n\n    def __init__(self, app: ASGIApp) -> None:\n        self.app = app\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        if scope[\"type\"] != \"http\":\n            await self.app(scope, receive, send)\n            return\n\n        path = scope.get(\"path\", \"\")\n        if path in HEALTH_PATHS:\n            await self.app(scope, receive, send)\n            return\n\n        start = time.perf_counter()\n        status_code = 0\n\n        async def send_wrapper(message: MutableMapping[str, Any]) -> None:\n            nonlocal status_code\n            if message[\"type\"] == \"http.response.start\":\n                status_code = message[\"status\"]\n            await send(message)\n\n        await self.app(scope, receive, send_wrapper)\n\n        dur_ms = (time.perf_counter() - start) * 1000\n        method = scope.get(\"method\", \"?\")\n        access_logger.info(\n            \"%s %s\",\n            method,\n            path,\n            extra={\"status_code\": status_code, \"duration_ms\": round(dur_ms, 1)},\n        )\n\n\napp = FastAPI(title=\"K8s OTEL Example\", lifespan=lifespan)\n\n# Mount the workflow server's Starlette app under /api\napp.mount(\"/api\", workflow_server.app)\n\n# Access log middleware wraps the entire ASGI app including mounts\napp.add_middleware(AccessLogMiddleware)\n\n# Instrument FastAPI with OpenTelemetry\nFastAPIInstrumentor.instrument_app(app, excluded_urls=\"health\")\n\n\n@app.get(\"/\")\nasync def index() -> RedirectResponse:\n    return RedirectResponse(url=\"/api/?api=/api/\")\n\n\n@app.get(\"/health\")\nasync def health() -> dict[str, str]:\n    return {\"status\": \"ok\"}\n\n\n# ---------------------------------------------------------------------------\n# Entrypoint\n# ---------------------------------------------------------------------------\n\n\nasync def main() -> None:\n    log.info(\n        \"server.starting\",\n        port=SERVER_PORT,\n        executor_pool_size=EXECUTOR_POOL_SIZE,\n        idle_timeout=IDLE_TIMEOUT,\n    )\n    config = uvicorn.Config(\n        app, host=\"0.0.0.0\", port=SERVER_PORT, log_config=None, access_log=False\n    )\n    server = uvicorn.Server(config)\n    await server.serve()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/k8s-otel/k8s/app.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: app\n  namespace: llama-k8s-otel\nspec:\n  selector:\n    app: workflow-app\n  ports:\n    - port: 8080\n      targetPort: 8080\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: app\n  namespace: llama-k8s-otel\nspec:\n  replicas: 2\n  strategy:\n    type: RollingUpdate\n    rollingUpdate:\n      # maxSurge 0 ensures new pods only start after old ones terminate,\n      # freeing executor lease slots for the incoming replicas.\n      maxSurge: 0\n      maxUnavailable: 1\n  selector:\n    matchLabels:\n      app: workflow-app\n  template:\n    metadata:\n      labels:\n        app: workflow-app\n    spec:\n      initContainers:\n        - name: wait-for-postgres\n          image: postgres:16\n          command:\n            - sh\n            - -c\n            - |\n              until pg_isready -h postgres -p 5432 -U workflows; do\n                echo \"Waiting for postgres...\"\n                sleep 2\n              done\n      containers:\n        - name: app\n          image: k8s-otel-app\n          ports:\n            - containerPort: 8080\n          env:\n            - name: POSTGRES_DSN\n              value: postgresql://workflows:workflows@postgres:5432/workflows\n            - name: OTEL_EXPORTER_OTLP_ENDPOINT\n              value: http://otel-collector:4317\n            - name: SERVER_PORT\n              value: \"8080\"\n            - name: EXECUTOR_POOL_SIZE\n              value: \"3\"\n          livenessProbe:\n            httpGet:\n              path: /health\n              port: 8080\n            initialDelaySeconds: 10\n            periodSeconds: 10\n          readinessProbe:\n            httpGet:\n              path: /health\n              port: 8080\n            initialDelaySeconds: 5\n            periodSeconds: 5\n"
  },
  {
    "path": "examples/k8s-otel/k8s/jaeger.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: jaeger\n  namespace: llama-k8s-otel\nspec:\n  selector:\n    app: jaeger\n  ports:\n    - name: ui\n      port: 16686\n      targetPort: 16686\n    - name: otlp-grpc\n      port: 4317\n      targetPort: 4317\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: jaeger\n  namespace: llama-k8s-otel\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: jaeger\n  template:\n    metadata:\n      labels:\n        app: jaeger\n    spec:\n      containers:\n        - name: jaeger\n          image: jaegertracing/jaeger:latest\n          ports:\n            - containerPort: 16686\n            - containerPort: 4317\n          env:\n            - name: COLLECTOR_OTLP_ENABLED\n              value: \"true\"\n"
  },
  {
    "path": "examples/k8s-otel/k8s/kustomization.yaml",
    "content": "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n  - namespace.yaml\n  - postgres.yaml\n  - app.yaml\n  - phoenix.yaml\n  - jaeger.yaml\n  - otel-collector.yaml\n"
  },
  {
    "path": "examples/k8s-otel/k8s/namespace.yaml",
    "content": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: llama-k8s-otel\n"
  },
  {
    "path": "examples/k8s-otel/k8s/otel-collector.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: otel-collector-config\n  namespace: llama-k8s-otel\ndata:\n  config.yaml: |\n    receivers:\n      otlp:\n        protocols:\n          grpc:\n            endpoint: 0.0.0.0:4317\n    exporters:\n      otlp/phoenix:\n        endpoint: phoenix:4317\n        tls:\n          insecure: true\n      otlp/jaeger:\n        endpoint: jaeger:4317\n        tls:\n          insecure: true\n    service:\n      pipelines:\n        traces:\n          receivers: [otlp]\n          exporters: [otlp/phoenix, otlp/jaeger]\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: otel-collector\n  namespace: llama-k8s-otel\nspec:\n  selector:\n    app: otel-collector\n  ports:\n    - name: otlp-grpc\n      port: 4317\n      targetPort: 4317\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: otel-collector\n  namespace: llama-k8s-otel\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: otel-collector\n  template:\n    metadata:\n      labels:\n        app: otel-collector\n    spec:\n      containers:\n        - name: collector\n          image: otel/opentelemetry-collector-contrib:latest\n          args: [\"--config=/etc/otel/config.yaml\"]\n          ports:\n            - containerPort: 4317\n          volumeMounts:\n            - name: config\n              mountPath: /etc/otel\n      volumes:\n        - name: config\n          configMap:\n            name: otel-collector-config\n"
  },
  {
    "path": "examples/k8s-otel/k8s/phoenix.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: phoenix\n  namespace: llama-k8s-otel\nspec:\n  selector:\n    app: phoenix\n  ports:\n    - name: otlp-grpc\n      port: 4317\n      targetPort: 4317\n    - name: ui\n      port: 6006\n      targetPort: 6006\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: phoenix\n  namespace: llama-k8s-otel\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: phoenix\n  template:\n    metadata:\n      labels:\n        app: phoenix\n    spec:\n      containers:\n        - name: phoenix\n          image: arizephoenix/phoenix:latest\n          ports:\n            - containerPort: 4317\n            - containerPort: 6006\n          env:\n            - name: PHOENIX_PORT\n              value: \"6006\"\n            - name: PHOENIX_SQL_DATABASE_URL\n              value: \"postgresql://workflows:workflows@postgres:5432/phoenix\"\n"
  },
  {
    "path": "examples/k8s-otel/k8s/postgres.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: postgres-init\n  namespace: llama-k8s-otel\ndata:\n  create-phoenix-db.sql: |\n    CREATE DATABASE phoenix;\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: pgdata\n  namespace: llama-k8s-otel\n  annotations:\n    tilt.dev/down-policy: keep\nspec:\n  accessModes: [\"ReadWriteOnce\"]\n  resources:\n    requests:\n      storage: 1Gi\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: postgres\n  namespace: llama-k8s-otel\nspec:\n  selector:\n    app: postgres\n  ports:\n    - port: 5432\n      targetPort: 5432\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: postgres\n  namespace: llama-k8s-otel\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postgres\n  template:\n    metadata:\n      labels:\n        app: postgres\n    spec:\n      containers:\n        - name: postgres\n          image: postgres:16\n          ports:\n            - containerPort: 5432\n          env:\n            - name: POSTGRES_USER\n              value: workflows\n            - name: POSTGRES_PASSWORD\n              value: workflows\n            - name: POSTGRES_DB\n              value: workflows\n          volumeMounts:\n            - name: pgdata\n              mountPath: /var/lib/postgresql/data\n            - name: init-scripts\n              mountPath: /docker-entrypoint-initdb.d\n      volumes:\n        - name: pgdata\n          persistentVolumeClaim:\n            claimName: pgdata\n        - name: init-scripts\n          configMap:\n            name: postgres-init\n"
  },
  {
    "path": "examples/k8s-otel/kind-config.yaml",
    "content": "kind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nname: llama-k8s-otel\n"
  },
  {
    "path": "examples/k8s-otel/pyproject.toml",
    "content": "[project]\nname = \"k8s-otel-example\"\nversion = \"0.0.0\"\nrequires-python = \">=3.10\"\ndependencies = [\n  \"llama-agents-dbos\",\n  \"opentelemetry-exporter-otlp\",\n  \"opentelemetry-instrumentation-fastapi\",\n  \"structlog\",\n  \"llama-index-observability-otel>=0.5.1\",\n  \"fastapi\",\n  \"uvicorn\"\n]\n\n[tool.uv.sources]\nllama-agents-dbos = {workspace = true}\n"
  },
  {
    "path": "examples/observability/README.md",
    "content": "# Observability Examples\n\nTrace and inspect workflow runs using the built-in context logger and third-party observability platforms.\n\n## Examples\n\n| Notebook | What it shows |\n| --- | --- |\n| [`workflows_observability_pt1.ipynb`](workflows_observability_pt1.ipynb) | Part 1: the basics of instrumenting a workflow and reading its event stream. **Start here.** |\n| [`workflows_observability_pt2.ipynb`](workflows_observability_pt2.ipynb) | Part 2: deeper patterns — custom spans, nested workflows, and step-level timing. |\n| [`workflow_context_logging.ipynb`](workflow_context_logging.ipynb) | Use the built-in context logger to emit structured logs tied to a run. |\n| [`workflows_observablitiy_arize_phoenix.ipynb`](workflows_observablitiy_arize_phoenix.ipynb) | Export traces to [Arize Phoenix](https://phoenix.arize.com/) for visualization. |\n| [`workflows_observablitiy_langfuse.ipynb`](workflows_observablitiy_langfuse.ipynb) | Export traces to [Langfuse](https://langfuse.com/). |\n"
  },
  {
    "path": "examples/observability/workflow_context_logging.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"29188397\",\n   \"metadata\": {},\n   \"source\": [\n    \"Llama Index Dispatcher context fields are passed through to workflow runs. Additionally, workflow runs track their individual `run_id`s as a context field. Tracking these fields in logs can be useful to differentiate runs when running with concurrency, and associate them back to a trace.\\n\",\n    \"\\n\",\n    \"This notebook demonstrates how to integrate with standard library logging, as well as with structlog, for including these fields in logs.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"d1b89a92\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install structlog llama-index-workflows\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"6bb3c1e0\",\n   \"metadata\": {},\n   \"source\": [\n    \"Set up imports\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"e1942819\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import logging\\n\",\n    \"from typing import Any, MutableMapping\\n\",\n    \"\\n\",\n    \"import structlog\\n\",\n    \"from llama_index_instrumentation.dispatcher import (\\n\",\n    \"    active_instrument_tags,\\n\",\n    \"    instrument_tags,\\n\",\n    \")\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"552d7606\",\n   \"metadata\": {},\n   \"source\": [\n    \"set up structlog to read from the dispatcher context:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 16,\n   \"id\": \"e5995e5d\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"def merge_custom_context(\\n\",\n    \"    _logger: structlog.BoundLogger,\\n\",\n    \"    _method_name: str,\\n\",\n    \"    event_dict: MutableMapping[str, Any],\\n\",\n    \") -> MutableMapping[str, Any]:\\n\",\n    \"    \\\"\\\"\\\"\\n\",\n    \"    Merge values from your ContextVar dict into structlog's event_dict.\\n\",\n    \"    Later processors (e.g., JSONRenderer) will see these keys as if bound.\\n\",\n    \"    \\\"\\\"\\\"\\n\",\n    \"    ctx = active_instrument_tags.get()\\n\",\n    \"    if ctx:\\n\",\n    \"        # don't clobber explicitly-set event keys unless you want to:\\n\",\n    \"        for k, v in ctx.items():\\n\",\n    \"            event_dict.setdefault(k, v)\\n\",\n    \"            # or: event_dict[k] = v  # if you want your ctx to win\\n\",\n    \"    return event_dict\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"structlog.configure(\\n\",\n    \"    processors=[\\n\",\n    \"        merge_custom_context,  # <------------- Add this to add llama index dispatcher tags to structlog\\n\",\n    \"        structlog.processors.add_log_level,\\n\",\n    \"        structlog.processors.TimeStamper(fmt=\\\"%Y-%m-%d %H:%M:%S\\\", utc=False),\\n\",\n    \"        structlog.dev.ConsoleRenderer(),\\n\",\n    \"    ],\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"8e8a195b\",\n   \"metadata\": {},\n   \"source\": [\n    \"Set up structlog to read from the dispatcher context:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"id\": \"a5e51f78\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"def merge_custom_context(\\n\",\n    \"    _logger: structlog.BoundLogger,\\n\",\n    \"    _method_name: str,\\n\",\n    \"    event_dict: MutableMapping[str, Any],\\n\",\n    \") -> MutableMapping[str, Any]:\\n\",\n    \"    \\\"\\\"\\\"\\n\",\n    \"    Merge values from your ContextVar dict into structlog's event_dict.\\n\",\n    \"    Later processors (e.g., JSONRenderer) will see these keys as if bound.\\n\",\n    \"    \\\"\\\"\\\"\\n\",\n    \"    ctx = active_instrument_tags.get()\\n\",\n    \"    if ctx:\\n\",\n    \"        # don't clobber explicitly-set event keys unless you want to:\\n\",\n    \"        for k, v in ctx.items():\\n\",\n    \"            event_dict.setdefault(k, v)\\n\",\n    \"            # or: event_dict[k] = v  # if you want your ctx to win\\n\",\n    \"    return event_dict\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"structlog.configure(\\n\",\n    \"    processors=[\\n\",\n    \"        merge_custom_context,  # <------------- Add this to add llama index dispatcher tags to structlog\\n\",\n    \"        structlog.processors.add_log_level,\\n\",\n    \"        structlog.processors.TimeStamper(fmt=\\\"%Y-%m-%d %H:%M:%S\\\", utc=False),\\n\",\n    \"        structlog.dev.ConsoleRenderer(),\\n\",\n    \"    ],\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"c7f3672d\",\n   \"metadata\": {},\n   \"source\": [\n    \"Set up stdlib logging to include the run_id from the dispatcher context. Note that stdlib logging is much harder to configure correctly, and has difficulty with extra fields being optional or overwritten.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"id\": \"f1f724fc\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"old_factory = logging.getLogRecordFactory()\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def record_factory(*args, **kwargs):\\n\",\n    \"    record = old_factory(*args, **kwargs)  # get the unmodified record\\n\",\n    \"    record.run_id = active_instrument_tags.get().get(\\\"run_id\\\", \\\"\\\")\\n\",\n    \"    return record\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"logging.setLogRecordFactory(record_factory)\\n\",\n    \"\\n\",\n    \"logging.basicConfig(level=logging.INFO, format=\\\"%(message)s run_id=%(run_id)s\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"id\": \"759295ee\",\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\u001b[2m2025-11-05 22:48:59\\u001b[0m [\\u001b[32m\\u001b[1minfo     \\u001b[0m] \\u001b[1mHello from structlog          \\u001b[0m\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Hello from stdlib run_id=\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"structlog_logger = structlog.get_logger()\\n\",\n    \"regular_logger = logging.getLogger()\\n\",\n    \"structlog_logger.info(\\\"Hello from structlog\\\")\\n\",\n    \"regular_logger.info(\\\"Hello from stdlib\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"eb68f22b\",\n   \"metadata\": {},\n   \"source\": [\n    \"Set up an example workflow that demonstrates log context:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"id\": \"83a19c90\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"class LoggingWorkflow(Workflow):\\n\",\n    \"    \\\"\\\"\\\"A workflow that demonstrates log context.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def log_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\\n\",\n    \"        # Any fields bound here will also appear alongside dispatcher tags\\n\",\n    \"        structlog_logger.info(\\\"structlog processing step\\\")\\n\",\n    \"        # Without a more complex wrappers, the fields must be manually passed into standard logging\\n\",\n    \"        regular_logger.info(\\\"regular processing step\\\")\\n\",\n    \"\\n\",\n    \"        return StopEvent(result=\\\"ok\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"4c8681a0\",\n   \"metadata\": {},\n   \"source\": [\n    \"And run it! Try multiple times and see the run_id change.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 15,\n   \"id\": \"baa2b7d6\",\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\u001b[2m2025-11-05 22:51:47\\u001b[0m [\\u001b[32m\\u001b[1minfo     \\u001b[0m] \\u001b[1mstructlog processing step     \\u001b[0m \\u001b[36mrequest_id\\u001b[0m=\\u001b[35mreq-123\\u001b[0m \\u001b[36mrun_id\\u001b[0m=\\u001b[35mlBUAX17ywM\\u001b[0m \\u001b[36muser\\u001b[0m=\\u001b[35malice\\u001b[0m\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"regular processing step run_id=lBUAX17ywM\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\u001b[2m2025-11-05 22:51:47\\u001b[0m [\\u001b[32m\\u001b[1minfo     \\u001b[0m] \\u001b[1mfinal result 'ok'             \\u001b[0m\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Tags set outside the workflow run will be captured in all logs emitted\\n\",\n    \"# during the run (together with run_id injected by the broker).\\n\",\n    \"wf = LoggingWorkflow()\\n\",\n    \"\\n\",\n    \"with instrument_tags({\\\"request_id\\\": \\\"req-123\\\", \\\"user\\\": \\\"alice\\\"}):\\n\",\n    \"    result = await wf.run()\\n\",\n    \"structlog_logger.info(f\\\"final result '{result}'\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"c64826c9\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.14.0b4\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 5\n}\n"
  },
  {
    "path": "examples/observability/workflows_observability_pt1.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"sunqHvPG4el9\"\n   },\n   \"source\": [\n    \"# Workflows Observability - Part 1\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Vx09YKJjycji\"\n   },\n   \"source\": [\n    \"\\n\",\n    \"## Use native instrumentation from LlamaIndex + OpenTelemetry to fine-grain tracing in your code!\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"s1yQ1Rmr4I6Q\"\n   },\n   \"source\": [\n    \"In this notebook, we will go through an example of how to use instrumentation natively implemented in `llama-index` (combined with OpenTelemetry) to define costum span and events within your code. Before we get started:\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"⭐ Don'f forget to star the `llama-index-workflows` [GitHub repo](https://github.com/run-llama/llama-agents)\\n\",\n    \"\\n\",\n    \"🦙☁ Register to [LlamaCloud](https://cloud.llamaindex.ai) not to miss out on all our awesome products\\n\",\n    \"\\n\",\n    \"If you have feedback, questions, issues, or you just want to follow us not to miss out on any news, please find us on:\\n\",\n    \"\\n\",\n    \"[![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/run-llama/)\\n\",\n    \"[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/eN6D2HQ4aX)\\n\",\n    \"[![X](https://img.shields.io/badge/@llama__index-%23000000.svg?style=for-the-badge&logo=X&logoColor=white)](https://x.com/@llama_index)\\n\",\n    \"[![Bluesky](https://img.shields.io/badge/Bluesky-0285FF?style=for-the-badge&logo=Bluesky&logoColor=white)](https://bsky.app/profile/llamaindex.bsky.social)\\n\",\n    \"[![LinkedIn](https://img.shields.io/badge/linkedin-%230077B5.svg?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/llamaindex/)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"PCIOKSNJzwJB\"\n   },\n   \"source\": [\n    \"## 1. Setting up\\n\",\n    \"\\n\",\n    \"Before diving deep into all of this, let's install all the needed dependencies.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"9yQ8Z5zByLuj\",\n    \"outputId\": \"274bc1da-8d3f-4947-b7a4-fdec0276084b\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\u001b[?25l   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m0.0/7.6 MB\\u001b[0m \\u001b[31m?\\u001b[0m eta \\u001b[36m-:--:--\\u001b[0m\\r\\u001b[2K   \\u001b[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m\\u001b[90m╺\\u001b[0m\\u001b[90m━━━━━━━━━━\\u001b[0m \\u001b[32m5.6/7.6 MB\\u001b[0m \\u001b[31m169.2 MB/s\\u001b[0m eta \\u001b[36m0:00:01\\u001b[0m\\r\\u001b[2K   \\u001b[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m\\u001b[91m╸\\u001b[0m \\u001b[32m7.6/7.6 MB\\u001b[0m \\u001b[31m174.1 MB/s\\u001b[0m eta \\u001b[36m0:00:01\\u001b[0m\\r\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m7.6/7.6 MB\\u001b[0m \\u001b[31m76.3 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[?25h\\u001b[?25l   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m0.0/65.8 kB\\u001b[0m \\u001b[31m?\\u001b[0m eta \\u001b[36m-:--:--\\u001b[0m\\r\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m65.8/65.8 kB\\u001b[0m \\u001b[31m4.6 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[?25h\\u001b[?25l   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m0.0/118.5 kB\\u001b[0m \\u001b[31m?\\u001b[0m eta \\u001b[36m-:--:--\\u001b[0m\\r\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m118.5/118.5 kB\\u001b[0m \\u001b[31m9.4 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m196.2/196.2 kB\\u001b[0m \\u001b[31m16.7 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m1.2/1.2 MB\\u001b[0m \\u001b[31m58.3 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m50.9/50.9 kB\\u001b[0m \\u001b[31m3.9 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m129.3/129.3 kB\\u001b[0m \\u001b[31m11.5 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[?25h\\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\\n\",\n      \"ipython 7.34.0 requires jedi>=0.16, which is not installed.\\u001b[0m\\u001b[31m\\n\",\n      \"\\u001b[0m\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"! pip install -q llama-index-workflows llama-index-instrumentation llama-index-llms-openai llama-index-observability-otel\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"cyHfFgXP0rzA\"\n   },\n   \"source\": [\n    \"## 2. Experiment with instrumentation\\n\",\n    \"\\n\",\n    \"Let's now play around with `llama-index` dispatcher and see how we can make it work.\\n\",\n    \"\\n\",\n    \"Let's start by initializing it:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 17,\n   \"metadata\": {\n    \"id\": \"IRGWgr4t1EXl\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index_instrumentation import get_dispatcher\\n\",\n    \"\\n\",\n    \"dispatcher = get_dispatcher()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"YAnMutOL1xa6\"\n   },\n   \"source\": [\n    \"Now we can use the `@dispatcher.span` decorator on a function that we defined to emit spans (containers for events) and use `dispatcher.event` to emit an event (we can define custom events by subclassing the `BaseEvent` class):\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 18,\n   \"metadata\": {\n    \"id\": \"hprqOiV81w25\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index_instrumentation.base import BaseEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ExampleEvent(BaseEvent):\\n\",\n    \"    data: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class AnotherExampleEvent(BaseEvent):\\n\",\n    \"    print_statement: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"@dispatcher.span\\n\",\n    \"def example_fn(data: str) -> None:\\n\",\n    \"    dispatcher.event(ExampleEvent(data=data))\\n\",\n    \"    s = \\\"This are example string data: \\\" + data\\n\",\n    \"    dispatcher.event(AnotherExampleEvent(print_statement=s))\\n\",\n    \"    print(s)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"iDrSR1Xw6Wvn\"\n   },\n   \"source\": [\n    \"## 3. Add OpenTelemetry\\n\",\n    \"\\n\",\n    \"We can now add OpenTelemetry so that we can export all our span and events as ordered traces.\\n\",\n    \"We will be using the LlamaIndex integration for that, `llama-index-observability-otel`.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"6dauMQp6Jt9r\"\n   },\n   \"source\": [\n    \"We start by defining a custom `SpanExporter` that can write all our traces to a file:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {\n    \"id\": \"lKIvFShXD4qz\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import os\\n\",\n    \"from os import linesep\\n\",\n    \"from pathlib import Path\\n\",\n    \"from typing import Callable, Sequence\\n\",\n    \"\\n\",\n    \"from llama_index.observability.otel import LlamaIndexOpenTelemetry\\n\",\n    \"from opentelemetry.sdk.trace import ReadableSpan\\n\",\n    \"from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class FileSpanExporter(SpanExporter):\\n\",\n    \"    \\\"\\\"\\\"Implementation of :class:`SpanExporter` that prints spans to the\\n\",\n    \"    console.\\n\",\n    \"\\n\",\n    \"    This class can be used for diagnostic purposes. It prints the exported\\n\",\n    \"    spans to the console STDOUT.\\n\",\n    \"    \\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    def __init__(\\n\",\n    \"        self,\\n\",\n    \"        service_name: str | None = None,\\n\",\n    \"        file_path: os.PathLike[str] | None = None,\\n\",\n    \"        formatter: Callable[[ReadableSpan], str] = lambda span: json.dumps(\\n\",\n    \"            json.loads(span.to_json())\\n\",\n    \"        )\\n\",\n    \"        + linesep,\\n\",\n    \"    ):\\n\",\n    \"        if not file_path:\\n\",\n    \"            file_path = \\\"traces.json\\\"\\n\",\n    \"        if Path(file_path).exists():\\n\",\n    \"            raise ValueError(f\\\"File {file_path} already exists\\\")\\n\",\n    \"        self.file_path = file_path\\n\",\n    \"        self.formatter = formatter\\n\",\n    \"        self.service_name = service_name\\n\",\n    \"\\n\",\n    \"    def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:\\n\",\n    \"        print(f\\\"Writing {len(spans)} spans to {self.file_path}\\\")\\n\",\n    \"        if Path(self.file_path).exists():\\n\",\n    \"            mode = \\\"a\\\"\\n\",\n    \"        else:\\n\",\n    \"            mode = \\\"w\\\"\\n\",\n    \"        with open(self.file_path, mode) as out:\\n\",\n    \"            for span in spans:\\n\",\n    \"                out.write(self.formatter(span))\\n\",\n    \"            out.flush()\\n\",\n    \"        return SpanExportResult.SUCCESS\\n\",\n    \"\\n\",\n    \"    def force_flush(self, timeout_millis: int = 30000) -> bool:\\n\",\n    \"        return True\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"2k0mZcDO5lcy\"\n   },\n   \"source\": [\n    \"It is important to notice that we are defining here a custom span exporter since it is an easier implementation for notebooks, but there are many exporting options detailed by OpenTelemetry in [this page](https://opentelemetry.io/docs/languages/python/exporters/).\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"A4KJhJTuJ6MR\"\n   },\n   \"source\": [\n    \"Now we can pass that to the instrumentation class as a span exporter\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {\n    \"id\": \"pLWJQSEZJ5fg\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"se = FileSpanExporter(file_path=\\\"traces_example.json\\\")\\n\",\n    \"\\n\",\n    \"instrumentor = LlamaIndexOpenTelemetry(\\n\",\n    \"    span_exporter=se,\\n\",\n    \"    service_name_or_resource=\\\"example_service\\\",\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"i57Ozf_rK9lz\"\n   },\n   \"source\": [\n    \"And we can try and see how events are registered simply by calling `example_fn` as defined before :)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"collapsed\": true,\n    \"id\": \"h9vLcbwdK7Ae\",\n    \"outputId\": \"9d12f590-97c7-471a-cba4-279b2b4e1b07\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"This are example string data: Hello world!\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"instrumentor.start_registering()\\n\",\n    \"\\n\",\n    \"example_fn(data=\\\"Hello world!\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"collapsed\": true,\n    \"id\": \"qdg-bqKFLMn7\",\n    \"outputId\": \"674b09f2-08a6-4780-d511-66d05e2663bb\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"example_fn-0bf17e38-04b4-410c-ae22-e35179eb34ea\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x62cd1ca61b98012a18f99ed325613669\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xba365ecb80b88383\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": null,\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-07T12:39:26.164184Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-07T12:39:26.165149Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"BaseEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-07T12:39:26.165097Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"762b691e-f96d-41bd-8702-024f057fb3bd\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"example_fn-0bf17e38-04b4-410c-ae22-e35179eb34ea\\\",\\n\",\n      \"                \\\"data\\\": \\\"Hello world!\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"BaseEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        },\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"BaseEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-07T12:39:26.165124Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"c47cbda1-91f2-4880-a15b-796073d69514\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"example_fn-0bf17e38-04b4-410c-ae22-e35179eb34ea\\\",\\n\",\n      \"                \\\"print_statement\\\": \\\"This are example string data: Hello world!\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"BaseEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"example_service\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"with open(\\\"traces_example.json\\\") as f:\\n\",\n    \"    lines = f.readlines()\\n\",\n    \"    print(json.dumps(json.loads(lines[0]), indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"dlt0n55V5nD4\"\n   },\n   \"source\": [\n    \"As you can see, we have both our events, which we can recognize by the presence of their `data` and `print_statement` attributes\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Y3FvDOByPKyr\"\n   },\n   \"source\": [\n    \"## 4. Instrument a workflow\\n\",\n    \"\\n\",\n    \"Now that we know:\\n\",\n    \"1. How to dispatch span and events\\n\",\n    \"2. How to register those events as OpenTelemetry traces\\n\",\n    \"\\n\",\n    \"It's time to use this knowledge to build and instrument a workflow!\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"AfjtWqy3Qwbn\"\n   },\n   \"source\": [\n    \"The workflow that we want to build is very simple, and involves using OpenAI to analyze some short novels and breaking them down in different parts such as introduction, development of the plot and conclusion.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"OcxSqxE7Vkx0\"\n   },\n   \"source\": [\n    \"### 4.1 Define custom events\\n\",\n    \"\\n\",\n    \"The first thing that we need to do is to define the custom event that we will us throughout our framework.\\n\",\n    \"\\n\",\n    \"We can do it by subclassing the general `Event` classes that the `llama-index-workflows` package provides us with\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {\n    \"id\": \"5wJ_xQ-dVacH\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from workflows.events import Event, StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InputTextEvent(StartEvent):\\n\",\n    \"    input_text: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class AnalyzedTextEvent(StopEvent):\\n\",\n    \"    introduction: str\\n\",\n    \"    development: str\\n\",\n    \"    conclusion: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ProgressEvent(Event):\\n\",\n    \"    msg: str\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"gzoPnxIpn_JJ\"\n   },\n   \"source\": [\n    \"### 4.2 Define custom resources\\n\",\n    \"\\n\",\n    \"[Resources](https://docs.llamaindex.ai/en/stable/understanding/workflows/resources/) are a way of performing dependency injection in workflow steps.\\n\",\n    \"\\n\",\n    \"We will need just one resource, i.e. an LLM able to produce a structured output that aligns with the text analysis we want to perform.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"x0di0zk4pUwY\"\n   },\n   \"source\": [\n    \"We need to specify a schema for the structured output first:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {\n    \"id\": \"FebjynuqiZMu\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from pydantic import BaseModel, Field\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class TextAnalysis(BaseModel):\\n\",\n    \"    introduction: str = Field(description=\\\"Introduction of the novel\\\")\\n\",\n    \"    development: str = Field(description=\\\"Development of the novel\\\")\\n\",\n    \"    conclusion: str = Field(description=\\\"Conclusion of the novel\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"AyYTvTsq76JW\"\n   },\n   \"source\": [\n    \"Let's then initialize an OpenAI LLM as a structured LLM:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"vWZxGUpVOFd7\",\n    \"outputId\": \"65339f71-a28b-4379-a557-f2d24ff84555\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"··········\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from getpass import getpass\\n\",\n    \"\\n\",\n    \"os.environ[\\\"OPENAI_API_KEY\\\"] = getpass()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {\n    \"id\": \"ZRWbHkAjOXJF\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"\\n\",\n    \"llm = OpenAI(model=\\\"gpt-4.1\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {\n    \"id\": \"-Zkt11QY8FAJ\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"struct_llm = llm.as_structured_llm(TextAnalysis)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"ilPjdAXlzhEZ\"\n   },\n   \"source\": [\n    \"Let's now define the function that would get us our LLM as resource:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {\n    \"id\": \"oRiKpN1QzrD3\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index.core.llms.structured_llm import StructuredLLM\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_llm(*args, **kwargs) -> StructuredLLM:\\n\",\n    \"    return struct_llm\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"KwxmTX030oHH\"\n   },\n   \"source\": [\n    \"### 4.3 Create the workflow\\n\",\n    \"\\n\",\n    \"Finally, after defining events and resources, we can create our workflow:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {\n    \"id\": \"dauCQq1a8uTp\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from llama_index.core.llms import ChatMessage\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class TextAnalysisWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def analyze_text(\\n\",\n    \"        self,\\n\",\n    \"        event: InputTextEvent,\\n\",\n    \"        ctx: Context,\\n\",\n    \"        llm: Annotated[StructuredLLM, Resource(get_llm)],\\n\",\n    \"    ) -> AnalyzedTextEvent:\\n\",\n    \"        response = await llm.achat(\\n\",\n    \"            messages=[\\n\",\n    \"                ChatMessage(\\n\",\n    \"                    role=\\\"user\\\",\\n\",\n    \"                    content=f\\\"Analyze the following text: {event.input_text}\\\",\\n\",\n    \"                )\\n\",\n    \"            ]\\n\",\n    \"        )\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Text analyzed successfully\\\"))\\n\",\n    \"        response_json = json.loads(response.message.content)\\n\",\n    \"        return AnalyzedTextEvent(**response_json)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"BVtoh_cvBCg3\"\n   },\n   \"source\": [\n    \"Now we will run the workflow as-is, an you will already see that it produces OpenTelemtry traces.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"k1yUKH2YBcYE\",\n    \"outputId\": \"f5bc1b27-3a48-4689-9293-5945c8158175\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\\n\",\n      \"                                 Dload  Upload   Total   Spent    Left  Speed\\n\",\n      \"\\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\\r100  1297  100  1297    0     0   7581      0 --:--:-- --:--:-- --:--:--  7584\\r100  1297  100  1297    0     0   7562      0 --:--:-- --:--:-- --:--:--  7540\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# first let's get some data\\n\",\n    \"! curl https://raw.githubusercontent.com/run-llama/workflows-observability-support-data/main/data/short-stories/short_story.txt > short_story.txt\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\",\n     \"height\": 120\n    },\n    \"id\": \"ZzlU5fUdBqQ3\",\n    \"outputId\": \"92696a54-8d1a-482f-8a69-7cb1b5917a98\"\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"application/vnd.google.colaboratory.intrinsic+json\": {\n       \"type\": \"string\"\n      },\n      \"text/plain\": [\n       \"'Clara wandered through the old town library, seeking quiet more than books. On a dusty shelf near the back, she pulled down a forgotten novel, its spine cracked and pages yellowed. As she flipped it open, a folded letter slipped out and fluttered to the floor. Curious, she picked it up and read the faded ink: a heartfelt message from a soldier named James to someone named Eleanor, dated 1943. He spoke of love, hope, and his promise to return from war.\\\\n\\\\nUnable to shake the letter from her mind, Clara began digging into the town’s history. She scoured archives, interviewed elderly residents, and traced records through the war memorials. Piece by piece, the story came together: James had never made it home. Eleanor had waited, never knowing why he stopped writing. After weeks of searching, Clara finally found her—Eleanor, now 98, living in a quiet nursing home just outside town.\\\\n\\\\nWhen Clara placed the letter in Eleanor’s hands, the old woman wept. Her voice trembled as she read James’s words for the first time. “I always thought he would have written,” she whispered. The letter, lost for decades, had found its way home. As Clara left the nursing home, heart full, she knew this was only the beginning—of her own journey into forgotten stories and voices long silenced.'\"\n      ]\n     },\n     \"execution_count\": 10,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"# Let's read these data\\n\",\n    \"\\n\",\n    \"with open(\\\"short_story.txt\\\", \\\"r\\\") as f:\\n\",\n    \"    text = f.read()\\n\",\n    \"text\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Crk4r4WSARIm\"\n   },\n   \"source\": [\n    \"We will now use a different instrumentation object, but be careful: you might need to restart the notebook session and re-run all cells apart from the one where we instantiate and start the `instrumentor` object to make it work.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 11,\n   \"metadata\": {\n    \"id\": \"4BgHaqhGCKUX\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# let's export the spans to a different files, and then start the instrumentation\\n\",\n    \"se_1 = FileSpanExporter(file_path=\\\"workflow_1.json\\\")\\n\",\n    \"\\n\",\n    \"instrumentor_1 = LlamaIndexOpenTelemetry(\\n\",\n    \"    span_exporter=se_1,\\n\",\n    \"    service_name_or_resource=\\\"tracing.a.workflow.1\\\",\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"instrumentor_1.start_registering()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 12,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"cOYzx6hSBJ_J\",\n    \"outputId\": \"bd4c1418-98d0-4c90-acaa-ba39f7bf4e64\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Text analyzed successfully\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"wf = TextAnalysisWorkflow(timeout=800)\\n\",\n    \"\\n\",\n    \"handler = wf.run(start_event=InputTextEvent(input_text=text))\\n\",\n    \"async for ev in handler.stream_events():\\n\",\n    \"    if isinstance(ev, ProgressEvent):\\n\",\n    \"        print(ev.msg, flush=True)\\n\",\n    \"\\n\",\n    \"result = await handler\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 13,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"H_2kI8U7Dm9b\",\n    \"outputId\": \"cfa0605b-1e37-4f92-a7bd-8184762f4f43\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Introduction\\n\",\n      \"\\n\",\n      \" Clara wandered through the old town library, seeking quiet more than books. On a dusty shelf near the back, she pulled down a forgotten novel, its spine cracked and pages yellowed. As she flipped it open, a folded letter slipped out and fluttered to the floor. Curious, she picked it up and read the faded ink: a heartfelt message from a soldier named James to someone named Eleanor, dated 1943. He spoke of love, hope, and his promise to return from war.\\n\",\n      \"\\n\",\n      \"--\\n\",\n      \"\\n\",\n      \"Development\\n\",\n      \"\\n\",\n      \" Unable to shake the letter from her mind, Clara began digging into the town’s history. She scoured archives, interviewed elderly residents, and traced records through the war memorials. Piece by piece, the story came together: James had never made it home. Eleanor had waited, never knowing why he stopped writing. After weeks of searching, Clara finally found her—Eleanor, now 98, living in a quiet nursing home just outside town.\\n\",\n      \"\\n\",\n      \"--\\n\",\n      \"\\n\",\n      \"Conclusion\\n\",\n      \"\\n\",\n      \" When Clara placed the letter in Eleanor’s hands, the old woman wept. Her voice trembled as she read James’s words for the first time. “I always thought he would have written,” she whispered. The letter, lost for decades, had found its way home. As Clara left the nursing home, heart full, she knew this was only the beginning—of her own journey into forgotten stories and voices long silenced.\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(\\\"Introduction\\\\n\\\\n\\\", result.introduction)\\n\",\n    \"print(\\\"\\\\n--\\\\n\\\")\\n\",\n    \"print(\\\"Development\\\\n\\\\n\\\", result.development)\\n\",\n    \"print(\\\"\\\\n--\\\\n\\\")\\n\",\n    \"print(\\\"Conclusion\\\\n\\\\n\\\", result.conclusion)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"cmiYCDstDzKz\"\n   },\n   \"source\": [\n    \"As you can see from the output of the cell where we executed the workflow, our OpenTelemtry instrumentation has wrote several spans to the traces file. We can confirm by printing some of the records out:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 14,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"et8hRebkFDlH\",\n    \"outputId\": \"4f06ee27-c6d7-4ea4-dd33-56ddf8bd99ee\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"OpenAI.astructured_predict-4832b122-fc75-47bf-a6f0-c4b8fe609fe9\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0xead97a6267821eead6f3e36efd510cc5\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xbef1f9081a428b3e\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x5867130dc1f850d1\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-07T12:45:54.794486Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-07T12:45:58.745575Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"StructuredLLM.achat-c86a3a30-447a-4ad1-9131-7641ec815908\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0xead97a6267821eead6f3e36efd510cc5\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x5867130dc1f850d1\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x1f9377ce2c5e5230\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-07T12:45:54.786104Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-07T12:45:58.745892Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"LLMChatStartEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-07T12:45:58.745867Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"c2ec830d-6b6a-499b-b818-664a4f676e5f\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"StructuredLLM.achat-c86a3a30-447a-4ad1-9131-7641ec815908\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"LLMChatStartEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        },\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"LLMChatEndEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-07T12:45:58.745880Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"0895e092-6768-4853-94cc-d51804b544ba\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"StructuredLLM.achat-c86a3a30-447a-4ad1-9131-7641ec815908\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"LLMChatEndEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"TextAnalysisWorkflow.analyze_text-10726e0b-637a-481f-9b67-b57bab75c289\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0xead97a6267821eead6f3e36efd510cc5\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x1f9377ce2c5e5230\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x12ae7fc4571fe790\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-07T12:45:54.785683Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-07T12:45:58.746056Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"Workflow._done-c50fb38b-ff7c-4884-82cb-4e9c8eeb3d24\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0xead97a6267821eead6f3e36efd510cc5\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xb6f768698873f129\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x12ae7fc4571fe790\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-07T12:45:58.747199Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-07T12:45:58.747395Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"ERROR\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"SpanDropEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-07T12:45:58.747379Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"e67cccad-25aa-44bf-8a59-fcc48bee0c35\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"Workflow._done-c50fb38b-ff7c-4884-82cb-4e9c8eeb3d24\\\",\\n\",\n      \"                \\\"err_str\\\": \\\"\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"SpanDropEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"Workflow.run-44e3c9de-fbc9-4aab-87fe-000d77db44c3\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0xead97a6267821eead6f3e36efd510cc5\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x12ae7fc4571fe790\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": null,\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-07T12:45:54.772105Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-07T12:45:58.747988Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"with open(\\\"workflow_1.json\\\", \\\"r\\\") as f:\\n\",\n    \"    lines = f.readlines()\\n\",\n    \"    for line in lines[-5:]:\\n\",\n    \"        print(json.dumps(json.loads(line), indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"zQVUbgEBFRwt\"\n   },\n   \"source\": [\n    \"You could also create a workflow that has customized events, in our case that would be:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"metadata\": {\n    \"id\": \"aR3C78CNF-iI\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# define base events\\n\",\n    \"class TextAnalyzedWorkflowEvent(BaseEvent):\\n\",\n    \"    success: bool\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InputTextWorkflowEvent(BaseEvent):\\n\",\n    \"    input_txt: str\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 21,\n   \"metadata\": {\n    \"id\": \"vUZdNOpyFq5M\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# create an instrumented workflow\\n\",\n    \"class TextAnalysisWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def analyze_text(\\n\",\n    \"        self,\\n\",\n    \"        event: InputTextEvent,\\n\",\n    \"        ctx: Context,\\n\",\n    \"        llm: Annotated[StructuredLLM, Resource(get_llm)],\\n\",\n    \"    ) -> AnalyzedTextEvent:\\n\",\n    \"        dispatcher.event(InputTextWorkflowEvent(input_txt=event.input_text))\\n\",\n    \"        response = await llm.achat(\\n\",\n    \"            messages=[\\n\",\n    \"                ChatMessage(\\n\",\n    \"                    role=\\\"user\\\",\\n\",\n    \"                    content=f\\\"Analyze the following text: {event.input_text}\\\",\\n\",\n    \"                )\\n\",\n    \"            ]\\n\",\n    \"        )\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Text analyzed successfully\\\"))\\n\",\n    \"        dispatcher.event(TextAnalyzedWorkflowEvent(success=True))\\n\",\n    \"        response_json = json.loads(response.message.content)\\n\",\n    \"        return AnalyzedTextEvent(**response_json)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"VH_u8WK06NNS\"\n   },\n   \"source\": [\n    \"As you can see, in this case we do not put the `@dispatcher.span` decorator before the function, since (as we could see with the example above) every step is already instrumented with a dispatcher!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 22,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"mazDgFljHY5J\",\n    \"outputId\": \"18140489-bb10-472d-ce0e-0c8ed84e9710\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Writing 1 spans to workflow_1.json\\n\",\n      \"Text analyzed successfully\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Let's re-run the custom instrumented workflow\\n\",\n    \"\\n\",\n    \"wf = TextAnalysisWorkflow(timeout=800)\\n\",\n    \"\\n\",\n    \"handler = wf.run(start_event=InputTextEvent(input_text=text))\\n\",\n    \"async for ev in handler.stream_events():\\n\",\n    \"    if isinstance(ev, ProgressEvent):\\n\",\n    \"        print(ev.msg, flush=True)\\n\",\n    \"\\n\",\n    \"result = await handler\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"iPx3T4j5IiaO\"\n   },\n   \"source\": [\n    \"If we now print workflow_1.json, we will see that the tracer has registered also our custom spans:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 23,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"collapsed\": true,\n    \"id\": \"sMf5cjYVIsif\",\n    \"outputId\": \"4d672e35-cde6-40e4-db40-8a5f0ef6a2b4\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"TextAnalysisWorkflow.analyze_text-cc255572-a7e4-4206-b4b4-00ca43d2fd2f\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x320aed58837e56db846d4db6bd4f9fdb\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x3c341ffbd43ab067\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x80995e414cd6ad4e\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-07T12:48:33.017811Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-07T12:48:35.417729Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"BaseEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-07T12:48:35.417710Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"d3e10d9d-0cb9-4bf1-923f-91f3677ed27c\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"TextAnalysisWorkflow.analyze_text-cc255572-a7e4-4206-b4b4-00ca43d2fd2f\\\",\\n\",\n      \"                \\\"input_txt\\\": \\\"Clara wandered through the old town library, seeking quiet more than books. On a dusty shelf near the back, she pulled down a forgotten novel, its spine cracked and pages yellowed. As she flipped it open, a folded letter slipped out and fluttered to the floor. Curious, she picked it up and read the faded ink: a heartfelt message from a soldier named James to someone named Eleanor, dated 1943. He spoke of love, hope, and his promise to return from war.\\\\n\\\\nUnable to shake the letter from her mind, Clara began digging into the town\\\\u2019s history. She scoured archives, interviewed elderly residents, and traced records through the war memorials. Piece by piece, the story came together: James had never made it home. Eleanor had waited, never knowing why he stopped writing. After weeks of searching, Clara finally found her\\\\u2014Eleanor, now 98, living in a quiet nursing home just outside town.\\\\n\\\\nWhen Clara placed the letter in Eleanor\\\\u2019s hands, the old woman wept. Her voice trembled as she read James\\\\u2019s words for the first time. \\\\u201cI always thought he would have written,\\\\u201d she whispered. The letter, lost for decades, had found its way home. As Clara left the nursing home, heart full, she knew this was only the beginning\\\\u2014of her own journey into forgotten stories and voices long silenced.\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"BaseEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        },\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"BaseEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-07T12:48:35.417721Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"69c55c3f-ded7-492a-a1cf-c260ea15155a\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"TextAnalysisWorkflow.analyze_text-cc255572-a7e4-4206-b4b4-00ca43d2fd2f\\\",\\n\",\n      \"                \\\"success\\\": true,\\n\",\n      \"                \\\"class_name\\\": \\\"BaseEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"with open(\\\"workflow_1.json\\\", \\\"r\\\") as f:\\n\",\n    \"    lines = f.readlines()\\n\",\n    \"    print(json.dumps(json.loads(lines[-3]), indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"NImiRjEsCI_v\"\n   },\n   \"source\": [\n    \"As you can see, these two `BaseEvent` instances are exactly our custom events (we can recognize them by the attributes, `input_txt` and `success`)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"MIBb_qieC7ft\"\n   },\n   \"source\": [\n    \"This is all for Part 1, in Part 2 we will be diving deeper into a more complex workflow, with many more events and more room for customization... See you there!\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"name\": \"python\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/observability/workflows_observability_pt2.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Vx09YKJjycji\"\n   },\n   \"source\": [\n    \"# Workflow Observability - Part 2\\n\",\n    \"\\n\",\n    \"## Use native instrumentation from LlamaIndex + OpenTelemetry to fine-grain tracing in your code!\\n\",\n    \"\\n\",\n    \"In this notebook, we will go through a more complex and complete example of how to use instrumentation natively implemented in `llama-index` (combined with OpenTelemetry) to define costum span and events within your code. Before we get started:\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"⭐ Don'f forget to star the `llama-index-workflows` [GitHub repo](https://github.com/run-llama/llama-agents)\\n\",\n    \"\\n\",\n    \"🦙☁ Register to [LlamaCloud](https://cloud.llamaindex.ai) not to miss out on all our awesome products\\n\",\n    \"\\n\",\n    \"If you have feedback, questions, issues, or you just want to follow us not to miss out on any news, please find us on:\\n\",\n    \"\\n\",\n    \"[![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/run-llama/)\\n\",\n    \"[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/eN6D2HQ4aX)\\n\",\n    \"[![X](https://img.shields.io/badge/@llama__index-%23000000.svg?style=for-the-badge&logo=X&logoColor=white)](https://x.com/@llama_index)\\n\",\n    \"[![Bluesky](https://img.shields.io/badge/Bluesky-0285FF?style=for-the-badge&logo=Bluesky&logoColor=white)](https://bsky.app/profile/llamaindex.bsky.social)\\n\",\n    \"[![LinkedIn](https://img.shields.io/badge/linkedin-%230077B5.svg?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/llamaindex/)\\n\",\n    \"\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"PCIOKSNJzwJB\"\n   },\n   \"source\": [\n    \"## 1. Setting up\\n\",\n    \"\\n\",\n    \"Before diving deep into all of this, let's install all the needed dependencies.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"9yQ8Z5zByLuj\",\n    \"outputId\": \"11c71357-ec47-4d90-e86e-16f54bd367d9\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\u001b[?25l   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m0.0/40.4 kB\\u001b[0m \\u001b[31m?\\u001b[0m eta \\u001b[36m-:--:--\\u001b[0m\\r\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m40.4/40.4 kB\\u001b[0m \\u001b[31m3.1 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[?25h\\u001b[?25l   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m0.0/282.1 kB\\u001b[0m \\u001b[31m?\\u001b[0m eta \\u001b[36m-:--:--\\u001b[0m\\r\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m282.1/282.1 kB\\u001b[0m \\u001b[31m12.9 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[?25h\\u001b[?25l   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m0.0/7.6 MB\\u001b[0m \\u001b[31m?\\u001b[0m eta \\u001b[36m-:--:--\\u001b[0m\\r\\u001b[2K   \\u001b[91m━━━━━━━━━━━━━━━━━━━\\u001b[0m\\u001b[91m╸\\u001b[0m\\u001b[90m━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m3.8/7.6 MB\\u001b[0m \\u001b[31m114.9 MB/s\\u001b[0m eta \\u001b[36m0:00:01\\u001b[0m\\r\\u001b[2K   \\u001b[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m\\u001b[91m╸\\u001b[0m \\u001b[32m7.6/7.6 MB\\u001b[0m \\u001b[31m119.8 MB/s\\u001b[0m eta \\u001b[36m0:00:01\\u001b[0m\\r\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m7.6/7.6 MB\\u001b[0m \\u001b[31m75.2 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m65.8/65.8 kB\\u001b[0m \\u001b[31m5.6 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m118.5/118.5 kB\\u001b[0m \\u001b[31m8.4 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m196.2/196.2 kB\\u001b[0m \\u001b[31m7.7 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m1.2/1.2 MB\\u001b[0m \\u001b[31m45.3 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m50.9/50.9 kB\\u001b[0m \\u001b[31m4.4 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[2K   \\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\u001b[0m \\u001b[32m129.3/129.3 kB\\u001b[0m \\u001b[31m10.9 MB/s\\u001b[0m eta \\u001b[36m0:00:00\\u001b[0m\\n\",\n      \"\\u001b[?25h\\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\\n\",\n      \"ipython 7.34.0 requires jedi>=0.16, which is not installed.\\u001b[0m\\u001b[31m\\n\",\n      \"\\u001b[0m\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"! pip install -q llama-index-workflows llama-index-instrumentation llama-index-llms-openai llama-index-observability-otel llama-cloud-services llama-index-indices-managed-llama-cloud llama-cloud llama-index-embeddings-openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"iDrSR1Xw6Wvn\"\n   },\n   \"source\": [\n    \"## 2. Set up OpenTelemetry\\n\",\n    \"\\n\",\n    \"As we did in part 1, we will use the LlamaIndex integration with OpenTelemtry (`llama-index-observability-otel`) and we will create our custom `SpanExporter`. You can find many other exporters in [OpenTelemetry docs](https://opentelemetry.io/docs/languages/python/exporters/).\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"lKIvFShXD4qz\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import os\\n\",\n    \"from os import linesep\\n\",\n    \"from pathlib import Path\\n\",\n    \"from typing import Callable, Sequence\\n\",\n    \"\\n\",\n    \"from llama_index.observability.otel import LlamaIndexOpenTelemetry\\n\",\n    \"from opentelemetry.sdk.trace import ReadableSpan\\n\",\n    \"from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class FileSpanExporter(SpanExporter):\\n\",\n    \"    \\\"\\\"\\\"Implementation of :class:`SpanExporter` that prints spans to the\\n\",\n    \"    console.\\n\",\n    \"\\n\",\n    \"    This class can be used for diagnostic purposes. It prints the exported\\n\",\n    \"    spans to the console STDOUT.\\n\",\n    \"    \\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    def __init__(\\n\",\n    \"        self,\\n\",\n    \"        service_name: str | None = None,\\n\",\n    \"        file_path: os.PathLike[str] | None = None,\\n\",\n    \"        formatter: Callable[[ReadableSpan], str] = lambda span: json.dumps(\\n\",\n    \"            json.loads(span.to_json())\\n\",\n    \"        )\\n\",\n    \"        + linesep,\\n\",\n    \"    ):\\n\",\n    \"        if not file_path:\\n\",\n    \"            file_path = \\\"traces.json\\\"\\n\",\n    \"        if Path(file_path).exists():\\n\",\n    \"            raise ValueError(f\\\"File {file_path} already exists\\\")\\n\",\n    \"        self.file_path = file_path\\n\",\n    \"        self.formatter = formatter\\n\",\n    \"        self.service_name = service_name\\n\",\n    \"\\n\",\n    \"    def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:\\n\",\n    \"        print(f\\\"Writing {len(spans)} spans to {self.file_path}\\\")\\n\",\n    \"        if Path(self.file_path).exists():\\n\",\n    \"            mode = \\\"a\\\"\\n\",\n    \"        else:\\n\",\n    \"            mode = \\\"w\\\"\\n\",\n    \"        with open(self.file_path, mode) as out:\\n\",\n    \"            for span in spans:\\n\",\n    \"                out.write(self.formatter(span))\\n\",\n    \"            out.flush()\\n\",\n    \"        return SpanExportResult.SUCCESS\\n\",\n    \"\\n\",\n    \"    def force_flush(self, timeout_millis: int = 30000) -> bool:\\n\",\n    \"        return True\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Y3FvDOByPKyr\"\n   },\n   \"source\": [\n    \"## 4. Instrument a workflow\\n\",\n    \"\\n\",\n    \"Let's take our knowledge of instrumentation to the next level: it is time to use this knowledge to build and instrument a workflow!\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"AfjtWqy3Qwbn\"\n   },\n   \"source\": [\n    \"The workflow that we want to build automates customer support e-mails by extracting important information from them, gather answers to the customers' questions from a company database, ask for feedback from a human employee and, if the feedback is positive, send the email.\\n\",\n    \"\\n\",\n    \"![image.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACGUAAA8ACAYAAABAziKKAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAIZaADAAQAAAABAAAPAAAAAAAiqNhWAABAAElEQVR4AezdB3xddfk/8Kdt2qZpCmXYMjqpjEIrewmKMpXND2SJiArIEhEVAcGBCoryB37sDYKAMmSrIBsEWVLbMgq0tBRKd0tHdvLPyc+AlDbJubknucl9H1955d5zvs9zvt/3uRrbfHpOj4bGLWwECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJ5FeiZ126aESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQINAkIZfggECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQyEBDKyABVSwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAUIbPAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgAwGhjAxQtSRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICGX4DBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMhAQysgAVUsCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAgFCGzwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIAMBoYwMULUkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAhl+AwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBDIQEMrIAFVLAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIBQhs8AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCADAaGMDFC1JECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgIZfgMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQyEBDKyABVSwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAUIbPAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgAwGhjAxQtSRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICGX4DBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMhAQysgAVUsCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAgFCGzwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIAMBoYwMULUkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAhl+AwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBDIQEMrIAFVLAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIBQhs8AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCADAaGMDFC1JECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgIZfgMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQyEBDKyABVSwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAUIbPAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgAwGhjAxQtSRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICGX4DBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMhAQysgAVUsCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAgFCGzwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIAMBoYwMULUkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAhl+AwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBDIQEMrIAFVLAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIBQhs8AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCADAaGMDFC1JECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgIZfgMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQyEBDKyABVSwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAUIbPAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgAwGhjAxQtSRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICGX4DBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMhAQysgAVUsCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAgFCGzwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIAOBkgx6akmAAAECBAgQIECAAAECBAgQIEAgZ4HFiyujsrIq6uuroqqqqvF1zX++10ZtbW3OfRUSINA9BEpKSqK0tCT69u3b+L130/eePZPXfaO8vLR7LNIqCBAgQIAAAQIEuo2AUEa3uZQWQoAAAQIECBAgQIAAAQIECBDoGgINDQ0xY+qMmPT21JgyZVpMnvyf75OmxtyFC7vGIsySAIGCFRi86moxcuSwWGf94bHOOsObXq+33vAYNGhQwc7ZxAgQIECAAAECBLqvQI/GPwQ3dN/lWRkBAgQIECBAgAABAgQIECBAgEBnCyTBi5de+ne8/PIrMWHCpJj8+pSorKvp7Gk5PwECRSbQv0/fxqDGiPjMZzaMTTYZHZtuulEMHTq0yBQslwABAgQIECBAoKMFhDI6Wtz5CBAgQIAAAQIECBAgQIAAAQLdWKCioiLGjXstxv9r4odBjHkffNCNV2xpBAh0ZYFBq6wam222UYzdbExjSGNsjBmzbtPjULrymsydAAECBAgQIECgsASEMgrrepgNAQIECBAgQIAAAQIECBAgQKDLCdTU1MQTDz8b99z7t3jkiaejurquy63BhAkQIJAIlPXuEzvutkPsvfcusf32W0avXr3AECBAgAABAgQIEGiXgFBGu/gUEyBAgAABAgQIECBAgAABAgSKV+DFFyfEvY1BjL/e92jMX+RuGMX7SbByAt1TILmLxu577Bz77L9LbLjh+t1zkVZFgAABAgQIECCQuYBQRubETkCAAAECBAgQIECAAAECBAgQ6D4CM6bOiNvvfCDuu+/BeHv6jO6zMCshQIBACwLrrDM8/mfv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IECAAAECBHIXmDdvURx99Pdi/PhJuTdZpnLUyKHxq7NPi003G7PMEW8JECBAYHkCTz35XJz543PjvZmzlnc4p33bbb1ZXHz52VFWVpZTvSICBAgQIECAAIHCEBDKKIzrYBYECBAgQIAAAQIECBAgQIAAgdQCM2fOi8MP/068/fY7qWtXVPD97x0dRx9z2IoO20+AAAECKxCoqqqOSy66Lq646g8rGJF+95gx68b1118cAwb0S1+sggABAgQIECBAoCAEhDIK4jKYBAECBAgQIECAAAECBAgQIEAgncCiRRVxyCHHxhtvTE5XuILRo0YNj0svPSdGjBiyghF2EyBAgEBbBCa9/lZ8+6gf5e2uGVtsMTauu+786NOnT1tObwwBAgQIECBAgECBCfQssPmYDgECBAgQIECAAAECBAgQIECAQCsC1dXVccwxP8hbIONLu+4Qd955tUBGK+4OEyBAoC0C660/Ku65//r47Labt2V4q2NeeGF8nPydn0Z9fX2rYw0gQIAAAQIECBAoPAF3yii8a2JGBAgQIECAAAECBAgQIECAAIEVCiS/lDvx2B/HQ489vcIxbT1QWtIrTjn1hPjq1/Zva4lxBAgQINBGgYaGhrjishvjoguvjto21rQ07IAD9ohf/epHLQ1xjAABAgQIECBAoAAFhDIK8KKYEgECBAgQIECAAAECBAgQIEBgRQI//vFv4vbb71/R4TbvHzx49bj4ol/FZzYe3eYaAwkQIEAgvcDzz70cJ55wesxbuDh98TIVJx73jTj+u99YZq+3BAgQIECAAAEChSzg8SWFfHXMjQABAgQIECBAgAABAgQIECDwXwIXXXR9XgIZ644aEXfdda1Axn/ZekmAAIGsBLbcapO4/c5rYshaa7T7FP976XWNPwfubXcfDQgQIECAAAECBDpOQCij46ydiQABAgQIECBAgAABAgQIECCQs0DyS7iLL7425/rmws023ij+eNvlseqqA5t3+U6AAAECGQusPWTN+NPtV8T6667T7jOdeeZ58dhDT7a7jwYECBAgQIAAAQIdI+DxJR3j7CwECBAgQIAAAQIECBAgQIAAgZwF/v3v1+Kgg46J+vr6nHskhTt8bpu4+NJfRp8+fdrVRzEBAgQI5CawZMnSOPKbP4iXXp6QW4P/VJX26h33/OWGGD58SLv6KCZAgAABAgQIEMhe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3rumrlFAgAABAsUhkMvPiAWLF8WLL05sM1Byl4zq6uqoqalpc42BBAgQIECAAAECHwkIZXxk4RUBAgQIECBAgAABAgQIECBAIDOBRx99NnXvtQYPik03G5O6TgEBAgQIFIfADl/YNsrLy1Iv9pknXkhVkwQzKisrU91hI9UJDCZAgAABAgQIdGMBoYxufHEtjQABAgQIECBAgAABAgQIECgcgeeefjH1ZPbe110yUqMpIECAQBEJ9O5dErt/acfUK/7XhPGpa5qDGcl3GwECBAgQIECAQNsFhDLabmUkAQIECBAgQIAAAQIECBAgQCAngWnT3o+Z8+amrt1xp+1T1yggQIAAgeIS2HGn7VIv+LXXJjfd+SJNYRLGqKura3qUSZo6YwkQIECAAAECxS4glFHsnwDrJ0CAAAECBAgQIECAAAECBDIXGDduXOpzlJeWxdixG6SuU0CAAAECxSWw1dabRknKJdfX18fLL7+WsioiCWZUVVVFbW1t6loFBAgQIECAAIFiFRDKKNYrb90ECBAgQIAAAQIECBAgQIBAhwk8//y/U59r0y3GRM+e/uomNZwCAgQIFJlA//5lMXr0eqlXPf6lialrkoLmYIbHmOTEp4gAAQIECBAoQgF/si/Ci27JBAgQIECAAAECBAgQIECAQMcK5BLK2LrxXz7bCBAgQIBAWwS23HqTtgz72JiXcgxlJE2Sx5i4W8bHOL0hQIAAAQIECKxQQChjhTQOECBAgAABAgQIECBAgAABAgTaLzBv3qKYPHlq6kbJ7ehtBAgQIECgLQK5/MyYOPG1nIMVyV0yKisrI3kMio0AAQIECBAgQKBlAaGMln0cJUCAAAECBAgQIECAAAECBAi0S+Dl58elri8vLYuxYzdIXaeAAAECBIpTIAlllKRcemVdTbzyylspqz4angQzqqurP9rhFQECBAgQIECAwHIFhDKWy2InAQIECBAgQIAAAQIECBAgQCA/Aq+8Oil1o023GBM9e/prm9RwCggQIFCkAv37l8Xo0eulXv2kSZNT1zQXNIcykkeZ2AgQIECAAAECBFYs4E/3K7ZxhAABAgQIECBAgAABAgQIECDQboGp785I3WOjjdL/Yi31SRQQIECAQLcSGJ3Dz4733nu3XQbNwYzku40AAQIECBAgQGD5AkIZy3exlwABAgQIECBAgAABAgQIECCQ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uQkIZeTmpooAAQIECBAgQIAAAQIECBAgQIAAAQIECBBog0ASyhDMaAOUIQQIECBAgEC3FBDK6JaX1aIIECBAgAABAgQIECBAgAABAgQIECBAgEBhCCSBjLq6usKYjFkQIECAAAECBDpYQCijg8GdjgABAgQIECBAgAABAgQIECBAgAABAgQIFJuAO2UU2xW3XgIECBAgQKBZQCijWcJ3AgQIECBAgAABAgQIECBAgAABAgQIECBAIBMBoYxMWDUlQIAAAQIEuoCAUEYXuEimSIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHQ9AaGMrnfNzJgAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDoAgJCGV3gIpkiAQIECBAgQIAAAQIECBAgQIAAAQIECBDoygIeX9KVr565EyBAgAABAu0REMpoj55aAgQIECBAgAABAgQIECBAgAABAgQIECBAoFWBJJQhmNEqkwEECBAgQIBANxQQyuiGF9WSCBAgQIAAAQIECBAgQIAAAQIECBAgQIBAIQkIZBTS1TAXAgQIECBAoCMFhDI6Utu5CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaIREMoomkttoQQIECBAgAABAgQIECBAgAABAgQIECBAoHME3Cmjc9ydlQABAgQIEOh8AaGMzr8GZkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAh0QwGhjG54US2JAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6HwBoYzOvwZmQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHRDAaGMbnhRLYkAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDofAGhjM6/BmZAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIdEMBoYxueFEtiQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOh8gZLOn4IZECBAgAABAgQIECBAgAABAgQIEOg6Auf99vK4+qqbVzjhoUPXirvvuz769Std4Zh8HmhtPiNGDIk77762w+aTz7XpRYAAAQIECBAgQIAAga4u4E4ZXf0Kmj8BAgQIECBAgAABAgQIECBAgEBBCbzzznvx/oxZBTOnt9+eXlDzKRgYEyFAgAABAgQIECBAgEAHCAhldACyUxAgQIAAAQIECBAgQIAAAQIECBSXQENDQ0EtuNDmU1A4JkOAAAECBAgQIECAAIEMBYQyMsTVmgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECheAaGM4r32Vk6AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkKCCUkSGu1gQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDxCghlFO+1t3ICBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgQwGhjAxxtSZAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgSKV0Aoo3ivvZUTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECGQoIZWSIqzUBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQvAJCGcV77a2cAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQyFBAKCNDXK0JECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB4hUQyijea2/lBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQIYCQhkZ4mpNgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIFK+AUEbxXnsrJ0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBDIUKMmwt9YECBAgQIAAAQIECBAgQIAAAQIECBSZwLRp78a/XpoQb74xJSZPnhazZs2JuXPmxaJFS6Kqqjpqamqid++S6N2nd/Tt0ycGrrJyrL7aKjFo8Kdi5MihMWLksNhozPoxYsSQTpdbvHhJPPfPf8X48a/Fa6++Ge+9N7NpPRVLKxrXURv9+pVGeXn/xrmvHqNGDY/11lsntv3sFrHB6E93+NwXLVoctbV1KzzvSiuVR69evVZ4vPnA0sa1Pf3U8/HoI083rXlO47WbN29BlJT0itUar9N6668TX9xx+9h3v92iT+P1sxEgQIAAAQIECBAg0LKAUEbLPo4SIECAAAECBAgQIECAAAECBAgQINCKwAvPj4u/PPBIPPz3p2LmzNmtjI6mQEMSali6pCLmz18YUxrDG8tuAweu1BRw2HGn7WLnXT4fpaV9lx2Syfvq6up46MEn4s47HmgKZLQUdFiyZGkkX8max//71Q/nk4Q09txrlzj00H1j7SFrfrg/yxfHH3NaPN94HVa0jVxnWDzw15tWdDiSdV91xR/iumv/2LSmZQfW1dU1hVKSYMpjjz4TEye+Hj8/6wfLDvOeAAECBAgQIECAAIFlBIQylgHxlgABAgQIECBAgAABAgQIECBAgACB1gWSX9Lffdff4tqrb4m33praekHKEQsWfNAU9EjCHgMGlMdhX9s/TjzpWym7tH14EhL50x/viSsuuzFmz57b9sLljJw1c06Ty3XX3Bpf3n3H+MEpx8aaaw5azsiO25UEX5KASXLHi2W3qVOnxwnHnh5vvvn2sodW+P7tKe+s8JgDBAgQIECAAAECBAh8JNDzo5deESBAgAABAgQIECBAgAABAgQIECBAoHWBl/81IfbZ84j48Wm/ziSQsewMkkdzXHbpDY2PQFm87KG8vJ844fXYf78j45dnXdDuQMZ/T6ihoSEeuP/h2H23rzaFNP77WKG8fv21t+LgA49NFchI5t5QX18oSzAPAgQIECBAgAABAgUtIJRR0JfH5AgQIECAAAECBAgQIECAAAECBAgUlkASjjj04OM7JIyx7MpbepTIsmPb+v7WW+5uDCUcE29MmtzWktTjKiur4rfnXhYnnfiTWLq0InV9VgUzZsyKb33j5FjQ+AgZGwECBAgQIECAAAEC2Qh4fEk2rroSIECAAAECBAgQIECAAAECBAgQ6FYCyV0fzvzxuXHH7fd3m3VdcP5VTY8r6agF/e2vj8WcOfPi6mvPi9LSvh112uWeJ3lcy3eOOz3mzp2/3ON2EiBAgAABAgQIECCQHwF3ysiPoy4ECBAgQIAAAQIECBAgQIAAAQIEurVAcqeH7hTIuPKKmzo0kNH84XjxhX/H9777k6irq2ve1Snf//eCq2PixEmdcm4nJUCAAAECBAgQIFBMAu6UUUxX21oJECBAgAABAgQIECBAgAABAgQI5CDw2KP/iOuuuTWHysIsefqp5+KC/3dVqsmtM2p4bL752Bg0aPUYOHDl6F9eFosXL4kPFi6KyZOnxb9eGh/J40Dasj326DNx1RV/iGOOO7wtw/M+5tVX34hru9H1zDuQhgQIECBAgAABAgTyKCCUkUdMrQgQIECAAAECBAgQIECAAAECBAh0N4Hq6ur45S8uTLWsDTdcL7baepNYb/1RscYag2K11QZG38bHdfTp0ydqa2ujtvHRGVVV1bFw4QeNX4ti5szZ8e7092PKlGkxccLrmT5SIwlSnHrK2ZE8jqW1bdVVB8bXDj8gvnLQXo1rWKW14fHGG1PiphvviLvu/Gskbi1tl1x8XXxuh21io43Wa2lYJsfO+tn5UV9f32LvgQNXijXWHBRlZf2artnCBYvi3XdnNL7u3Dt8tDhpBwkQIECAAAECBAgUoIBQRgFeFFMiQIAAAQIECBAgQIAAAQIECBAgUCgC99z9YGNgYkar0yltDF0c9rX945BD94211l6j1fEtDXjv3ffj6adfiKee/Gc88fizUVlZ1dLwVMcuuvDamDNnXqs1Xzlwzzj9jO9Gsq62buuuOzJ+ftYPmhxO/u5P4803315haRJu+M3ZF8Xv/3DRCsfk+8AD9z8c06ZOj5f/NWG5rbfZdvPYZ59d47PbbRmDBq/+iTFJkGbcyxObgjOfGrTaJ47bQYAAAQIECBAgQIDAJwWEMj5pYg8BAgQIECBAgAABAgQIECBAgAABAv8RuOO2+1u12GjM+nHh/54Vaw9Zs9WxbRmQhDqSUETytXRJRTz++DMxb+78prs2tKV+RWNmz54bt9x814oOf7j/pz//fhx8yD4fvk/7Igln3Hrb5XHAfkfG229PX2H588+Pi+Rryy03XuGYfB740Q9/udx2mzU+liUJoLR2146+ffs03gFl06av5TaykwABAgQIECBAgACBTwgIZXyCxA4CBAgQIECAAAECBAgQIECAAAECBBKBBQs+iHHjXmkRY/jwIXHdDefHgAHlLY7L9WBZ/37x5d13zLX8Y3U3/f6OqKmp+di+Zd8cdvj+7QpkNPfr378s/veSX8WB+x/d4p0+rrvm1g4LZTTPrfl7z54946STj4ojjzo0evTo0bzbdwIECBAgQIAAAQIE8ijQM4+9tCJAgAABAgQIECBAgAABAgQIECBAoBsJvPrKG9HQ0NDiin502vGZBTJaPHHKg8k67r7rby1WjRg5NE750fEtjklzMLljxlcO3KvFkqefer7pbiAtDsrgYHLXi0svPyeOOvqrAhkZ+GpJgAABAgQIECBAoFlAKKNZwncCBAgQIECAAAECBAgQIECAAAECBD4m8M47733s/bJv+vUrjc99fptldxfk+3+9NCFmzpzd4ty+dvgB0bt3fm8unNx5o6W7UFRXV8eTT/6zxXnl+2Dv3r3j8it/Ezt8Ydt8t9aPAAECBAgQIECAAIFlBIQylgHxlgABAgQIECBAgAABAgQIECBAgACB/xNYsnhJixSrrb5KlJT0anFMoRx89tmXWpxK8piUfff9Uotjcjk4bNjasfEmG7VY+vzz41o8ns+DSUDkvPN/Gttsu3k+2+pFgAABAgQIECBAgMAKBIQyVgBjNwECBAgQIECAAAECBAgQIECAAIFiF+jZq+W/Ppw3d0GXIXr5XxNanOsmjcGJJJiRxbbxxhu22PbNN6a0eDyfB086+ajYZdfP57OlXgQIECBAgAABAgQItCDQ8p+qWih0iAABAgQIECBAgAABAgQIECBAgACB7i2w8koDWlzg0qUVMW7cKy2OKZSDk16f3OJUxn5mdIvH23PwMxu33LujQhlf+OK2cfS3D2vPUtQSIECAAAECBAgQIJBSQCgjJZjhBAgQIECAAAECBAgQIECAAAECBIpFYMQ6w1pd6vnnXRk1NbWtjuvMAcn8Zs2a0+IUNtpo/RaPt+fgkCFrtVg+d+78DjH81TmntTgPBwkQIECAAAECBAgQyL+AUEb+TXUkQIAAAQIECBAgQIAAAQIECBAg0C0ERo9eN0pL+7a4ln8++1Iccfh34623prY4rjMPzp49NxoaGlqcwqqrDmzxeHsODhjQv9XyJUuWtjqmvQNWauXOJ+3tr54AAQIECBAgQIAAgU8KCGV80sQeAgQIECBAgAABAgQIECBAgAABAgQaBfr27ROf32GbVi1eenF87LX74XHst0+N++97OD74YHGrNR05IHnMSmvbSiuVtzYk5+MD2tB7yeIlOfdXSIAAAQIECBAgQIBA4QqUFO7UzIwAAQIECBAgQIAAAQIECBAgQIAAgc4W+PoRB8aDf3u81Wkkd6J47NF/NH317NkzRo/+dGyy6ZgY+5nRMXbsBjGy8VEoPXr0aLVPFgOqKqtabVs+ILtQRlm/fq2ef2lFZatjDCBAgAABAgQIECBAoOsJCGV0vWtmxgQIECBAgAABAgQIECBAgAABAgQ6TGCzzcfGHnvu1HQHjLaetL6+PiZOnNT01VxT1r9fjNlo/Rjzn5DGmDEbxJChazYfzvR7TW1tq/1LSnq1OibTAa08XiXTc2tOgAABAgQIECBAgEBmAkIZmdFqTIAAAQIECBAgQIAAAQIECBAgQKB7CPzsrB/EpNcnxxtvTMl5QUuXVMRzz73c9NXcZODAlWJM4100tthi49h6281i44037LS7aXx+u/0iucOHjQABAgQIECBAgAABAvkUEMrIp6ZeBAgQIECAAAECBAgQIECAAAECBLqhQHl5/7j2hvPjmKN/FBMnvJ63FS5Y8EE89eRzTV9xfsSnBq0Wu+++Uxx62H4xbNjaeTtPWxsld/iwESBAgAABAgQIECBAIJ8Cot/51NSLAAECBAgQIECAAAECBAgQIECAQDcVWH31VeOmmy+Ow7/+lczuZjF71ty44fo/xZd2OTRO+cEvY8aMWd1U07IIECBAgAABAgQIECgWAaGMYrnS1kmAAAECBAgQIECAAAECBAgQIECgnQKlpX3jtB9/J+6659rYcaftMwtnNDQ0xL33PBh7fvlr8ec7/9LOWSsnQIAAAQIECBAgQIBA5wkIZXSevTMTIECAAAECBAgQIECAAAECBAgQ6JIC660/Ki657Ox46OFb4/gTjoh11x2ZyTqWLq2I0089J3577mWZ9C+opj16FNR0TIYAAQIECBAgQIAAgfwIlOSnjS4ECBAgQIAAAQIECBAgQIAAAQIECBSbwNpD1owTTvxm09d7782MF54fFy+9OD7G//vVmDTpraitrcsLybVX3xKlffvEd777rbz0K7Qma601ONZea41Cm5b5ECBAgAABAgQIECCQBwGhjDwgakGAAAECBAgQIECAAAECBAgQIECg2AWSYMHe++za9JVYVFdXx2uvvhXjx78aEye83hTUeOutqZE8miSX7dJLbojNNh8b222/VS7lrdY88fSf41OfWq3VcQYQIECAAAECBAgQIEAgjYBQRhotYwkQIECAAAECBAgQIECAAAECBAgQaJNAnz594jMbj276ai5IHkfyysRJjUGN1+Llf02I5557ORbMX9h8uNXvP/3JefHXB2+OkpJerY41gAABAgQIECBAgAABAoUgIJRRCFfBHAgQIECAAAECBAgQIECAAAECBAjkKDDu5Yk5VnZ8WVlZv9hiy42bviIOivr6+njxhX/HrbfcHX954JFW76Lx7vQZcf99f4999t0t1eR79OjR+vjcbuDRel8jCBAgQIAAAQIECBAoaoGeRb16iydAgAABAgQIECBAgAABAgQIECDQhQVqamri+efHddkV9OzZM7bcapM47/yfxk03XxxDh67V6lruveehVscsO6Bv4107WtsqKitbG+I4AQIECBAgQIAAAQIEUgsIZaQmU0CAAAECBAgQIECAAAECBAgQIECgMAQuveSGwphIHmax2eZj4/obL4zVVlulxW7P/fOlSMIoabbSfqWtDl+0aEmrYwwgQIAAAQIECBAgQIBAWgGhjLRixhMgQIAAAQIECBAgQIAAAQIECBS1QFsehVHfkP2zMJI7ZFx5+U3d6lqstdbgOPGkI1tcU01Nbbw95Z0Wxyx7cJVVVl521yfevzPt3U/ss4MAAQIECBAgQIAAAQLtFRDKaK+gegIECBAgQIAAAQIECBAgQIAAgaISaNNdFz5YnKnJvHkLI6yqpwAAQABJREFU4vvf+1nU19dnep7OaL7Hnju1etoZM2a1Oua/B6y88oAo69/vv3d94vVrr735iX12ECBAgAABAgQIECBAoL0CQhntFVRPgAABAgQIECBAgAABAgQIECBQVAIrDShvdb3z5y9odUyuA6qrq+PEE86I2bPm5tqioOv69y+LVVcd2OIclyxZ2uLx5R0cOXLY8nZ/uO/pp57/8LUXBAgQIECAAAECBAgQyJeAUEa+JPUhQIAAAQIECBAgQIAAAQIECBAoCoGVGu+60No2ccLrrQ3J6XhyZ4wf/fBX8eIL/86pvqsU1dbWtjjV3r17t3h8eQfHjt1gebs/3Ddh/GsxZfK0D997QYAAAQIECBAgQIAAgXwICGXkQ1EPAgQIECBAgAABAgQIECBAgACBohEYOHClVtf63HMvtzom7YC6uro49ZSz469/eTRtaZcaP2vmnPiglce/rNyGa7DsorfYcpNld33i/fXX/fET++wgQIAAAQIECBAgQIBAewSEMtqjp5YAAQIECBAgQIAAAQIECBAgQKDoBDbY4NOtrvmF58fFG5MmtzqurQOWLq2Ik078Sdx7z4NtLWn3uJ+ccW78+uyLY8H8he3ulabBbX+6t9XhQ4eu2eqYZQfs8IVtonfvkmV3f+z97bfdHxMnTvrYPm8IECBAgAABAgQIECDQHgGhjPboqSVAgAABAgQIECBAgAABAgQIECg6gUGDV4811hjU6rp/99vLo6GhodVxrQ2YOnV6HHLQcfH3h55s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XzudCBAgQIAAAQIECBAgQIAAAQIEmgX22usbMWnSW81v2/T9DzddFFtsuXGbxhpUuALJX7mNawwtPPPMizFxwusxbdp7MWvm7Fi6tDJqa2ub7n4xYEB5rLHmoBgxYmh8pvEuG1tvu1kkj+zoKlt9fX0kIY3XXnszpk6dHlPfnh4zZ86JefMWxPz5C5oCGzWNQZGampro2bNn9C3tE6V9+8YqjQGRtRofiZI8FmW99UfFxptsGOs3fu+MIEZbrJM7nyQBlEmTJsfkt6Y2hW2SfQsXfhBVVdVRXV3TOPfG9TWurbS0byQBk8GDV4/BawyKtdYaHOuuNzI22mj9GDFyaCTBDxuBLAUe/vtTcdzxp6c6xRYbj41Lrzo7VU0ug0tKSqKsrMx/D3LBU0OAAAECBAh0aQF3yujSl8/kCRAgQIAAAQIECBAgQIAAAQIEClEg+eX7JpuOafoqxPnlY05J0GKdUcObvvLRr1B7fGrQarHLrp9v+irUOZoXAQIECBAgQIAAAQKFK9CzcKdmZgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBrisglNF1r52ZEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgUsIJRRwBfH1AgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGuKyCU0XWvnZkTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBSwglFHAF8fUCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAga4rIJTRda+dmRMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIFLCCUUcAXx9QIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBrisglNF1r52ZEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgUsIJRRwBfH1AgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGuKyCU0XWvnZkTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBSwglFHAF8fUCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAga4rIJTRda+dmRMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIFLCCUUcAXx9QIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBrisglNF1r52ZEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgUsIJRRwBfH1AgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGuKyCU0XWvnZkTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBSwglFHAF8fUCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAga4rIJTRda+dmRMgQIAAAQIECBAgQIAAAQIECBAgQIAAgS4h0NDQ0CXmaZIECBAgQIAAgXwLCGXkW1Q/AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECjgFCGjwEBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIAMBoYwMULUkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAhl+AwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBDIQEMrIAFVLAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIBQhs8AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCADAaGMDFC1JECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgIZfgMECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQyEBDKyABVSwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAUIbPAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgAwGhjAxQtSRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICGX4DBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMhAQysgAVUsCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAgFCGzwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIAMBoYwMULUkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAhl+AwQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBDIQEMrIAFVLAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIBQhs8AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCADAaGMDFC1JECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgIZfgMECBAgAABAgQIECBAgAABAgQyEujRo0fqzrnUpD6JAgIECBDolgI5/NiJnj39mqBbfhgsigABAgQIECgYAf9vq2AuhYkQIECAAAECBAgQIECAAAEC3U2gb9+S1EuqrKpKXaOAAAECBAgkAhUV6X+G9MnhZxVtAgQIECBAgACBtgsIZbTdykgCBAgQIECAAAECBAgQIECAQCqB8r79U41PBi9dUpG6RgEBAgQIEEgEli5N/zOkT1k/eAQIECBAgAABAhkKCGVkiKs1AQIECBAgQIAAAQIECBAgUNwCZWV9UwNUVFSmrlFAgAABAgQSgaVLl6aG6N8//c+q1CdRQIAAAQIECBAoYgGhjCK++JZOgAABAgQIECBAgAABAgQIZCvQv7w09Qly+VfOqU+igAABAgS6pUAuP0P6909/V6duiWdRBAgQIECAAIGMBIQyMoLVlgABAgQIECBAgAABAgQIECCQyy3hK3K49TxpAgQIECCQCOTyCKyyPu6U4dNDgAABAgQIEMhSQCgjS129CRAgQIAAAQIECBAgQIAAgaIWKOtTlnr9ufwr59QnUUCAAAEC3VIgl58hZWXpf1Z1SzyLIkCAAAECBAhkJCCUkRGstgQIECBAgAABAgQIECBAgACB8v45PL6kohIcAQIECBDISSCXUEafMnfKyAlbEQECBAgQIECgjQJCGW2EMowAAQIECBAgQIAAAQIECBAgkFYgl399PGfO3LSnMZ4AAQIECDQJzJ41L7VEeXn6AGHqkyggQIAAAQIECBSxgFBGEV98SydAgAABAgQIECBAgAABAgSyFVhtjVVSn2Da1PdS1yggQIAAAQKJwPRp76aGGDgw/c+q1CdRQIAAAQIECBAoYgGhjCK++JZOgAABAgQIECBAgAABAgQIZCswbNiw1Cd4Z9r01DUKCBAgQIBAXV1dvDP9/dQQ66yzVuoaBQQIECBAgAABAm0XEMpou5WRBAgQIECAAAECBAgQIECAAIFUAkOHrp1qfDJ49rwFUVlZlbpOAQECBAgUt8C7774ftVGXCqFXr14xePDgVDUGEyBAgAABAgQIpBMQykjnZTQBAgQIECBAgAABAgQIECBAoM0CgwatEgP6lbV5fPPAKVOmNb/0nQABAgQItElg2tT0jy4ZttbgSIIZNgIECBAgQIAAgewEhDKys9WZAAECBAgQIECAAAECBAgQIBDDR6W/W0Yuv1hDTYAAAQL/n717gbO6rBPH/5mB4SJEYdlmiUYWJqWgE1FycwCDUAFLXVPIG272T9fKNPM6ZNaW2WWzTdvWNK1MTAGTJOWiA64Io5itJlZouJGyXiIUEIH/POf3whCHy7nMub6/r9fsOef7/X6ey/s5O0Oez/k8tS3w5z9nn5TRp4+tS2r7XWP2BAgQIECAQDEEJGUUQ1kfBAgQIECAAAECBAgQIECAQM0K7LtPn6zn/uSTT2UdI4AAAQIEalsgl4S+vd6xV22jmT0BAgQIECBAoAgCkjKKgKwLAgQIECBAgAABAgQIECBAoHYF9t5776wn//vf/yHrGAEECBAgUNsCv3/08awB9uqrUkbWaAIIECBAgAABAlkKSMrIEsztBAgQIECAAAECBAgQIECAAIFsBHLZvmTJ4qXZdOFeAgQIEKhxgVde2RgPPvC7rBX22mvPrGMEECBAgAABAgQIZCcgKSM7L3cTIECAAAECBAgQIECAAAECBLIS6N9/v6zuTzc/veq5+POf/zfrOAEECBAgUJsCD//20Vj3yoasJ7/ffu/JOkYAAQIECBAgQIBAdgKSMrLzcjcBAgQIECBAgAABAgQIECBAICuBffd9Z+zeq1dWMenmJYsfyjpGAAECBAjUpsCSJdn/zejbd+/o3btHbYKZNQECBAgQIECgiAKSMoqIrSsCBAgQIECAAAECBAgQIECgNgUGDz4o64lLysiaTAABAgRqVmDJ/dknZXzgAwfUrJeJEyBAgAABAgSKKSApo5ja+iJAgAABAgQIECBAgAABAgRqUqBx8MCs571k8dKsYwQQIECAQO0JbNq0qa260m+znnjjgZIyskYTQIAAAQIECBDIQUBSRg5oQggQIECAAAECBAgQIECAAAEC2Qh88IPZJ2U8+dTKeObp/8umG/cSIECAQA0KPPbYH2PNupeynvlBOfxtyroTAQQIECBAgAABAiEpw5uAAAECBAgQIECAAAECBAgQINDBAvvtt2/07NY9617mz7s36xgBBAgQIFBbArn8rdh7772id+8etQVltgQIECBAgACBEglIyigRvG4JECBAgAABAgQIECBAgACB2hL48IcOznrCt82YnXWMAAIECBCoLYFbfzkr6wk3Nr4/6xgBBAgQIECAAAECuQlIysjNTRQBAgQIECBAgAABAgQIECBAICuBEaOHZnV/uvn+Bx6OlSufyTpOAAECBAjUhsDvHv59pO2usj1GHDIo2xD3EyBAgAABAgQI5CggKSNHOGEECBAgQIAAAQIECBAgQIAAgWwERo8eEQ0N2f+nmJnTVcvIxtm9BAgQqCWBmTN+k/V039ijZww65KCs4wQQIECAAAECBAjkJpD9fwnIrR9RBAgQIECAAAECBAgQIECAAIGaFujdu2eMOORDWRvcenP2Zemz7kQAAQIECFScwMaNG2PW7XdlPe5DRx/SliTYkHWcAAIECBAgQIAAgdwEJGXk5iaKAAECBAgQIECAAAECBAgQIJC1wLgJo7OOWf7U/8b//M+yrOMEECBAgEB1C9y7cEmseu6FrCc5ZkxT1jGFCKirqytEM9ogQIAAAQIECFScgKSMilsyAyZAgAABAgQIECBAgAABAgQqVWDkyCHRvXOXrId/409vzTpGAAECBAhUt8DPcvjb8JY3vikaG99X3TBmR4AAAQIECBAoMwFJGWW2IIZDgAABAgQIECBAgAABAgQIVK9A9+7d4yNjhmY9wVt+eUc88/T/ZR0ngAABAgSqU+CJJ56KufPvzXpyH/nIoVGqihWl6jdrJAEECBAgQIAAgQILSMooMKjmCBAgQIAAAQIECBAgQIAAAQI7Ejh8XPZbmLwSG+Oa/7pxR826RoAAAQI1JPAf3/txTrP9yOGH5hRXiCBJGYVQ1AYBAgQIECBQiQKSMipx1YyZAAECBAgQIECAAAECBAgQqFiBEaOHxpt6viHr8U/7xcz429/+nnWcAAIECBCoLoG//OXpmPGrO7Oe1D7veFv0779v1nECCBAgQIAAAQIE8hOQlJGfn2gCBAgQIECAAAECBAgQIECAQNYCJxw/MeuYNevWxfXXTcs6TgABAgQIVJfAf159Q04TOuqYw3OKK1SQShmFktQOAQIECBAgUGkCkjIqbcWMlwABAgQIECBAgAABAgQIEKh4geMmHx1d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QIAAAQIEshKQlJEVl5sJECBAgAABAgQIECBAgAABAtUpsHbt2tiwYUMmKWPLDJ944qn46U9vidm/mh/rNm7YctojAQIECLQJ9OzWPSaMHxuf+Ocj4q193lrVJg0NDZGSMjpi25Jt4U466aS47rrrtj1dFa9Tcsb5558fp556quSMqlhRkyBAgAABArsmIClj15zcRYAAAQIECBAgQIAAAQIECBCoaoFUJSNtY5K2M0nPtz6ee25N3HbLrJg27fZ45vnntr7kOQECBGpOYK+37RFHHzchJkwYFz16NFT9/FMiRtq2JFUEKcbx7LPPxn777RfpsVqPPn36ZCpnSM6o1hU2LwIECBAg8FoBSRmv9fCKAAECBAgQIECAAAECBAgQIFCzAikhIyVmbNq0absGs2bNjVkz5sb9Dz603XtcIECAQDUKDBl8cBx5xEdi5Jgh1Ti97c4pJWSkShnFPK699to4+eSTi9llSfp6+9vfnqmc8ZnPfKYk/euUAAECBAgQKI6ApIziOOuFAAECBAgQIECAAAECBAgQIFARAi+//HKsW7fuddUyth38s8/+PebObYk777w7Hnro0Z3ev2281wQIECh3gVQZ4gMDDojRHx0eI0YMiTe9abdyH3LBx5e2LOnSpUvU1dUVvO2dNdjU1BTz58/f2W1VcX3vvfd+tXJGsRNgqgLQJAgQIECAQJkLSMoo8wUyPAIECBAgQIAAAQIECBAgQIBAsQXWr18f6WfbbUy2N46nn34h5sy5O+666+743e8e395tzhMgQKDsBVLywcCB/WP06OExcuSwePOb31D2Y+6oAXbt2jXSTykSMtKcHnvssRgwYEDm71FHzbHc2k3JGeeff36ccsopRa9OUm4WxkOAAAECBKpJQFJGNa2muRAgQIAAAQIECBAgQIAAAQIECiSwdu3a2LBhwy4nZmzpNiVo/O7B38WShx6OJUsejiefXLHlkkcCBAiUnUBKOHjXu/aJQYMOzFTFOLDxwJqsiLHtwqRqDalKRn19/baXivq6ubk5pk6dWtQ+y6GzlJxxwQUXZLZwUTmjHFbEGAgQIECAQH4CkjLy8xNNgAABAgQIECBAgAABAgQIEKhKgVQlIyVmvPLKK1knZmwNsmrV6vht628laWyN4jkBAiUTSEkY++77zvjABw6QhLGdVUjbtuy2224lT8hIw0tVm1K1jFQ1oxaPffbZJ1M54+STT1Y5oxbfAOZMgAABAlUjICmjapbSRAgQIECAAAECBAgQIECAAAEChRXYtGlTJjFj48aNeSVmbD2ql156KZ5++tlY+eeVseKvz7Q9/2usXPFMPPXXVW2PK2P1Sy9ufbvnBAgQyFqg9xt6xZ593hr/9E9viz5tj29729vafvZoe/62tnNvyVSAyLrRGglIlTFSQkZKzCiXY/78+dHU1FQWw5kwYUJ84QtfiFTBY86cOUUbU0rOSJUzTjrpJMkZRVPXEQECBAgQKJyApIzCWWqJAAECBAgQIECAAAECBAgQIFB1AikxIyVSpMdUPaMjj/vuuy+OPPLItm1T6ts+NO0cdXWpbH7n6FLXEHVd/t/rhtjxB4W77757nNj2oVXXLl06cqjaJkCghAKdujVEr15viF7de0T3XrvFG9/Yq+31btG1a49461vfWMKRVXbXqYpISsjo3Llz2U0kJSNcd911JR1X8kl/C7ccCxYsKHpyxjvf+c5McsaJJ54oOWPLQngkQIAAAQIVICApowIWyRAJECBAgAABAgQIECBAgAABAqUUSJUy0lYmHZmY8eSTT8aIESPihRdeyHmqb3jDGyJ9SJa+UewgQIAAgV0XSAkH3bt3zyRkpOfldjz77LOx3377RXos1ZGqZEyfPv113UvOeB2JEwQIECBAgMA2ApIytgHxkgABAgQIECBAgAABAgQIECBA4PUCHZmY8fe//z1Tmv4Pf/jD6zvexTOp1H76sGzYsGG7GOE2AgQIEEgC5Z6QsWWVrr322jj55JO3vCzq47ZVMtrrvBTJGX379n21ckY5Vjhpz8k5AgQIECBQiwKpBqSDAAECBAgQIECAAAECBAgQIECAwA4FUtJD+hZ1fX195gO8Hd6cxcVUfeP444+PfBIyUnff/va3JWRk4e5WAgQIJIH0O33LliXlWCFj61VKW5gceuihW58q2vPx48fvtK+hQ4fGXXfdFS0tLTFq1Kid3l+IG5YvXx5TpkyJfv36xX/913/FK6+8UohmtUGAAAECBAgUWECljAKDao4AAQIECBAgQIAAAQIECBAgUM0CKYkibWWSKmds3rw576mee+658cMf/jCvdj796U/H1772tbzaEEyAAIFaE9iSkJGS7irleOyxx2LAgAGxfv36og15V6pktDeYUlTOeNe73pWpnPHJT34ysxVNe+NyjgABAgQIECi+gEoZxTfXIwECBAgQIECAAAECBAgQIECgYgXSh3ipYkb6EC/fb1X/5Cc/yTshI30b+bLLLqtYTwMnQIBAKQTS7/BUIaOSEjKS03777RfnnXdeUcl2pUpGewMqReWMP/3pT3HqqadmnH784x+rnNHewjhHgAABAgRKIKBSRgnQdUmAAAECBAgQIECAAAECBAgQqHSBVCVj3bp1sWHDhpwqZqTy7hMnTsxU3MjV4r3vfW/MmTMnevTokWsT4ggQIFBzAp07d351O6pKnHyqkpGqZaSqGR195Folo71xlapyxoUXXhiTJ09WOaO9RXGOAAECBAgUSUCljCJB64YAAQIECBAgQIAAAQIECBAgUE0C6YOqbt26RZcuXbKumPGHP/whjj/++LwSMt7ylrfEL3/5SwkZ1fSmMhcCBDpcIP3OTtWOUtWjSj26du0aV111VVGGn2uVjPYGV6rKGaecckqkJMZrr71W5Yz2FsY5AgQIECBQBAGVMoqArAsCBAgQIECAAAECBAgQIECAQDULvPzyy5G+uZyqZ6SfHR0vvPBCjBgxIp588skd3bbDaw0NDTF79uw4+OCDd3ifiwQIECDw/wRSIl1KZsglka5cDU866aS47rrrOmx4hayS0d4gS1E5Y999942LLrooJk2aVHFb17Rn6BwBAgQIEKgUgcpNh60UYeMkQIAAAQIECBAgQIAAAQIECFS5QPqQb7fddst8wJM+xNrekbY6Oe644/JKyEhtX3nllRIytofsPAECBLYR6NSpU+Z3dDUlZKQpXnHFFbHXXnttM9vCvSxklYz2RlWKyhl//OMfIyWzpMoZKaFl48aN7Q3NOQIECBAgQKDAAiplFBhUcwQIECBAgAABAgQIECBAgACBWhVIVTK2rpqxrcOnPvWp+MUvfrHt6axef/7zn4+LL744qxg3EyBAoFYFUiJGqpBRyduV7Gjtpk+fHkcdddSObsnpWkdXyWhvUKWonPHud787UznjhBNOUDmjvUVxjgABAgQIFEhApYwCQWqGAAECBAgQIECAAAECBAgQIFDrAlvK46eqGekDwK2rZnz/+9/POyHjox/9aObDo1p3Nn8CBAjsTCD9/u3evXt069atahMyksHEiRNjwoQJO+PI+npHV8lob0ClqJzxhz/8IU488cTYf//94yc/+YnKGe0tjHMECBAgQKAAAiplFABREwQIECBAgAABAgQIECBAgAABAq8V2LRpU6xbty5eeeWVuOuuu+KYY46JdC7X44ADDog777wz8wFjrm2II0CAQC0IdO7c+dVkjK2T46p17k899VQmqWDNmjUFmWIyy+fvVUEG0dZIKSpnvOc978kkPx5//PEqZxRqIbVDgAABAgTaBFTK8DYgQIAAAQIECBAgQIAAAQIECBAouECqlJG+pf3EE0/EJz/5ybw+4Hrb294W06ZNk5BR8FXSIAEC1SSQfu+myhjpd2+nTp1eU62omua57Vz22muvuPTSS7c9nfPrUlTJaG+wpaic8fjjj2f+Zvfv3z9uuOEGlTPaWxjnCBAgQIBADgIqZeSAJoQAAQIECBAgQIAAAQIECBAgQGDnAqtWrYrGxsZYsWLFzm/ezh1du3bNVNpIlTIcBAgQIPB6gVTZoaGhIdLvy/S8FqpjbKuwcePGGDx4cLS2tm57KavX5VIlo71Bp8oZl1xyScydO7e9yx1yrl+/fq9WzkhJPw4CBAgQIEAgNwF/RXNzE0WAAAECBAgQIECAAAECBAgQILADgfXr18fhhx+eV0JGav7qq68OCRk7gHaJAIGaFkjJGLvttltNbVfS3oKnyiDp70V6zOcolyoZ7c0hVc6YM2dOtLS0xMiRI9u7peDnli1bFpMnT45UOeOnP/1pXlWvCj44DRIgQIAAgQoSkJRRQYtlqAQIECBAgAABAgQIECBAgACBShFIH+IsXrw4r+Gef/75MXHixLzaEEyAAIFqFEjJBykZI21V0rlz55qsjrHtuqbKTGeccca2p3f5daqSMX369F2+v1Q3liI547HHHotJkyZlkjN+9rOfSc4o1eLrlwABAgQqVkBSRsUunYETIECAAAECBAgQIECAAAECBMpT4LLLLotp06blNbiUjHHuuefm1YZgAgQIVJtASsBIiRgpISNVyajFrUp2tKZf+cpXYq+99trRLdu9Vs5VMtobdKmSM0444YR43/veF5Iz2lsV5wgQIECAQPsCdZvbjvYvOUuAAAECBAgQIECAAAECBAgQIEAgO4GZM2dmqlvk85+cDj744Pj1r38dXbt2za5zdxMgQKAKBVLiRUrG6NKly6vbc0jG2P5Cp2oXRx111PZvaOdK8ty0aVM7Vyrn1IIFC+KSSy6JuXPnFm3Q733ve+Piiy+Of/7nf476et8BLhq8jggQIECg4gT8lay4JTNgAgQIECBAgAABAgQIECBAgEB5CixdujSOO+64yCchI33D+eabb45u3bqV5ySNigABAkUSSIkCKTmtR48er9mmRELGjhcgVVqaMGHCjm/a5mqlVcnYZviZl6WonPH73/8+jj/++Hj/+98fP//5zys+saU9V+cIECBAgEAhBFTKKISiNggQIECAAAECBAgQIECAAAECNS6wcuXKaGxsjPSY69GzZ89YtGhR7L///rFhw4Z45ZVXMj/5JHnkOhZxBAgQKJVAp06dMluTbNmeRBJG9ivx1FNPZf6WrFmzZqfB1VAlo71JlqJyRvr7nSpnHHvssSpntLcozhEgQIBAzQqolFGzS2/iBAgQIECAAAECBAgQIECAAIHCCKxduzbGjRuXV0JG+lAsVcjo379/pOepTH/37t0jJWqkxy0fThZmxFohQIBAeQmkRIxUISj9zkuVMdLvwLQdhISM3NYpVV269NJLdym4GqpktDfRUlTOePTRR+MTn/hEHHDAAfGLX/wir8pZ7c3JOQIECBAgUKkCKmVU6soZNwECBAgQIECAAAECBAgQIECgDARSFYtUKn7mzJl5jeYb3/hGnHPOOdttI/WTfrZU0Ni4caMPe7ar5QIBApUgsKUiRufOnV+tKiAJo2hwsLAAAEAASURBVHArl/5ODB48OFpbW7fbaPLetGnTdq9X04VSVM5IiZZbKmd4b1fTu8lcCBAgQCBbAZUyshVzPwECBAgQIECAAAECBAgQIECAwKsCF110Ud4JGZMmTdphQkbqLH2Yk7413rVr19htt90y3yZPj+nb5OmDTR/2vLoknhAgUKYC6XdYqvqzbUWMLb/D/B4r7MIl16uvvjrzN2J7LVdrlYz25luKyhmPPPJIHHfccZnKGTfddJNkyvYWxjkCBAgQqAkBlTJqYplNkgABAgQIECBAgAABAgQIECBQeIFp06Zl9o3Pp+UhQ4bEvHnzMh9U5tJOqp6RjvSYvhX9yiuvZB7TN5+3XMulXTEECBDIVyAlYaTEgPSzdTWM1K4EjHx1dz3+s5/9bHz3u999XUBag1qpkvG6ybedKEXljPe9731xySWXxNFHH+3/B9pbFOcIECBAoGoFJGVU7dKaGAECBAgQIECAAAECBAgQIECg4wQWL14cw4YNi/Xr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gECHC6REjLFjx+aVkNHQ0BC33XabhIwOXy0dECBAgAABAtUiMHTo0EiVylpaWoq2vcjdd98daau5Qw89tKjbqFTLmpkHAQIEykFAUkY5rIIxECBAgAABAgQIECBAgAABAgR2USBtVTJx4sRYtmzZLka0f9s111wTQ4YMaf+iswQIECBAgAABAtsVkJyxXRoXCBAgQKAdAUkZ7aA4RYAAAQIECBAgQIAAAQIECBAoV4HTTz898w3NfMZ3zjnnxKRJk/JpQiwBAgQIECBAoOYFJGfU/FsAAAECBHZJoG5z27FLd7qJAAECBAgQIECAAAECBAgQIECgpAL//u//HmeddVZeYxg/fnxMnz496urq8mpHMAECBAgQIECAwGsFFixYEBdddFFRtxkZMWJENDc3F207ldfO2CsCBAgQ2BUBSRm7ouQeAgQIECBAgAABAgQIECBAgECJBWbPnh3jxo2LTZs25TyS/v37x5IlS6J79+45tyGQAAECBAgQIEBgxwKSM3bs4yoBAgRqTUBSRq2tuPkSIECAAAECBAgQIECAAAECFSewbNmyaGxsjDVr1uQ89j322CNaW1ujT58+ObchkAABAgQIECBAYNcFJGfsupU7CRAgUM0C9dU8OXMjQIAAAQIECBAgQIAAAQIECFS6wPPPPx9jx47NKyGja9eucfvtt0vIqPQ3g/ETIECAAAECFSUwdOjQmDdvXrS0tBRte5G77747mpqaMv3Nnz+/orwMlgABAtUqICmjWlfWvAgQIECAAAECBAgQIECAAIGKF9iwYUMceeSRsXz58rzmcv3118egQYPyakMwAQIECBAgQIBAbgKSM3JzE0WAAIFqEZCUUS0raR4ECBAgQIAAAQIECBAgQIBA1QmccsopsXDhwrzmdfHFF8cxxxyTVxuCCRAgQIAAAQIE8heQnJG/oRYIECBQiQJ1m9uOShy4MRMgQIAAAQIECBAgQIAAAQIEqlng61//epx33nl5TTElY9x00015tSGYAAECBAgQIECgYwQWLFgQX/rSlyI9FusYPHhwNDc3Z7bHK1af+iFAgECtC0jKqPV3gPkTIECAAAECBAgQIECAAAECZScwc+bMmDhxYuTzXZqBAwfGfffdF127di27+RkQAQIECBAgQIDAPwTmz5+fSZS4++67/3Gyg5+NGDEi0+ehhx7awT1pngABAgQkZXgPECBAgAABAgQIECBAgAABAgTKSGDp0qVxyCGHxNq1a3Me1Z577hmtra2RHh0ECBAgQIAAAQKVISA5ozLWySgJECCQrYCkjGzF3E+AAAECBAgQIECAAAECBAgQ6CCBVatWxYABA2LlypU599C9e/e49957I1XKcBAgQIAAAQIECFSegOSMylszIyZAgMCOBOp3dNE1AgQIECBAgAABAgQIECBAgACB4gisX78+Dj/88LwSMurq6uLGG2+UkFGcJdMLAQIECBAgQKBDBNKWIikxY968eTF8+PAO6WPbRtPWKU1NTbGl722ve02AAAECuQtIysjdTiQBAgQIECBAgAABAgQIECBAoGACkydPjsWLF+fV3le/+tUYP358Xm0IJkCAAAECBAgQKA+BlCCRkiXmzp0rOaM8lsQoCBAgkJOApIyc2AQRIECAAAECBAgQIECAAAECBAonMHXq1Jg2bVpeDU6aNCnOO++8vNoQTIAAAQIECBAgUH4CqYKF5IzyWxcjIkCAwK4K1G1uO3b1ZvcRIECAAAECBAgQIECAAAECBAgUViAlYxx77LF5NTpkyJBMeeuGhoa82hFMgAABAgQIECBQ/gJpW5Pm5ua45557ijbYESNGZPpM1TscBAgQIJCdgKSM7LzcTYAAAQIECBAgQIAAAQIECBAomEDarmTYsGGxfv36nNvs06dPPPTQQ9G7d++c2xBIgAABAgQIECBQeQJpW5OUnNHS0lK0wUvOKBq1jggQqCIBSRlVtJimQoAAAQIECBAgQIAAAQIECFSOwMqVK2PAgAGxatWqnAfds2fPWLRoUfTv3z/nNgQSIECAAAECBAhUtkCpkjMuueSSSFurOAgQIEBgxwL1O77sKgECBAgQIECAAAECBAgQIECAQKEF1q5dG+PGjcsrIaO+vj5uvvlmCRmFXhztESBAgAABAgQqTGDkyJGZrUzmzJmTqcJWjOHffffdkfpNlTPSdioOAgQIENi+gKSM7du4QoAAAQIECBAgQIAAAQIECBAouMDmzZvjuOOOi6VLl+bV9re+9a0YM2ZMXm0IJkCAAAECBAgQqB6BUiRn3HPPPZIzquctZCYECHSQgKSMDoLVLAECBAgQIECAAAECBAgQIECgPYEvfelLMXPmzPYu7fK5KVOmxFlnnbXL97uRAAECBAgQIECgdgS2JGfcddddMXTo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FbVyBkugfAQkZZTPWhgJAQIECBAgQIAAAQIECBAg0AECp5xySixcuDCvli+44II45phj8mpDMAECBAgQIECAAAEClSswfvx4yRmVu3xGTqCkAnWb246SjkDnBAgQIECAAAECBAgQIECAAIEOErjiiiviC1/4Ql6tp//4On369Kirq8urHcEECBAgQIAAAQIECFSPQNrWZOrUqfHAAw8UbVLDhg2LSy65JLPFSdE61REBAnkLSMrIm1ADBAgQIECAAAECBAgQIECAQDkKzJ49O8aNGxebNm3KeXgDBw6Me++9N7p3755zGwIJECBAgAABAgQIEKhegZSc0dzcHA8++GDRJjl06NBMn6NGjSpanzoiQCB3AUkZuduJJECAAAECBAgQIECAAAECBMpU4JFHHonBgwfHmjVrch7hnnvumSlPnB4dBAgQIECAAAECBAgQ2JHAjBkzMpUzJGfsSMk1ArUpUF+b0zZrAgQIECBAgAABAgQIECBAoFoFVq1aFWPHjs0rISNVxpg1a1ZIyKjWd4l5ESBAgAABAgQIECiswIQJEzJbmaStDw866KDCNr6d1hYsWBCjR4+OtK3JnDlztnOX0wQIlFpAUkapV0D/BAgQIECAAAECBAgQIECAQMEE1q9fH4cffnisWLEi5zbr6urixhtvjLR1iYMAAQIECBAgQIAAAQLZCEjOyEbLvQRqQ0BSRm2ss1kSIECAAAECBAgQIECAAIGaEJg8eXIsXrw4r7leeumlMX78+LzaEEyAAAECBAgQIECAQG0LbJ2cUayEb5Uzavs9Z/blK1C3ue0o3+EZGQECBAgQIECAAAECBAgQIEBg1wQuu+yyuPDCC3ft5u3cdcwxx8RNN920natOEyBAgAABAgQIECBAIHuB9HHsjBkzYurUqbF06dLsG8gxYujQodHc3ByjRo3KsQVhBAgUQkBSRiEUtUGAAAECBAgQIECAAAECBAiUVGDmzJkxceLEyOe7J4MGDYqWlpbo2rVrSeeicwIECBAgQIAAAQIEqlNAckZ1rqtZEdiZgKSMnQm5ToAAAQIECBAgQIAAAQIECJS1QPqm2SGHHBJr167NeZx9+vSJ1tbW2GOPPXJuQyABAgQIECBAgAABAgR2RUByxq4ouYdA9QhIyqietTQTAgQIECBAgAABAgQIECBQcwIrV66MxsbGSI+5Hj179oxF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AggggAACCCCAAAIIIIAAAggggAACYSeggRlz5swxtzVp3769X+a3cuVKufrqq6Vp06YyceJEv/RJJ74RICjDN660igACCCCAAAIIIICATwXS0s7K8E9/kFde/kB2bN/j076yN37yxCkzOOPlwe/LsWPHs1/iZwQQQAABBCJCoGLFspIvH/+kFhE3m0kigAACCCCAAAIIIHCRgAZnzJ4928yc4a/gjBUrVkivXr2kXbt2smTJkotGxK+hIMD2JaFwlxgjAggggAACCCCAAALZBI4cOSZvvfmpX4MxsnVv/rhp4zYZ/MIweebZ+6R0YsmLL/M7AggggAACYSvQrHlDGT7i3y5njcqfPzZsLZgYAggggAACCCCAAAKRKtClSxfR1/Tp0+XFF180s2j42mLu3LnSokUL0e1MXnvtNaldu7avu6R9hwQI63cIkmYQQAABBBBAAAEEEPCHwOFDR+TF594NaEBG1jwPHUyW8eNmZP3KnwgggAACCESMQExMtGiwhSuviEFhoggggAACCCCAAAIIRKCABmZkZc7o0KGDXwR+/vlnqV+/vtx9992yd+9ev/RJJ94JEJThnR+1EUAAAQQQQAABBBDwm8AJY+uQ1175UDRTRrAcKSkngmUojAMBBBBAAAEEEEAAAQQQQAABBBBAAIGACGhwxqxZs8zMGf4IzsjMzJRPP/1UatWqJW+88YZkZGQEZN506poA25e45kQpBBBAAAEEEEAAAQQCLvDfj0bKgQOHXR6H7ndfp251qVW7qlStWlHKlU+UwoULSsGCcVKgQH7jL2uZkp6eIcePn5SUY8dl375DsnPnXtm4YWtQZOJweaIURAABBBBAAAEEEEAAAQQQQAABBBBAIAgEOnfuLPqaMWOGua2JZtHw5XH69Gl5+umn5fPPP5cvv/xSWrZs6cvuaNtDAYIyPISjGgIIIIAAAggggAAC/hSYM3uxLF+2zqUuixYtLP36d5U2bZtJ8eLF8qyTP38+M+26BmqUK1da6tar8WdZDdCYM2uRjBk99c9z/IAAAggggAACCCCAAAIIIIAAAggggAAC9gLZgzMGDx5sZtGwr+V5iQ0bNkjr1q3lzjvvlDfffFMSEhI8b4yajguwfYnjpDSIAAIIIIAAAggggICzAunp6TLq23EuNXp593by1tBnpXffzpYBGXaNaZDGdQP7yLXX9bYr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JFPG/v37Pewt+KsRlBH894gRIoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEBICngRlnD17Vk6cOBES83N3kARluCtGeQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDIVaB48eISFRWV6zWrkykpKVaXQ/YaQRkhe+sYOAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAsEnoIEZ7h6aLSMcD4IywvGuMicEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQCJFCgQAG3e05LS3O7TihUICgjFO4SY0QAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCBEBAjK+OtGEZTxlwU/IYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggICXAp4EZbB9iZfoVEcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACB8BfwJCiD7UvC/33BDBFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEDASwGCMv4CZPuSvyz4CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQS8FIiLi3O7BbYvcZuMCggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQaQJRUVFuTzkzM9PtOqFQgUwZoXCXGCMCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIhJwAQRkhd8sYMAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgiEggBBGaFwlxgjAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCIScAEEZIXfLGDACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIhIIAQRmhcJcYIwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgiEnEBMyI2YASOAAAIIIIAAAgggEGYCtWvXlnPnzoXZrJgOAgj4QqBr166ybt06t5r+9NNPpU+fPm7VoTACCESeQHR0dORNmhkjgAACCCCAAAIIIIAAAn4QICjDD8h0gQACCCCAAAII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BAgQIECAAAECBAgQ6OwCgjI6+wzqPwECBAgQIECAAAECBAikIlBTUxONjY2R/JVaLzDLsfU1OJMAAQIECBAgQIAAAQIECBAg0HkFBGV03rnTcwIECBAgQIAAAQIECBBIWaBPnz4xZMiQlFup7uqHDh0aDQ0N1T1IoyNAgAABAgQIECBAgAABAgQINCMgKKMZGIcJECBAgAABAgQIECBAgEAiMGbMmOjduzeMVgjU19fH6NGjW3GmUwgQIECAAAECBAgQIECAAAEC1SEgKKM65tEoCBAgQIAAAQIECBAgQCAlgSQgY+zYsSnVXt3VTpw4Merq6qp7kEZHgAABAgQIECBAgAABAgQIECggICijAI4sAgQIECBAgAABAgQIECCQCAwePDj69u0LowUC/fr1i4EDB7bgDEUJECBAgAABAgQIECBAgAABAtUnICij+ubUiAgQIECAAAECBAgQIEAgBYHGxsaoqalJoebqq7K2tjYmTJhQfQMzIgIECBAgQIAAAQIECBAgQIBACwUEZbQQTHECBAgQIECAAAECBAgQyKZAQ0NDDB06NJuDb+GoR4wYEYmXRIAAAQIECBAgQIAAAQIECBDIuoCgjKxfAcZPgAABAgQIECBAgAABAiULjB49Ourr60sun8WCSTDGyJEjszh0YyZAgAABAgQIECBAgAABAgQIzCcgKGM+EgcIECBAgAABAgQIECBAgEDTAnV1dTFx4sSmMx3NC4wfPz6S7UskAgQIECBAgAABAgQIECBAgACBCEEZrgICBAgQIECAAAECBAgQINACgYEDB0a/fv1acEZ2ig4YMCD69++fnQEbKQECBAgQIECAAAECBAgQIECgiICgjCJAsgkQIECAAAECBAgQIECAwLwCEyZMsBrEPCjJKiKTJk2a56iPBAgQIECAAAECBAgQIECAAIFsCwjKyPb8Gz0BAgQIECBAgAABAgQItEKgoaEhRowY0Yozq/eUUaNGRX19ffUO0MgIECBAgAABAgQIECBAgAABAq0QEJTRCjSnECBAgAABAgQIECBAgACBkSNHRhKcIUXeYdiwYSgIECBAgAABAgQIECBAgAABAgTmERCUMQ+IjwQIECBAgAABAgQIECBAoBSB2traGD9+fClFq75MY2Nj1NTUVP04DZAAAQIECBAgQIAAAQIECBAg0FIBQRktFVOeAAECBAgQ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BNIREJSRjqtaCRAgQIAAAQIECBAgQIBAyQLjxo2Lurq6ksuXo2Dv3r1jzJgx5ahKHQQIECBAgAABAgQIECBAgAABAs0ICMpoBsZhAgQIECBAgAABAgQIECDQXgJJgMSoUaPaq7l8O2PHjo2kXYkAAQIECBAgQIAAAQIECBAgQCA9AUEZ6dmqmQABAgQIECBAgAABAgQIlCwwbNiwdttKJNkyZfDgwSX3TUECBAgQIECAAAECBAgQIECAAIHWCQjKaJ2bswgQIECAAAECBAgQIECAQFkFampqorGxMZK/aaZZ7aTZhroJECBAgAABAgQIECBAgAABAgT+KyAow5VAgAABAgQIECBAgAABAgQqRKBPnz4xZMiQVHszdOjQaGhoSLUNlRMgQIAAAQIECBAgQIAAAQIECPxXQFCGK4EAAQIECBAgQIAAAQIECFSQwJgxY6J3796p9Ki+vj5Gjx6dSt0qJUCAAAECBAgQIECAAAECBAgQmF9AUMb8Jo4QIECAAAECBAgQIECAAIEOE0gCMsaOHZtK+xMnToy6urpU6lYpAQIECBAgQIAAAQIECBAgQIDA/AKCMuY3cYQAAQIECBAgQIAAAQIECHSowODBgyPZyqScqV+/fjFw4MByVqkuAgQIECBAgAABAgQIECBAgACBIgKCMooAySZAgAABAgQIECBAgAABAh0h0NjYGDU1NWVpura2NiZMmFCWulRCgAABAgQIECBAgAABAgQIECBQuoCgjNKtlCRAgAABAgQIECBAgAABAu0mkKyUMXTo0LK0N2LEiGhoaChLXSohQIAAAQIECBAgQIAAAQIECBAoXUBQRulWShIgQIAAAQIECBAgQIAAgXYVGD16dNTX17epzSQYY+TIkW2qw8kECBAgQIAAAQIECBAgQIAAAQKtExCU0To3ZxEgQIAAAQIECBAgQIAAgdQF6urq4vTTT29TO+PHj49k+xKJAAECBAgQIECAAAECBAgQIECg/QUEZbS/uRYJECBAgAABAgQIECBAgEDJAt///vejb9++JZefs+CAAQOif//+cx7yngABAgQIECBAgAABAgQIECBAoB0FBGW0I7amCBAgQIAAAQIECBAgQIBAawQaGxtbvNpFssrGpEmTWtOccwgQIECAAAECBAgQIECAAAECBMokICijTJCqIUCAAAECBAgQIECAAAECaQk0NDTEiBEjWlT9qFGjor6+vkXnKEyAAAECBAgQIECAAAECBAgQIFBeAUEZ5fVUGwECBAgQIECAAAECBAgQSEVg5MiRJQdZJEEcw4YNS6UfKiVAgAABAgQIECBAgAABAgQIEChdQFBG6VZKEiBAgAABAgQIECBAgACBDhOora2NiRMnltR+st1JTU1NSWUVIkCAAAECBAgQIECAAAECBAgQSE9AUEZ6tmomQIAAAQIECBAgQIAAAQJlFRg4cGAMGDCgYJ2DBg2Kvn37FiwjkwABAgQIECBAgAABAgQIECBAoH0EBGW0j7NWCBAgQIAAAQIECBAgQIBAWQQmTZoUyaoZTaXevXvHuHHjmspyjAABAgQIECBAgAABAgQIECBAoAMEBGV0ALomCRAgQIAAAQIECBAgQIBAawXq6+tjxIgRTZ4+evToSAIzJAIECBAgQIAAAQIECBAgQIAAgcoQEJRRGfOgFwQIECBAgAABAgQIECBAoGSBkSNHRkNDw1zl+/TpE/vvv/9cx3wgQIAAAQIECBAgQIAAAQIECBDoWAFBGR3rr3UCBAgQIECAAAECBAgQINBigWT7ksbGxtnn1dTU5D8nfyUCBAgQIECAAAECBAgQIECAAIHKERCUUTlzoScECBAgQIAAAQIECBAgQKBkgb59+8aee+6ZLz906NBIVsqQCBAgQIAAAQIECBAgQIAAAQIEKktAUEZlzYfeECBAgAABAgQIECBAgACBkgXGjh0bK6+8cowePbrkcxQkQIAAAQIECBAgQIAAAQIECBBoP4Gu7deUlggQIECAAAECBAgQIECAAIFyCtTX18cLL7xQzirVRYAAAQIECBAgQIAAAQIECBAgUEYBK2WUEVNVBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFZAoIyZkn4S4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoo4CgjDJiqooAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgMEtAUMYsCX8JECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUUEJRRRkxVESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRmCQjKmCXhLwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgjAKCMsqIqSoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwCwBQRmzJPwlQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJRRQFBGGTFVRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCYJSAoY5aEvwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBMgoIyigjpqoIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABArMEBGXMkvCXAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBGAUEZZcRUFQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgloCgjFkS/hIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyiggKKOMmKoiQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECMwSEJQxS8JfAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAZBQRllBFTVQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBWQKCMmZJ+EuAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKNA1zLWpSoCBAgQIECAAIH/L/Dxx5/FJ598yoMAAQIECBAgQIAAAQJVI9CrV20stNBCVTMeAyFAgAABAgQIECDQHgKCMtpDWRsECBAgQIBA5gTOHn9RXHLZ7zI3bgMmQIAAAQIECBAgQKB6BYYeOCiGDd+vegdoZAQIECBAgAABAgRSELB9SQqoqiRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICMpwDRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEUhAQlJECqioJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAoIyXAMECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgRQEBGWkgKpKAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgICgDNcAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCAFAUEZKaCqkgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAgKMM1QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBIQUBQRgqoqiRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICMpwDRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEUhAQlJECqioJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAl0RECBAgAABAgQIVIZA16iJrl3FzFbGbOgFAQIECBAgQIAAgeoWmD79q5geM6p7kEZHgAABAgQIECBAoAIEBGVUwCToAgECBAgQIEAgEfjpQT+KoQf/GAYBAgQIECBAgAABAgRSFzj9F+fGRRdfmXo7GiBAgAABAgQIECCQdQE/xcz6FWD8BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQCoCgjJSYVUpAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkHUBQRlZvwKMnwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEEhFQFBGKqwqJUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBLIuICgj61eA8RMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKpCAjKSIVVpQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDWBQRlZP0KMH4CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgFQFBGamwqpQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIuoCgjKxfAcZPgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIpCIgKCMVVpUSIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWRcQlJH1K8D4CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgVQEBGWkwqpSAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIOsCXbMOYPwECBAgQIAAAQIECBAgQIAAAQIECBAgUFzgyy++jI8++qh4wTaW6N69e9TW1raxFqcTIECAAAECBAgQqAwBQRmVMQ96QYAAAQIECBAgQIAAAQIECBAgQIAAgYoWmDlzZnz11Vep9zFpJ3l16dIl9bY0QIAAAQIECBAgQCBtAduXpC2sfgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCCTAoIyMjntBk2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAikLSAoI21h9RMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKZFBCUkclpN2gCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgbQFBGWkLq58AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIpICgjExOu0ETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECaQsIykhbWP0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJgUEZWRy2g2aAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQSFtAUEbawuonQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMikgKCOT027QBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQNoCgjLSFlY/AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkEkBQRmZnHaDJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNIWEJSRtrD6CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUwKCMrI5LQbNAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJC2gKCMtIXVT4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGRSQFBGJqfdoAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG0BQRlpC2sfgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCCTAoIyMjntBk2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAikLSAoI21h9RMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKZFBCUkclpN2gCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgbQFBGWkLq58AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIpICgjExOu0ETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECaQsIykhbWP0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJgUEZWRy2g2aAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQSFtAUEbawuonQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMikgKCOT027QBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQNoCgjLSFlY/AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkEkBQRmZnHaDJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNIWEJSRtrD6CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUwKCMrI5LQbNAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJC2gKCMtIXVT4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGRSQFBGJqfdoAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG0BQRlpC2sfgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCCTAoIyMjntBk2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAikLSAoI21h9RMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKZFBCUkclpN2gCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgbQFBGWkLq58AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIpICgjExOu0ETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECaQsIykhbWP0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJgUEZWRy2g2aAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQSFtAUEbawuonQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMikgKCOT027QBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQNoCgjLSFlY/AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkEkBQRmZnHaDJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNIWEJSRtrD6CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUwKCMrI5LQbNAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJC2gKCMtIXVT4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGRSQFBGJqfdoAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG0BQRlpC2sfgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCCTAoIyMjntBk2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAikLSAoI21h9RMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKZFBCUkclpN2gCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgbQFBGWkLq58AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIpICgjExOu0ETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECaQsIykhbWP0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJgUEZWRy2g2aAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQSFtAUEbawuonQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMikgKCOT027QBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQNoCgjLSFlY/AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkEkBQRmZnHaDJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNIWEJSRtrD6CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUwKCMrI5LQbNAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJC2gKCMtIXVT4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGRSQFBGJqfdoAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG0BQRlpC2sfgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCCTAoIyMjntBk2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAikLSAoI21h9RMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKZFBCUkclpN2gCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgbQFBGWkLq58AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIpICgjExOu0ETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECaQsIykhbWP0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJgUEZWRy2g2aAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQSFtAUEbawuonQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEMikgKCOT027QBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQNoCgjLSFlY/AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkEkBQRmZnHaDJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBNIWEJSRtrD6CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUwKCMrI5LQbNAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJC2gKCMtIXVT4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGRSQFBGJqfdoAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG0BQRlpC2sfgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCCTAoIyMjntBk2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAikLSAoI21h9RMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKZFBCUkclpN2gCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgbQFBGWkLq58AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIpICgjExOu0ETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECaQt0TbsB9RMgQIAAAQIECBAgULkCUx5/Om6dfHuTHezSpUtsvMn6sdXWmzaZ7yABAgQIECBAgAABAgQIECBAgAABAgQIFBYQlFHYRy4BAgQIECBAgACBqhZ45pkX4pcXXtHsGK+/bnLcefcNzebLIECAAAECBAgQIECAAAECBAgQIECAAIHmBWxf0ryNHAIECBAgQIAAAQKZF/jss88zbwCAAAECBAgQIECAAAECBAgQIECAAAECrRUQlNFaOecRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAoI2L6kAI4sAgQIECBAgAABAvMKnD3x4nkPzf5c07UmvrvjtrHCCsvNPuYNAQLZEDjh+HHxysuvV/xgV1pp+Tj+xMOiS5cuFd/XjuzgC8+/HLfc/Jdmu9Cwxv/Fdttv3Wy+DAKVLnDlb2+Md95+t8ludu/ePXb93neid+/Fm8x3kAABAgQIECBAgAABAgRaJiAoo2VeShMgQIAAAQIECGRc4Jyzf1VQ4KUXX41fjD22YBmZBAhUn8AjD0+Jp596vuIH9s97Hoj9hvwwlq9fpuL72pEdfOGFV6LY9/29D9wSdXW9OrKb2ibQaoErf3tDwe+s996bGiOOGdrq+p1IgAABAgQIECBAgAABAv8TsH3J/yy8I0CAAAECBAgQINBmgc8++7zNdaiAAAECaQrM+GpGmtVnpm7f95mZ6kwOdNpnn2Vy3AZNgAABAgQIECBAgACBNAQEZaShqk4CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAg8wKCMjJ/CQAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE0hAQlJGGqjoJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBzAsIysj8JQCAAAECBAgQIECAAAECBNoq0KVLl7ZW4XwCBAgQIECAAAECBAgQIECAAIEqFBCUUYWTakgECBAgQIAAAQIECBAg0L4CfTb+evs2qDUCBAgQIECAAAECBAgQIECAAIFOIdC1U/RSJwkQIECAAAECBAgQIECAQAUL/PzYQyJ5zZn+eOsdcejBx895aL73vxh7bOw8YPv5jjtAgAABAgQIECBAgAABAgQIECBQHQJWyqiOeTQKAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoMIEBGVU2IToDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAdAoIyqmMejYIAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoMAFBGRU2IbpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVIeAoIzqmEejIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBCpMQFBGhU2I7hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLVIdC1OoZhFAQIECBAgAABAgQIVJPAzJkz47XX3oynn3o+Xn3l9fz75PN7702NaZ9Oi88++zymTfssarrWRPdu3aJnzx6x2GKLRO+vLRErrLhcrLzyCrHuumvESivXR5cuXTolzZNPPhu3Tr497r/vkXjpxVfjww8/itra2lh88UVjmWWXim9stmFstdWmsU5unK1NX331VTz04OPxtzvuifty7bz91jvxzjvv5V2XWHyxWHGl5WPzzfvEt7bdMlZccfnWNlOW8z79ZFo8/fTz8cILL+evh1dfeSPefPM/8Wnuepj26Wfx6bRpMWP6jOjevXvOqXssvEhd/ppYNme14kr1scYaq8a6660ZCy3Usyz9UQmBjhZIvgOT78LmUvK9mNwLxdKMGTPy3wN//etd8eD9j8bbb7+b+x54P5Lv4YUX7hUrr7JCbL5Fn9hjz51jiSUWK1ZdSflZuZ+n576THnro8Xjk4Snx/HMvxfPPvxzvvft+fPzxJ/HJJ5/m/n1aIHr0qM29euS/25Pv3OTfreTfrz4br5/3LwlUIQIECBAgQIAAAQIECBCoaAFBGRU9PTpHgAABAgQIECBAIBsCSXDAo48+GffcfX88cN+j+fcfffRxmwefBGp869tbxk47bxebfmPDNtdXSgVrrr51k8XWXqchrrnuwibz5jw45fGnY9zY8+Kf9zww5+H8++nTP80/yHv11Tfi3n89FBPHXxQbbrRuDB3249hs843mK9/cgeRh659u+3vu/F/mHxLOVy73nDd5aJq0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CjQW2bak/w59Y+21V5/d05VXWSH2GbzH7M9Nvbnn7gfiqt/9oams1I8VWxkh6cC77xa/flPvaAU1UM3381JLf62g9IsvvhKdLYhhwdrCq3+4vgtOuUwCBAgQIECAAAECBAi0SEBQRou4FCZAgAABAgQIECBQWQLJ8vn/+udDldWpAr15p8hD+F69FipwtqxqE3jnncJBOsl4XROVP+vJtiUXnP+bZjtaU1MThwzff778gw4aHF9bcon5js954PTTzok333xrzkPt8n7RxRYp2s5ruZVepP8JVPP9vOyyS/1voE28S/4tfvSRwitBNXFahx4qdo27vjt0ejROgAABAgQIECBAgECVCQjKqLIJNRwCBAgQIECAAIHsCcycObPTDHpabksCicAsgWkduP1DstWC1HaBUrYt2XW3HWKllZafr7GeC/WIo47+2XzH5zzwySefxvEdsI3Jkkv2nrMbTb6fkgtGkf4nUM3382qrr/K/gTbz7u67728mpzIPL7lU4Wv89df/HZ1xW5bK1NYrAgQIECBAgAABAgSyLiAoI+tXgPETIECAAAECBAhkWqBLkdF3poCPQkP55YVXFMqWlzGBt/7zTjz+2FMZG3U6wy22bUltbfc4aNi+zTa+407bxUZ91ms2P8n4x533xnXX3lKwTLkzF1mkLhZddOGC1f7t9nuiWr4jCw60wjPb435eZ52Gogq/u/L30ZGBKUU7OE+BFVeYP1BqniLxtzvumfeQzwQIECBAgAABAgQIECDQCgFBGa1AcwoBAgQIECBAgACBahHonntgWii19wOmzz//olB3WpX3q0uuiuuvm9yqc53U8QKff1HeayJ5gLvv4OEdP7Aq6EGxbUuSIe4zaPdYaqmvFRztsccdGgssUPg/T5x2ytmRzF17ptUbVi3YXLLaSmdbHaHggNohs7Pez/+32sqx+OKLFhSa+v4Hce3VNxcsU0mZDWv+X9HuXPnbG4uWUYAAAQIECBAgQIAAAQIEigsU/q8exc9XggABAgQIECBAgACBTixQV9erYO+/+uqrsj4I7d69W8H23n773YL5Lc2cNOGi+MWpZ5d02ledaBuYkgbUCQoVux6SIbz9VvkexL/00mvxwx8cFC88/3JRneTal5oX+PLLL2PEUafEjBkzmi2UPMQ+4MC9m82flbFG7uHwnt/fedbHJv9+9NHHccLx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npPO+8/Hfd0LBAgQIECAAAECBAgQGCgCg97ef/r0N6AeKFrGSYAAAQIECBDopcDXvvzf4r+3rO7l2f922uduuLr0icQ+XeRkAgUT2L17T7zwo83xv7a9GLt27S698bN//6/efkO4J9Ib4mkFhbR8+3/5L/9HKYTx8T/6g/hPb7/p7SimQHpDe/PfdEQK7ex8e5ue3W8Hovbte/PtNwLfejssMzRGvr0axtkfGBspLDV58qQ4/w+nxkc/el7pjepiihgVgdoVGIj/Pae/x1JY5+/+7u/jH1/+53jt1X+N198Oe7319v/Dkkc6UnAhreyTAkUfePv/ZxP+/f9n06ZNKYXianfG9bwWBJqXPx7/z3dW9amrS/7vT8dnr/tMn645lZNTKDB9FXlFoVNxcQ0BAgQIECBAoJYELrroonjhhb6tbLl27dqYN29eLQ2zV321UkavmJxEgAABAgQIECBAgEA5BCZMGB+Lmi4rfZWjPW1Ut0AK4qRtZmw1U93zpHcEeiMwEP97njhxQqSv+Qt6I+QcAgQIECBAgAABAgQIECiqwOCiDsy4CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBDKqKS+tgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHCCghlFHZqDYwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopIBQRiX1tU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUVkAoo7BTa2AECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQWEMiqpr20CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgsAJCGYWdWgMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikglFFJfW0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEChRUQyijs1BoYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBoYxK6mubAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKyAUEZhp9bACBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCGVUUl/bBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGEFhDIKO7UGRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRSQCijkvraJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAorIJRR2Kk1MAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCSAkIZldTXNgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBYAaGMwk6tgREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBDKqKS+tgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHCCghlFHZqDYwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopIBQRiX1tU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUVkAoo7BTa2AECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQWEMiqpr20CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgsAJCGYWdWgMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikglFFJfW0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEChRUQyijs1BoYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBoYxK6mubAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKyAUEZhp9bACBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCGVUUl/bBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGEFhDIKO7UGRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRSQCijkvraJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAorIJRR2Kk1MAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCSAkIZldTXNgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBYAaGMwk6tgREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBDKqKS+tgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHCCghlFHZqDYwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopIBQRiX1tU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUVkAoo7BTa2AECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQWEMiqpr20CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgsAJCGYWdWgMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikglFFJfW0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEChRUQyijs1BoYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBoYxK6mubAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKyAUEZhp9bACBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCGVUUl/bBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGEFhDIKO7UGRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRSQCijkvraJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAorIJRR2Kk1MAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCSAkIZldTXNgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBYAaGMwk6tgREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBDKqKS+tgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHCCghlFHZqDYwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopIBQRiX1tU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUVkAoo7BTa2AECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQWEMiqpr20CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgsAJCGYWdWgMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikglFFJfW0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEChRUQyijs1BoYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBoYxK6mubAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKyAUEZhp9bACBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCGVUUl/bBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGEFhDIKO7UGRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRSQCijkvraJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAorIJRR2Kk1MAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCSAkIZldTXNgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBYAaGMwk6tgREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBDKqKS+tgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHCCghlFHZqDYwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopIBQRiX1tU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUVkAoo7BTa2AECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQWEMiqpr20CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgsAJCGYWdWgMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikglFFJfW0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEChRUQyijs1BoYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBoYxK6mubAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKyAUEZhp9bACBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCGVUUl/bBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGEFhDIKO7UGRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRSQCijkvraJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAorIJRR2Kk1MAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCSAkIZldTXNgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBYAaGMwk6tgREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBDKqKS+tgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHCCghlFHZqDYwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopIBQRiX1tU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUVkAoo7BTa2AECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQWEMiqpr20CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgsAJCGYWdWgMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikglFFJfW0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEChRUQyijs1BoYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBoYxK6mubAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKyAUEZhp9bACBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCGVUUl/bBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGEFhDIKO7UGRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRSQCijkvraJkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAorIJRR2Kk1MAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCSAkIZldTXNgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBYAaGMwk6tgREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBDKqKS+tgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHCCghlFHZqDYwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopIBQRiX1tU2AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgUVkAoo7BTa2AECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQWEMiqpr20CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgsAJCGYWdWgMjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikglFFJfW0TIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEChRUQyijs1BoYAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBoYxK6mubAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKKyAUEZhp9bACBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCGVUUl/bBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGEFhhZ2ZAZGgAABAgQIEKgxgb//+5fj2f/xwxrrte4SIECAAAECBAgQIFCLAi//w+5a7LY+EyBAgAABAgQIEKg5AaGMmpsyHSZAgAABAgSKKvD9H/wo0peDAAECBAgQIECAAAECBAgQIECAAAECBAgQKIaA7UuKMY9GQYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFSZgFBGlU2I7hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLFEBDKKMY8GgUBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQZQJCGVU2IbpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIFENAKKMY82gUBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJUJCGVU2YToDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAMAaGMYsyjURAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJVJiCUUWUTojsECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAMQSEMooxj0ZBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVJmAUEaVTYjuECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAsUQEMooxjwaBQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBlAkOrrD+6Q4AAAQIECBAohMDESR+OT/zRH/T7WHp6eqKn53C/t6MBAuUS+N8//3n87d/+bXT9+tfZmvyt33p/1NfXx/veNyJbTYUIECBAgAABAgNRYPy54wbisI2ZAAECBAgQIECAwGkJCGWcFp+LCRAgQIAAAQLHFrj88ksiffX3cfDgwUhfhw8LZvS3tfr9L/DQQw/Fd//8m28HjXqyNXbFFVfEo48+GnV1ddlqKkSAAAECBAgQIECAAAECBAgQIECAAIHeCghl9FbKeQQIECBAgAABAgQI9IvAm2++GYsXL47vf//72eoPGzYsvvrVr8ZnP/vZbDUVIkCAAAECBAgQIECAAAECBAgQIECAQF8FhDL6KuZ8AgQIECBAgAABAgSyCfzsZz+LT3/60/Hyyy9nq/nBD34wWlpaYtq0adlqKkSAAAECBAgQIECAAAECBAgQIECAAIFTERh8Khe5hgABAgQIECBAgAABAqcrsG7duvjEJz6RNZDxh3/4h/HXf/3XAhmnOzmuJ0CAAAECBAgQIECAAAECBAgQIEAgi4BQRhZGRQgQIECAAAECBAgQ6K1Ad3d33HHHHbFo0aJ46623envZSc+79tpro62tLc4666yTnusEAgQIECBAgAABAgQIECBAgAABAgQIlEPA9iXlUNYGAQIECBAgQIAAAQIlgddff70Uxvibv/mbbCLve9/74vHHH4/58+dnq6kQAQIECBAgQIAAAQIECBAgQIAAAQIEcggIZeRQVIMAAQIECBAgQIAAgZMKbN26tRTIePXVV096bm9POPfcc+OZZ56J8847r7eXOI8AAQIECBAgQIAAAQIECBAgQIAAAQJlE7B9SdmoNUSAAAECBAgQIEBg4Ao8/fTTMWfOnMgZyPjkJz8ZL7zwgkDGwP2xMnICBAgQIECAAAECBAgQIECAAAECVS9gpYyqnyIdJECAAAECBAgQIFC7AgcPHozrrrsuWltbsw1i0KBBsXTp0tJXeuwgQIAAAQIECBAgQIAAAQIECBAgQIBAtQoIZVTrzOgXAQIECBAgQIAAgRoX+Od//uf49Kc/HS+++GK2kZx55pnxne98J9IqGQ4CBAgQIHC6AjfddFPU1dXF7/7u78b48ePf/fMDH/jA6ZZ2PQECBAgQIECAAAECBAgQKAkIZfhBIECAAAECBAgQIEAgu8APf/jD+JM/+ZP4xS9+ka32eeedF9/73vdKb5hlK6oQAQIECAxogfe9733x5JNPHmWQghof+tCH3g1qpNDGkcGN9HjYsGFHXecJAgQIECBAgAABAgQIECDwXgGhjPeK+J4AAQIECBAgQIAAgVMWOHz4cDQ3N8fy5cujp6fnlOu898L58+fH448/HunNMwcBAgQIEMglcOWVVx4zlJG23/qHf/iH0tex2krbZ5199tm/EdRIK22cc8457wY5Ro8efaxLPUeAAAECBAgQIECAAAECA0xAKGOATbjhEiBAgAABAgQIEOgvgTfffDOuvvrqSKtk5DrSp5Dvv/91rRL2AABAAElEQVT+uPbaa3OVVIcAAQIECLwr8NGPfjQ+8pGP9HmrrRRCfO2110pfP/nJT96td+SDM844oxTaOHKFjSODG7/zO78TQ4YMOfISjwkQIECAAAECBAgQIECggAJCGQWcVEMiQIAAAQIECBAgUG6Bn/3sZ/HpT386Xn755WxNn3XWWfHMM8/EtGnTstVUiAABAgQIvFcgrZZx++23v/fp0/7+V7/6VezcubP0daxiKZAxbty40soaR66wkYIbKcjx4Q9/2ApRx4LzHAECBAgQIECAAAECBGpMQCijxiZMdwkQIECAAAECBAhUm0Bra2tcf/318dZbb2XrWgpitLS0xAc/+MFsNRUiQIAAAQLHEkihwv4IZRyrrSOf6+7ujj179pS+Nm/efORL7z7+7d/+7dLfhSm0ceSKG+lx+vrABz4QaSsVBwECBAgQIECAAAECBAhUr4BQRvXOjZ4RIECAAAECBAgQqGqBQ4cOxZ133hnf/va3s/ZzyZIl8cADD8TQof65khVWMQIECBA4psDo0aNjzpw58eyzzx7z9Uo++fOf/zzS19/93d8dsxtpm68PfehD7wY23hveSN+ncxwECBAgQIAAAQIECBAgUDkBv+WsnL2WCRAgQIAAAQIECNSswKuvvhqLFi2KrVu3ZhtDXV1dPPHEEzF//vxsNRUiQIAAAQK9EUhbmFRjKONkff/1r38d//iP/1j6Ota5aRWNs88+uxTaOHKljSO3SznzzDOPdannCBAgQIAAAQIECBAgQCCTgFBGJkhlCBAgQIAAAQIECAwUgRTESEu9v/7669mGnN4oeuaZZ+IjH/lItpoKESBAgACB3grMmjUrzjrrrKx/t/W27f487/Dhw/Haa6+Vvn7yk58cs6n3v//974Y23tkWZfz48aXnUngjbSVmi5Rj0nmSAAECBAgQIECAAAECvRIQyugVk5MIECBAgAABAgQIEEgCTz75ZNx1112Rti7JdXzyk5+M73znO+GTurlE1SFAgACBvgqkLT4uu+yy0t9zfb221s//5S9/GT/96U9LX8cby4c//OGjtkj52Mc+Fuedd97xLvE8AQIECBAgQIAAAQIECPy7gFCGHwUCBAgQIECAAAECBE4q8NZbb8X1118fra2tJz23tyekT93eeuutcfvtt/sEbm/RnEeAAAEC/SaQtjBJ4UPH0QLv3SJl1KhR8U//9E9Hn+gZAgQIECBAgAABAgQIEDhKQCjjKBJPECBAgAABAgQIECBwpMDLL79c2q7kZz/72ZFPn9bj3/qt34oVK1bEH//xH59WHRcTIECAAIFcAh/96EdL22i9+OKLuUoWss7gwYMFMgo5swZFgAABAgQIECBAgEB/CQzur8LqEiBAgAABAgQIECBQ+wLf//7348ILL4ycgYy01PkLL7wgkFH7Px5GQIAAgcIJpNUyHCcWuO222058glcJECBAgAABAgQIECBA4DcEhDJ+g8M3BAgQIECAAAECBAgkgcOHD8cDDzxQWiHjzTffzIYyb968+Ku/+qs499xzs9VUiAABAgQI5BK4/PLLY9iwYbnKFa7O7NmzY9myZYUblwERIECAAAECBAgQIECgPwWEMvpTV20CBAgQIECAAAECNSjwi1/8Ii677LJobm4uhTNyDGHo0KHxla98JVpaWuJ973tfjpJqECBAgACB7AJnnXVWzJw5M3vdIhQcP358rF69ughDMQYCBAgQIECAAAECBAiUVUAoo6zcGiNAgAABAgQIECBQ3QIvvvhi/NEf/VH88Ic/zNbR9AbXunXr4oYbbshWUyECBAgQINBfArYwOVp2xIgRke4RHAQIECBAgAABAgQIECDQd4Ghfb/EFQQI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AAECBAgQINBbgblz58a2bdti0qRJvb3kpOdt3rw50v1p+tNBgAABAgQIECBAgACB3ggIZfRGyTkECBAgQIAAAQInFOjp6Ylly5bFvHnzIm1dkutYuHBhbNmyJSZMmJCrpDoECBAgQIAAAQIDSGDixInR0dER8+fPzzbqvXv3llbMePTRR7PVVIgAAQIECBAgQIAAgeIKCGUUd26NjAABAgQIECBQFoHOzs6YOXNmLF++PFt7w4YNi29961uxatWqqKury1ZXIQIECBAgQIAAgYEnMHLkyFizZk089NBDMXTo0CwAhw4diptuuimuuOKKOHDgQJaaihAgQIAAAQIECBAgUEwBoYxizqtRESBAgAABAgTKIpA+dVhfXx/t7e3Z2hs3bly88MILccMNN2SrqRABAgQIECBAgACBP/3TPy3dt44dOzYbxurVq2PatGmxa9eubDUVIkCAAAECBAgQIECgWAJCGcWaT6MhQIAAAQIECJRNoKWlJaZPnx579uzJ1mZjY2Ps2LEjLrjggmw1FSJAgAABAgQIECDwjkB/3G++9NJLMXXq1Ghra3unGX8SIECAAAECBAgQIEDgXQGhjHcpPCBAgAABAgQIEOiNQFdXVyxZsiSampoiPc513HzzzbFhw4bI+cnFXH1ThwABAgQIECBAoDgCaWW2jRs3xnXXXZdtUPv27YtLL700vvSlL8Xhw4ez1VWIAAECBAgQIECAAIHaFxDKqP05NAICBAgQIECAQNkE0qoYaXWMFStWZGtzxIgRpT2+H3744RgyZEi2ugoRIECAAAECBAgQOJ7A0KFD4/HHH4+VK1dGXV3d8U7r0/MpjHH//ffHpz71qXjjjTf6dK2TCRAgQIAAAQIECBAoroBQRnHn1sgIECBAgAABAlkF2tvbo76+Pjo6OrLVnThxYmzdujXmz5+fraZCBAgQIECAAAECBHorsGjRotiyZUtMmDCht5ec9Lznn3++dN+ctuVzECBAgAABAgQIECBAQCjDzwABAgQIECBAgMBJBZqbm2PWrFnR2dl50nN7e8LcuXNj27ZtMXny5N5e4jwCBAgQIECAAAEC2QWmTJkS27dvj9mzZ2ervXv37jj//POjpaUlW02FCBAgQIAAAQIECBCoTQGhjNqcN70mQIAAAQIECJRFYP/+/bFgwYJYunRpdHd3Z2lz8ODB8eUvfznWrVsXo0aNylJTEQIECBAgQIAAAQKnIzB69Oh47rnn4u67745BgwadTql3rz148GA0NTXF9ddfH4cOHXr3eQ8IECBAgAABAgQIEBhYAkIZA2u+jZYAAQIECBAg0GuBnTt3RkNDQ7S2tvb6mpOdmH7ZvX79+rjzzjtPdqrXCRAgQIAAAQIECJRVIIUx7rvvvli7dm3W8PATTzwRjY2N8corr5R1PBojQIAAAQIECBAgQKA6BIQyqmMe9IIAAQIECBAgUFUCbW1tpUBGCmbkOt5ZFnrGjBm5SqpDgAABAgQIECBAILtAf2yzt3nz5kj3wxs3bszeXwUJECBAgAABAgQIEKhuAaGM6p4fvSNAgAABAgQIlFWgp6cnli1bFvPmzYu0dUmuY9GiRbFly5aYMGFCrpLqECBAgAABAgQIEOg3gYkTJ8bWrVtj4cKF2drYu3dvpIDyww8/nK2mQgQIECBAgAABAgQIVL+AUEb1z5EeEiBAgAABAgTKItDZ2RkzZ86M5cuXZ2tv+PDh8eSTT8bKlSujrq4uW12FCBAgQIAAAQIECPS3wIgRI2LVqlXxyCOPxNChQ7M0d+jQobjllltiwYIFWUPQWTqnCAECBAgQIECAAAEC/SIglNEvrIoSIECAAAECBGpLoKOjI+rr66O9vT1bx8ePHx+bNm2Ka665JltNhQgQIECAAAECBAiUW+ALX/hCaduRsWPHZmu6tbW1tF3grl27stVUiAABAgQIECBAgACB6hQQyqjOedErAgQIECBAgEDZBFpaWmL69OmxZ8+ebG02NjbG9u3bS79ozlZUIQIECBAgQIAAAQIVErjgggtix44dkf7MdezcuTOmTp0abW1tuUqqQ4AAAQIECBAgQIBAFQoIZVThpOgSAQIECBAgQKAcAl1dXbFkyZJoamqK9DjXceutt8aGDRtizJgxuUqqQ4AAAQIECBAgQKDiAuPGjSutmPG5z30uW1/27dsXl156adxxxx3R09OTra5CBAgQIECAAAECBAhUj4BQRvXMhZ4QIECAAAECBMomkFbFSKtjrFixIlubI0eOjDVr1kRzc3MMGTIkW12FCBAgQIAAAQIECFSLwNChQ+Ob3/xmrFq1KkaMGJGlW4cPH46vfvWrMXPmzOjs7MxSUxECBAgQIECAAAECBKpHQCijeuZCTwgQIECAAAECZRFob2+P+vr66OjoyNbepEmTSvXmz5+fraZCBAgQIECAAAECBKpVYOHChbF169aYMGFCti6+c5+etklxECBAgAABAgQIECBQHAGhjOLMpZEQIECAAAECBE4q8OCDD8asWbOyfgIvBTFSwCMFMxwECBAgQIAAAQIEBorA5MmTY/v27TF79uxsQ04r2p1//vnR0tKSraZCBAgQIECAAAECBAhUVkAoo7L+WidAgAABAgQIlEVg//79sWDBgrjtttuiu7s7S5tpi5Kvfe1rpS1L0tYlDgIECBAgQIAAAQIDTWD06NHx3HPPxb333huDBg3KMvyDBw9GU1NTXHPNNdHV1ZWlpiIECBAgQIAAAQIECFROQCijcvZaJkCAAAECBAiURWDnzp3R0NAQra2t2dobM2ZM/OAHP4ilS5dmq6kQAQIECBAgQIAAgVoUSGGMe+65pxTOSCGNXMdTTz0V06dPj7R6hoMAAQIECBAgQIAAgdoVEMqo3bnTcwIECBAgQIDASQXa2tpKgYwUzMh1pIBHWqZ5xowZuUqqQ4AAAQIECBAgQKDmBdI2Juk+ecqUKdnGkrYJrK+vj/b29mw1FSJAgAABAgQIECBAoLwCQhnl9dYaAQIECBAgQKAsAj09PbFs2bKYN29epK1Lch2LFy+OTZs2xfjx43OVVIcAAQIECBAgQIBAYQQmTJgQW7ZsiYULF2YbU2dnZ8yaNSsefPDBbDUVIkCAAAECBAgQIECgfAJCGeWz1hIBAgQIECBAoCwC6Ze2M2fOjOXLl2drb/jw4bFy5cpISyinxw4CBAgQIECAAAECBI4tUFdXF6tWrYrHHnsshg0bduyT+vhsd3d33HbbbbFgwYKsoes+dsPpBAgQIECAAAECBAicgoBQximguYQAAQIECBAgUK0C/bG8cVoVI62OsWjRomodtn4RIECAAAECBAgQqDqBG2+8MV544YUYN25ctr61trZm354wW+cUIkCAAAECBAgQIEDgmAJCGcdk8SQBAgQIECBAoPYEWlpaYvr06bFnz55snZ8xY0ZpX+yGhoZsNRUiQIAAAQIECBAgMFAELrjggtixY0c0NjZmG/LOnTtLwYwU0HAQIECAAAECBAgQIFD9AkIZ1T9HekiAAAECBAgQOKFAV1dXLFmyJJqamiI9znEMGjQobr/99li/fn2MGTMmR0k1CBAgQIAAAQIECAxIgbFjx8aGDRvipptuyjb+/fv3l7YyWbp0afT09GSrqxABAgQIECBAgAABAvkFhDLym6pIgAABAgQIECibQFoVI62OsWLFimxtjho1KtauXRsPPPBADB7sdjEbrEIECBAgQIAAAQIDVmDIkCHxjW98I1atWhUjRozI5tDc3BwzZ86Mzs7ObDUVIkCAAAECBAgQIEAgr4Dfsuf1VI0AAQIECBAgUDaB9vb2qK+vj46OjmxtTpo0KbZt2xZz587NVlMhAgQIECBAgAABAgT+TWDhwoWxdevWmDhxYjaS/vh3QbbOKUSAAAECBAgQIECAQAhl+CEgQIAAAQIECNSgwIMPPhizZs3K+om4+fPnlwIeOX9BXIO0ukyAAAECBAgQIECgXwUmT56cPQidVtD7+Mc/nnUFvX5FUJwAAQIECBAgQIDAABIQyhhAk22oBAgQIECAQO0LvLN39G233Rbd3d1ZBjR06ND4+te/HmvWrImRI0dmqakIAQIECBAgQIAAAQLHF0hbBq5bty7uv//+bFsGHjx4MJYsWRJNTU3R1dV1/Ma9QoAAAQIECBAgQIBAWQWEMsrKrTECBAgQIECAwKkL7Ny5MxoaGqK1tfXUi7znyrFjx0Za7viWW255zyu+JUCAAAECBAgQIECgvwXuuuuuePbZZ2P06NHZmmppaYnp06dHWj3DQYAAAQIECBAgQIBA5QWEMio/B3pAgAABAgQIEDipQFtbWymQkYIZuY4U8NixY0c0NjbmKqkOAQIECBAgQIAAAQJ9FJg9e3Zs3749pkyZ0scrj396R0dH1NfXlwLYxz/LKwQIECBAgAABAgQIlENAKKMcytogQIAAAQIECJyiQE9PTyxbtizmzZsXaeuSXMd1110XmzZtinHjxuUqqQ4BAgQIECBAgAABAqcoMGHChNiyZUssWrToFCscfVlnZ2fMnDkzvvrVrx79omcIECBAgAABAgQIECibgFBG2ag1RIAAAQIECBDom8A7v0Rdvnx53y48wdl1dXWxcuXKePzxx2P48OEnONNLBAgQIECAAAECBAiUU+Cde/Unnngi2716CnnfcccdpZD3vn37yjkcbREgQIAAAQIECBAg8O8CQhl+FAgQIECAAAECVSjQH8sN98en76qQTpcIECBAgAABAgQI1LTAtddem31Vu7Qd4tSpUyPndog1jazzBAgQIECAAAEC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AAEEEEAAAQQQQAABBBBAAIEIESAoI0JOBN1AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgdgSICgjts4nR4MAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACESJAUEaEnAi6gQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKxJUBQRmydT44GAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBCJEgKCMCDkRdAMBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEYkuAoIzYOp8cDQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghEiABBGRFyIugGAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCMSWAEEZsXU+ORoEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiBABgjIi5ETQDQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCILQGCMmLrfHI0CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBAhAgRlRMiJoBsIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEFsCBGXE1vnkaBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgQgQIyoiQE0E3EEAAAQT+n737AJOrqhsHfJJNsrQFAktnCX0pQiihN0NLVCQRUVCBBCTWhPIpkMj3WRKKkKghEVeQXqRICwjSewuE3ksogvReE0r43zO6/pdlp+zszOydmfc8zzw7c8sp75mduXPv755DgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1JaAoIza6k+tIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFIiICgjJR2hGgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBtCQjKqK3+1BoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgJQKCMlLSEapBgAABAgS6Enj33XfDCiusEPr06dPrj0UXXbSrKuZdloa6xzqsscYaYe7c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gW4IxGk/pk2b1o090rdpnIaloaEhfRVTIwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBADgFBGTlwrCJAgAABArUiMHLkyDBixIiqbM6oUaPC0KFDq7LuKk2AAAECBAgQIECAAAECBAgQIECAAAECBAjUt4CgjPruf60nQIAAgToSmD59emhsbKyqFjc3N4cpU6ZUVZ1VlgABAgQIECBAgAABAgQIECBAgAABAgQIECDQLiAoo13CXwIECBAgUOMCLS0tYfz48VXVykmTJoUYmCERIECAAAECBAgQIECAAAECBAgQIECAAAECBKpRQFBGNfaaOhMgQIAAgSIFJkyYEFpbW4vcu7K7DRkyJIwZM6ayhSqNAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBCAUEZJcSUFQECBAgQSLtAnL6kra0t7dUMDQ0NmXrGvxIBAgQIECBAgAABAgQIECBAgAABAgQIECBAoFoFBGVUa8+pNwECBAgQKFJg6NChYbfddity78rsNnbs2BBHypAIECBAgAABAgQIECBAgAABAgQIECBAgAABAtUsICijmntP3QkQIECAQJECkydPDk1NTUXuXd7dmpubw6RJk8pbiNwJECBAgAABAgQIECBAgAABAgQIECBAgAABAhUQEJRRAWRFECBAgACBtAm0tLSEiRMnpq1amfqkOWAklWAqRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECKRWQFBGartGxQgQIECAQHkFxo0bl7opQuLUKqNHjy5vw+VOgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiQgKCMCkErhgABAgQIpE2goaEhtLW1paZaaatPamBUhAABAgQIECBAgAABAgQIECBAgAABAgQIEKhaAUEZVdt1Kk6AAAECBHouMGTIkDBq1KieZ1SCHMaOHRtaW1tLkJMsCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLpEBCUkY5+UAsCBAgQINBrAlOmTAnNzc29Vn4suKWlJUyaNKlX66BwAgQIECBAgAABAgQIECBAgAABAgQIECBAgECpBQRllFpUfgQIECBAoMoEYkDG5MmTe7XW06ZNC01NTb1aB4UTIECAAAECBAgQIECAAAECBAgQIECAAAECBEotICij1KLyI0CAAAECVSgwevToEKcy6Y00bNiwMHLkyN4oWpkECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgbIKCMooK6/MCRAgQIBA9Qi0tbWFhoaGila4sbExHHPMMRUtU2EECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoJCMqolLRyCBAgQIBAygXiSBljx46taC3Hjx8fWltbK1qmwggQIECAAAECBAgQIECAAAECBAgQIECAAAEClRIQlFEpaeUQIECAAIEqEJg0aVJoaWmpSE1jMMaECRMqUpZCCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQK9ISAoozfUlUmAAAECBFIq0NTUFKZNm1aR2k2dOjXE6UskAgQIECBAgAABAgQIECBAgAABAgQIECBAgECtCgjKqNWe1S4CBAgQIFCkwMiRI8OwYcOK3Luw3UaMGBGGDx9e2Ma2IkCAAAECBAgQIECAAAECBAgQIECAAAECBAhUqYCgjCrtONUmQIAAAQLlFDjmmGPKNopFHI1j+vTp5ay+vAkQIECAAAECBAgQIECAAAECBAgQIECAAAECqRAQlJGKblAJAgQIECCQLoHW1tYwfvz4slRq4sSJoaWlpSx5y5QAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkCYBQRlp6g11IUCAAAECKRKYMGFCiMEZpUxDhgwJ48aNK2WW8iJAgAABAgQIECBAgAABAgQIECBAgAABAgQIpFZAUEZqu0bFCBAgQIBA7wo0NjaGqVOnlrQSbW1toaGhoaR5yowAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkFYBQRlp7Rn1IkCAAAECKRAYPnx4GDFiRElqMmrUqBBHypAIECBAgAABAgQIECBAgAABAgQIECBAgAABAvUiICijXnpaOwkQIECAQJEC06dPD01NTUXu/e/dmpubw5QpU3qUh50JECBAgAABAgQIECBAgAABAgQIECBAgAABAtUmICij2npMfQkQIECAQIUFWlpawsSJE3tU6uTJk0MMzJAIECBAgAABAgQIECBAgAABAgQIECBAgAABAvUkICijnnpbWwkQIECAQJEC48aNC62trUXtHacsGT16dFH72okAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUM0CgjKquffUnQABAgQIVEigoaEhtLW1dbu0YvfrdkF2IECAAAECBAgQIECAAAECBAgQIECAAAECBAikUEBQRgo7RZUIECBAgEAaBYYOHRpGjRrVraqNHTs2xJEyJAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAPQoIyqjHXtdmAgQIECBQpMCUKVNCc3NzQXu3tLSESZMmFbStjQgQIECAAAECBAgQIECAAAECBAgQIECAAAECtSggKKMWe1WbCBAgQIBAmQRiQEahgRZHH310aGpqKlNNZEuAAAECBAgQIECAAAECBAgQIECAAAECBAgQSL+AoIz095EaEiBAgACBVAmMGTMm75QkcaqT3XffPVX1VhkCBAgQIECAAAECBAgQIECAAAECBAgQIECAQKUFBGVUWlx5BAgQIECgygUaGhpCW1tbiH+7So2NjZn1Xa2zjAABAgQIECBAgAABAgQIECBAgAABAgQIECBQTwKCMuqpt7WVAAECBAiUSGDIkCFh7NixXeY2fvz40Nra2uU6CwkQIECAAAECBAgQIECAAAECBAgQIECAAAEC9SQgKKOeeltbCRAgQIBACQUmTZoUmpubP5djDMaYMGHC55Z5QYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoVwFBGfXa89pNgAABAgR6KNDU1BQmT578uVymTp0a4vQlEgECBAgQIECAAAECBAgQIECAAAECBAgQIECAQAiCMrwLCBAgQIAAgaIFRo8eHYYOHZrZf8SIEWH48OFF52VHAgQIECBAgAABAgQIECBAgAABAgQIECBAgECtCQjKqLUe1R4CBAgQIFBhgba2tsw0JtOnT69wyYojQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECKRbQFBGuvtH7QgQIECAQOoFWltbw2233RZaWlpSX1cVJECAAAECBAgQIECAAAECBAgQIECAAAECBAhUUkBQRiW1lUWAAAECBGpUYNVVV63RlmkWAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKB4AUEZxdvZkwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQVUBQRlYaKwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECxQsIyijezp4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgawCgjKy0lhBgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECheQFBG8Xb2JECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkFRCUkZXGCgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA8QKCMoq3sycBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIKuAoIysNFYQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBIoXEJRRvJ09CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZBQRlZKWxggABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQvICgjOLt7EmAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQyCogKCMrjRUECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgeIFBGUUb2dPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBWAUEZWWmsIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgULyAoo3g7exIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEsgoIyshKYwUBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoHgBQRnF29mTAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJBVQFBGVhorCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLFC/Qrfld7EiAQBY7/0+nh3HNnwCBAgAABAgQIEKhzgb322T3stdeuda6g+QQIECBAgAABAgQIECBAgAABAgQIdBQQlNFRw3MCRQi8+e5b4bkXXyliT7sQIECAAAECBAjUksDbb79XS83RFgIECBAgQIAAAQIECBAgQIAAAQIESiBg+pISIMqCAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBZQFBGZxGvCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIlEBCUUQJEWRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEOgsIyugs4jUBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoAQCgjJKgCgLAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBnAUEZnUW8JkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUQEBQRgkQZUGAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6CwgKKOziNcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgRIICMooAaIsCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKdBQRldBbxmgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAgFBGSVAlAUBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoLNAv84LvCZAoPwCu++2c/jOd0aWvyAlECBAgAABAgQIFCVw0glnhRl/v6qofe1EgAABAgQIECBAgAABAgQIECBAgACBdgFBGe0S/hKooEDz4gPDGmuuWsESFUWAAAECBAgQINAdgUUHLtKdzW1LgAABAgQIECBAgAABAgQIECBAgACBLgVMX9Ili4UECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZ4JCMromZ+9CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJdCgjK6JLFQgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAzwQEZfTMz94ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgS4FBGV0yWIhAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBnAoIyeuZnbwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAlwKCMrpksZAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0DMBQRk987M3AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBLAUEZXbJYSIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDomYCgjJ752ZsAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0KWAoIwuWSwkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECPRMoF/Pdrc3AQIECBAgQIAAAQIECESBjz76OLz//vtlx+jfv38YMGBA2ctRAAECBAgQIECAAAECBAgQIECAAAECPRcQlNFzQzkQIECAAAECBAgQIEAgfPbZZ+HTTz8tu0S/fn7GlR1ZAQQIECBAgAABAgQIECBAgAABAgRKJOBsXokgZUOAAAECBAgQIECAQH0LxKCM+Ch3qkQZ5W6D/AkQIECAAAECBAgQIECAAAECBAjUi0DfemmodhIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikgKKOS2soiQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE6kZAUEbddLWGEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABApUUEJRRSW1lESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAnUjICijbrpaQwkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFKCgjKqKS2sggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG6ERCUUTddraEECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQUEZVRSW1kECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA3QgIyqibrtZQAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoJICgjIqqa0sAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoG4EBGXUTVdrKAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFBJAUEZldRWFgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFA3AoIy6qarNZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCopICgjEpqK4sAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoGwFBGXXT1RpKgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVFJAUEYltZVFgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI1I2AoIy66WoNJUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBCopICijktrKIkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBOpGQFBG3XS1hhIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKVFBCUUUltZREgQIAAAQIUUrFhAABAAElEQVQECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJ1IyAoo266WkMJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBSgoIyqiktrIIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBuhEQlFE3Xa2hBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQCUFBGVUUltZBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQN0ICMqom67WUAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCSAoIyKqmtLAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBuBARl1E1XaygBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQSQFBGZXUVhYBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQNwKCMuqmqzWUAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKSAoIxKaiuLAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqBsBQRl109UaSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRSQFBGJbWVRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNSNgKCMuulqDSVAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQqKSAoo5LayiJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTqRkBQRt10tYYSIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEClRQQlFFJbWURIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECdSMgKKNuulpDCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgUoKCMqopLayCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgboREJRRN12toQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAlBQRlVFJbWQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDdCAjKqJuu1lACBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgkgKCMiqprSwCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgbgQEZdRNV2soAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUEkBQRmV1FYWAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUDcCgjLqpqs1lAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKikgKCMSmoriwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKgbAUEZddPVGkqAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhUUkBQRiW1lUWAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjUjYCgjLrpag0lQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKikgKKOS2soiQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE6kZAUEbddLWGEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABApUUEJRRSW1lESBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAnUjICijbrpaQwkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFKCgjKqKS2sggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIG6ERCUUTddraEECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAJQUEZVRSW1kECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA3Qj0q5uWaigBAgQIECBAgAABAgQIECBQUwKvvPJKeO6552qqTRpDgAABAukUmDVrVrcrdu+993Z7HzsQIECAAIFCBPr06RM22GCDQja1DQECKRAwUkYKOkEVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdoTEJRRe32qRQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAKBARlpKATVIEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoPQFBGbXXp1pEgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIpEBAUEYKOkEVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdoTEJRRe32qRQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAKBARlpKATVIEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoPQFBGbXXp1pEgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIpEBAUEYKOkEVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdoTEJRRe32qRQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAKBARlpKATVIEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoPQFBGbXXp1pEgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIpEBAUEYKOkEVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdoTEJRRe32qRQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAKBARlpKATVIEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoPQFBGbXXp1pEgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIpEBAUEYKOkEVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdoTEJRRe32qRQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAKBARlpKATVIEAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoPQFBGbXXp1pEgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIpEBAUEYKOkEVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdoTEJRRe32qRQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAKBARlpKATVIEAAQIECBAgQIAAAQIECBAg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EEgYBBwAwQGsqFTFGbR1qM/3eL46VW3Vap8Pq1atdapWqxat2gooOAGKCggIGcg8zyHzPCfk7XX04uXknH3O3nff8XxL7++evdf6r73Wt8/Nj3v3/6zVle+uuREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLtJiApo93oXZgAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDoygKSMrry3TU3AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoN0EJGW0G70LEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAl1ZQFJGV7675kaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0m4CkjHajd2ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgKwtIyujKd9fcCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgXYTkJTRbvQuTIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECHRlAUkZXfnumhsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQbgKSMtqN3oUJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBrizQsytPztwIdDWBbt26dbUpmQ8BAp1IIO+/QXnjOhGNoRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKgpIyqjI4iSBjifQp0+f0Lt377oHdvz48brbakiAAIF6BI4dO1ZPsxPaDBgw4IRzThAgQIBAfoGY7CbhLb+fSAIECBAgQIAAAQIECBAgQIAAAQJtKSApoy21XYtACwWy/PE9S9sWDks4AQINItC9e75dz/LGNQiraRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJdWCDf05UuDGJqBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgSIEJGUUoagPAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgKSMMhCHBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEiBCRlFKGoDwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYCkjDIQhwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBIgQkZRShqA8CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmApIwyEIcECBAgQIAAAQIECBAgQIAAAQIECBAgQKBJ4NixY00vM33v0aNHpvYaEyBAgAABAgQIdE0BSRld876aFQECBAgQKFyge3f/2VA4qg4JECBAgAABAgQIECBAoMMLHDp0KPMYe/funTlGAAECBAgQIECAQNcU8HSla95XsyJAgAABAoUL9OrVK1efR44cyRUniAABAgQIECBAgAABAgQIdASBPEkZffr06QhDNwYCBAgQIECAAIEOICApowPcBEMgQIAAAQKdQaBv3765hpnnj1e5LiSIAAECBAgQIECAAAECBAi0gkCe32slZbTCjdAlAQIECBAgQKCTCkjK6KQ3zrAJECBAgEB7COTZDzfPH6/aY26uSYAAAQIECBAgQIAAAQIEKgnk+b3W9iWVJJ0jQIAAAQIECDSmgKSMxrzvZk2AAAECBHIJ5Pmj0uHDh3NdSxABAgQIECBAgAABAgQIEOgIAnmSMqyU0RHunDEQIECAAAECBDqGgKSMjnEfjIIAAQIECHQKgTxJGXn+eNUpMAySAAECBAgQIECAAAECBBpCIM+HDSRlNMRbwyQJECBAgAABAnUJSMqoi0kjAgQIECBAIApIyvA+IECAAAECBAgQIECAAIFGE8jzYYM8vz83mqv5EiBAgAABAgQaRUBSRqPcafMkQIAAAQIFCOT5pE+eTxQVMFRdECBAgAABAgQIECBAgACBQgTyJGXk+f25kMHqhAABAgQIECBAoMMJSMrocLfEgAgQIECAQMcVyPNJnzx/vOq4AkZGgAABAgQIECBAgAABAo0mkOf3WkkZjfYuMV8CBAgQIECAQHUBSRnVbdQQIECAAAECZQKSMspAHBIgQIAAAQIECBAgQIBAlxfIk5SR5/fnLg9pggQIECBAgACBBhWQlNGgN960CRAgQIBAHoE8f1SyfUkeaTEECBAgQIAAAQIECBAg0FEE8iRlWCmjo9w94yBAgAABAgQItL+ApIz2vwdGQIAAAQIEOo1AnqSMPH+86jQgBkqAAAECBAgQIECAAAECXV4gz++1kjK6/NvCBAkQIECAAAECdQtIyqibSkMCBAgQIEBAUob3AAECBAgQIECAAAECBAg0mkCepIw8vz83mqv5EiBAgAABAgQaRUBSRqPcafMkQIAAAQIFCOT5pI/tSwqA1wUBAgQIECBAgAABAgQItJtAnqSMPL8/t9sEXZgAAQIECBAgQKBVBSRltCqvzgkQIECAQNcSyPNJnzx/vOpaamZDgAABAgQIECBAgAABAp1ZIM/vtZIyOvMdN3YCBAgQIECAQLECkjKK9dQbAQIECBDo0gIDBgzIPL/du3dnjhFAgAABAgQIECBAgAABAgQ6ikCe32sHDhzYUYZvHAQIECBAgAABAu0sICmjnW+AyxMgQIAAgc4kMGTIkMzD3bp1a+YYAQQIECBAgAABAgQIECBAoKMIbNmyJfNQ8vz+nPkiAggQIECAAAECBDqFgKSMTnGbDJIAAQIECHQMgdNOOy3zQCRlZCYTQIAAAQIECBAgQIAAAQIdSEBSRge6GYZCgAABAgQIEOiEApIyOuFNM2QCBAgQINBeApIy2kvedQkQIECAAAECBAgQIECgvQQkZbSXvOsSIECAAAECBLqGgKSMrnEfzYIAAQIECLSJQJ6kjDx/vGqTybgIAQIECBAgQIAAAQIECBCoQyDP77W2L6kDVhMCBAgQIECAQIMISMpokBttmgQIECBAoAiBPEkZti8pQl4fBAgQIECAAAECBAgQINBeApIy2kvedQkQIECAAAECXUNAUkbXuI9mQYAAAQIE2kQgzyd9JGW0ya1xEQIECBAgQIAAAQIECBBoBYHjx4+H7du3Z+45z+/PmS8igAABAgQIECBAoFMISMroFLfJIAkQIECAQMcQyLNSxubNmzvG4I2CAAECBAgQIECAAAECBAhkFMj7O+3QoUMzXklzAgQIECBAgACBriogKaOr3lnzIkCAAAECrSCQJynj8OHDYe/eva0wGl0SIECAAAECBAgQIECAAIHWFcizdcmgQYNCt27dWndgeidAgAABAgQIEOg0ApIyOs2tMlACBAgQIND+Aqeeemro3j37fz7YwqT9750RECBAgAABAgQIECBAgEB2gTxJGbYuye4sggABAgQIECDQlQWyP1XpyhrmRoAAAQIECKQKxISM+ImfrEVSRlYx7QkQIECAAAECBAgQIECgIwhIyugId8EYCBAgQIAAAQKdW0BSRue+f0ZPgAABAgTaXCDPJ34kZbT5bXJBAgQIECBAgAABAgQIEChAYPPmzZl7yfN7c+aLCCBAgAABAgQIEOg0Aj07zUgNlAABAp1AYPfu3Z1glIZIoGUCz372s0PWPzAdPHgw+Plombvoji8wcOBA+0Z3/NtkhAQIECBAgAABAgQyCVgpIxOXxgQIECBAgAABAhUEJGVUQHGKAAECeQWWLl2aN1QcgU4j8La3vS3XWP185GIT1IkEZs6c2YlGa6gECBAgQIAAAQIECNQjICmjHiVtCBAgQIAAAQIE0gRsX5Kmo44AAQIECBAgQIAAAQIECBAgQIAAAQIEGlZgzZo1mec+YsSIzDECCBAgQIAAAQIEuq6ApIyue2/NjAABAgQIECBAgAABAgQIECBAgAABAgRaILB8+fLM0WPHjs0cI4AAAQIECBAgQKDrCkjK6Lr31swIECBAgAABAgQIECBAgAABAgQIECBAoAUCebbilJTRAnChBAgQIECAAIEuKCApowveVFMiQIAAAQIECBAgQIAAAQIECBAgQIAAgZYJxK1Ljh07lrmTcePGZY4RQIAAAQIECBAg0HUFJGV03XtrZgQIECBAgAABAgQIECBAgAABAgQIECCQU2DZsmWZI4cPHx769OmTOU4AAQIECBAgQIBA1xWQlNF1762ZESBAgAABAgQIECBAgAABAgQIECBAgEBOgeXLl2eOtHVJZjIBBAgQIECAAIEuLyApo8vfYhMkQIAAAQIECBAgQIAAAQIECBAgQIAAgawCkjKyimlPgAABAgQIECBQSUBSRiUV5wgQIECAAAECBAgQIECAAAECBAgQIECgoQXybF8ybty4hjYzeQIECBAgQIAAgRMFJGWcaOIMAQIECBAgQIAAAQIECBAgQIAAAQIECDS4gJUyGvwNYPoECBAgQIAAgYIEJGUUBKkbAgQIECBAgAABAgQIECBAgAABAgQIEOg6AgsXLsw8mbFjx2aOEUCAAAECBAgQINC1BSRldO37a3YECBAgQIAAAQIECBAgQIAAAQIECBAgkFFg8+bN4dChQxmjQpCUkZlMAAECBAgQIECgywtIyujyt9gECRAgQIAAAQIECBAgQIAAAQIECBAgQCCLwLJly7I0L7Xt06dPOOOMMzLHCSBAgAABAgQIEOjaApIyuvb9NTsCBAgQIECAAAECBAgQIECAAAECBAgQyCiwfPnyjBEhTJgwIXOMAAIECBAgQIAAga4vICmj699jMyRAgAABAgQIECBAgAABAgQIECBAgACBDAJ5VsqwdUkGYE0JECBAgAABAg0kICmjgW62qRIgQIAAAQIECBAgQIAAAQIECBAgQIBAbYE8K2VIyqjtqgUBAgQIECBAoBEFJGU04l03ZwIECBAgQIAAAQIECBAgQIAAAQIECBCoKpBnpYxx48ZV7U8FAQIECBAgQIBA4wpIymjce2/mBAgQIECAAAECBAgQIECAAAECBAgQIFBBwEoZFVCcIkCAAAECBAgQyCUgKSMXmyACBAgQIECAAAECBAgQIECAAAECBAgQ6IoCu3fvDlu3bs08NduXZCYTQIAAAQIECBBoCAFJGQ1xm02SAAECBAgQIECAAAECBAgQIECAAAECBOoRmDt3bj3NTmgzYcKEE845QYAAAQIECBAgQEBShvcAAQIECBAgQIAAAQIECBAgQIAAAQIECBD4o8DDDz+c2SImZPTo0SNznAACBAgQIECAAIGuLyApo+vfYzMkQIAAAQIECBAgQIAAAQIECBAgQIAAgToFHnrooTpb/qnZxRdf/KcDrwgQIECAAAECBAg0E5CU0QzDSwIECBAgQIAAAQIECBAgQIAAAQIECBBobIE8SRkXXXRRY6OZPQECBAgQIECAQFUBSRlVaVQQIECAAAECBAgQIECAAAECBAgQIECAQCMJbN26NaxevTrzlK2UkZlMAAECBAgQIECgYQQkZTTMrTZRAgQIECBAgAABAgQIECBAgAABAgQIEEgTePDBB9OqK9Z17949XHjhhRXrnCRAgAABAgQIECAgKcN7gAABAgQIECBAgAABAgQIECBAgAABAgQIJAJ5ti6ZPn166NWrFz8CBAgQIECAAAECFQUkZVRkcZIAAQIECBAgQIAAAQIECBAgQIAAAQIEGk0gT1KGrUsa7V1ivgQIECBAgACBbAKSMrJ5aU2AAAECBAgQIECAAAECBAgQIECAAAECXVTg4Ycfzjyziy66KHOMAAIECBAgQIAAgcYRkJTROPfaTAkQIECAAAECBAgQIECAAAECBAgQIECgisCiRYvC7t27q9RWP22ljOo2aggQIECAAAECBEKQlOFdQIAAAQIECBAgQIAAAQIECBAgQIAAAQINL5Bn65J+/fqFadOmNbwdAAIECBAgQIAAgeoCkjKq26ghQIAAAQIECBAgQIAAAQIECBAgQIAAgQYRyJOUccEFF4Tu3f2ZvUHeIqZJgAABAgQIEMgl4L8Wc7EJIkCAAAECBAgQIECAAAECBAgQIECAAIGuJPDwww9nns5FF12UOUYAAQIECBAgQIBAYwlIymis+222BAgQIECAAAECBAgQIECAAAECBAgQIFAmcOTIkTBr1qyys7UPL7744tqNtCBAgAABAgQIEGhogZ4NPXuTJ0CAQCcR+Ow/fT088vDczKONy2e+4oZrw4uvuypzrAACBAgQIECAAAECBAgQIECAQKMIPPLII+HJJ5/MPF0rZWQmE0CAAAECBAgQaDgBK2U03C03YQIEOpvA0aPHwuNzF+cadvxjwj2/uD9XrCACBAgQIECAAAECBAgQIEDb3eTeAABAAElEQVSAQKMI5Nm65OSTTw7jxo1rFCLzJECAAAECBAgQyCkgKSMnnDACBAi0lcCihcvDwYOHcl9u69YdYe3ajbnjBRIgQIAAAQIECBAgQIAAAQIEurrAQw89lHmKl156aeYYAQQIECBAgAABAo0nICmj8e65GRMg0MkEZs9a0OIRF9FHiwehAwIECBAgQIAAAQIECBAgQIBABxXIk5Rh65IOejMNiwABAgQIECDQwQQkZXSwG2I4BAgQKBd47NF55acyH8+ZvTBzjAACBAgQIECAAAECBAgQIECAQCMIbN26NaxcuTLzVC+++OLMMQIIECBAgAABAgQaT0BSRuPdczMmQKATCcRtRzZv3tbiES9ZvDLs23egxf3ogAABAgQIECBAgAABAgQIECDQ1QTuvffeXFOyfUkuNkEECBAgQIAAgYYTkJTRcLfchAkQ6EwCsx6bX8hwn3zyyTB3jtUyCsHUCQECBAgQIECAAAECBAgQINClBO68887M8xk5cmQYMmRI5jgBBAgQIECAAAECjScgKaPx7rkZEyDQiQRmPVo7KeNjH//rEL9qldmzFtRqop4AAQIECBAgQIAAAQIECBAg0FACx48fD3fddVfmOV9xxRWZYwQQIECAAAECBAg0poCkjMa872ZNgEAnENi9e29Ytmx16kif9/xnhdGjR5S+nnvVpalt58xZFOKKGQoBAgQIECBAgAABAgQIECBAgMAfBB566KGwdevWzBzXXntt5hgBBAgQIECAAAECjSkgKaMx77tZEyDQCQTiyhbx0xpp5bzzpzxVPe3cSU+9rvRi7559YfnyNZWqnCNAgAABAgQIECBAgAABAgQINKTAj3/848zz7tatW7jmmmsyxwkgQIAAAQIECBBoTAFJGY15382aAIFOIDDrsfStS3r37hXOmTrhqZnE1/GPAmmlVp9pseoIECBAgAABAgQIECBAgAABAl1N4M4778w8pcsuuyyccsopmeMEECBAgAABAgQINKaApIzGvO9mTYBABxc4evRoeHzu4tRRTp02McTEjKYyYMBJYey4kU2HFb/Pmb2w4nknCRAgQIAAAQIECBAgQIAAAQKNJrBhw4Ywe/bszNO2dUlmMgEECBAgQIAAgYYWkJTR0Lff5AkQ6KgCCxcsCwcPHkod3oyZU0+or7WFyepV68P27TtPiHOCAAECBAgQIECAAAECBAgQINBoAnm2LolGkjIa7Z1ivgQIECBAgACBlglIymiZn2gCBAi0isBjjy2o2e+MGeec0Gb69MknnCs/MXuW1TLKTRwTIECAAAECBAgQIECAAAECjSeQJyljyJAh4fzzz288LDMmQIAAAQIECBDILSApIzedQAIECLSewJxZ6UkZo0ePCIMGn7h36bjxo0Lfvn1SBzbrsfmp9SoJECBAgAABAgQIECBAgAABAl1d4PDhw+Gee+7JPM2XvvSlmWMEECBAgAABAgQINLZAz8aevtkTIECg4wls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/u3f/i1s2bIl88D69esX3v72t2eOE0CAAAECBAgQIECgXoHWfepX7yi0I0CAQIMIzJq1IHWm550/pdUTMuIAzjv/nNRxxMrZs9PHWrMDDQoT+PNXvDAMHnxqan9rayQppAb/sbJ//37h7LOHV226ZvWGE+pWray+3cnYcWeHvn37nBBT6cSUqRMqnS6dW15hFZC49URaqbXqR1psU93Agf2bXlb8Hh/S7969t2Kdk51LIG5v8qa/fGXNQa9dc+LPQM0gDQgQIECAAAECBAgQaHWBuOXgLbfckus6b3vb28KgQYNyxQoiQIAAAQIECBAgUI+ApIx6lLQhQIBAAQJbNm8LGzdsTu1pxsypqfVFVcYH1n37pT8snzMrfVWPosain9oCPXr0SFaKOD+14bZtO1Pr661MWwUgbpMSExGal5Ur1jU/fNrrqVMnPu047WBqWlLG8jUnhK5b98QJ55pODE+2paiVUNHUNu37kKGD06pLdbW2UanZgQYdRuDM4aeHMWNGpI6nqJ+z1IuoJECAAAECBAgQIEAgs8APfvCDsGrVqsxxPXv2DO9617syxwkgQIAAAQIECBAgkEVAUkYWLW0JECDQAoFaq2TErR6mnze5BVeoPzR+KjztIXjsafXq9WHHjl31d6plqwqcPmxIav8HDz49WSK1cUrlOVPTt2aI74vmZWXKShm1+mreT0wGqbZtz6qV68KxY8eeah63V1mfkpRRxNYl8WJnnDGktJ3LUxeu8GLevCUVzjrVWQXa6uess/oYNwECBAgQIECAAIGOKvDRj34019Be+9rXhuHDq68YmatTQQQIECBAgAABAgTKBCRllIE4JECAQGsJzJ2zKLXriZPGhpNO6pfapsjK6edNqdldrTHX7ECDwgSaJyVU6jQm2hRRJk0el5qIUJ6UsWpV5ZUyevXqFSZMHF33kOLWKaOrrFJw+PCRJAnjT9uVbNm8PRw6dLhq31OSlWCKKP369Q1x9YS08uD9jz0tYSStrbqOL1D756xnx5+EERIgQIAAAQIECBBoMIG77747zJs3L/Osu3XrFt7//vdnjhNAgAABAgQIECBAIKuApIysYtoTIEAgh0B8gDy/xifq22rrkqbhn3vupKaXVb/PnrWgap2KthVYt3Zj6gX79z8ptb7eypgccfao6p8SWrtmw1Nd7d9/IMQEiUolJmTExIws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BAgQKFLgPe95T9i/v76tLcuv++lPfzrEVSoVAgQIECBAgAABAu0pICmjPfVdmwABAgQIECBAgAABAgQIECBAgAABAgQqCixYsCD8+7//e8W6WicvueSScP3119dqpp4AAQIECBAgQIBAqwtIymh1YhcgQIAAAQIECBAgQIAAAQIECBAgQIAAgawCceuR48ePZw0rtf/CF76QK04QAQIECBAgQIAAgaIFJGUULao/AgQIECBAgAABAgQIECBAgAABAgQIEGiRwB133BHuu+++XH28+tWvDjNmzMgVK4gAAQIECBAgQIBA0QKSMooW1R8BAgQIECBAgAABAgQIECBAgAABAgQI5BY4duxY+Ou//utc8b169Qqf/OQnc8UKIkCAAAECBAgQINAaApIyWkNVnwQIECBAgAABAgQIECBAgAABAgQIECCQSyBuPbJs2bJcse985zvD8OHDc8UKIkCAAAECBAgQINAaApIyWkNVnwQIECBAgAABAgQIECBAgAABAgQIECCQWWD37t3hQx/6UOa4GDBo0KDwgQ98IFesIAIE/j97dx9b5ZXnCf5nKNrjbis0SN4V3cuWwpTCNpPtDiqBRoJZqRZpMDa26QFDmgExy4QaKqlUQl5IQhJCyBtJiiRFJanqgNh1maHNm8SbjRmJZlrCXRGoh6oexAikBpWsERqhwQ1yi6Zd2LsPs05TCQR8Hvva1/fz/HPte5/v75zzOZQKnJ+fQ4AAAQIECBAYKgFNGUMlqy4BAgQIECBAgAABAgQIECBAgAABAgQIDEjg+eefj66urgFl+m/euHFjPPDAA/3feiVAgAABAgQIECAwIgQ0ZYyIbTAJAgQIECBAgAABAgQIECBAgAABAgQIlLZAR0dHfPbZZ0kI3/rWt+J73/teUlaIAAECBAgQIECAwFAKaMoYSl21CRAgQIAAAQIECBAgQIAAAQIECBAgQOCeAn//938f//pf/+t73ne3GzZv3hxjx46928feJ0CAAAECBAgQIDBsApoyho3ewAQIECBAgAABAgQIECBAgAABAgQIECCQCbzwwgvxq1/9Kgnjn//zfx719fVJWSECBAgQIECAAAECQy2gKWOohdUnQIAAAQIECBAgQIAAAQIECBAgQIAAgbsKnDp1Kn784x/f9fN7ffDpp5/e6xafEyBAgAABAgQIEBg2AU0Zw0ZvYAIECBAgQIAAAQIECBAgQIAAAQIECJS2wD/8wz/En/zJn0RfX18SxPLly2P69OlJWSECBAgQIECAAAEChRDQlFEIZWMQIECAAAECBAgQIECAAAECBAgQIECAwFcEXnnllfibv/mbr7x/P2+Ul5fHpk2b7udW9xAgQIAAAQIECBAYNgFNGcNGb2ACBAgQIECAAAECBAgQIECAAAECBAiUrsAvf/nL2Lx5czLAhg0b4vd+7/eS84IECBAgQIAAAQIECiGgKaMQysYgQIAAAQIECBAgQIAAAQIECBAgQIAAgS8Efv3rX986tqS3t/eL9wbyxR/90R/F2rVrBxJxLwECBAgQIECAAIFhEdCUMSzsBiVAgAABAgQIECBAgAABAgQIECBAgEDpCmzcuDH+y3/5L0kA3/jGN+LP/uzPYswYP95OAhQiQIAAAQIECBAoqIC/tRaU22AECBAgQIAAAQIECBAgQIAAAQIECBAobYGsGeOdd95JRli/fn38wR/8QXJekAABAgQIECBAgEAhBTRlFFLbWAQIECBAgAABAgQIECBAgAABAgQIEChhgey4kj/5kz+J7PiSlCtrxnjppZdSojIECBAgQIAAAQIEhkVAU8awsBuUAAECBAgQIECAAAECBAgQIECAAAECpSewadOm+OUvf5m08P5jS7JXFwECBAgQIECAAIFiEdCUUSw7ZZ4ECBAgQIAAAQIECBAgQIAAAQIECBAoYoG/+Zu/iddffz15BdkTMv7oj/4oOS9IgAABAgQIECBAYDgENGUMh7oxCRAgQIAAAQIECBAgQIAAAQIECBAgUEICfX19t44t+Yd/+IekVWfHlqxfvz4pK0SAAAECBAgQIEBgOAU0ZQynvrEJECBAgAABAgQIECBAgAABAgQIECBQAgIffvhhnDp1KmmlY8aMiT/7sz8Lx5Yk8QkRIECAAAECBAgMs4CmjGHeAMMTIECAAAECBAgQIECAAAECBAgQIEBgNAv89V//daxbty55ic8//7xjS5L1BAkQIECAAAECBIZbQFPGcO+A8QkQIECAAAECBAgQIECAAAECBAgQIDBKBbq7u6O+vj5u3LiRtMJ/+k//aWzcuDEpK0SAAAECBAgQIEBgJAhoyhgJu2AOBAgQIECAAAECBAgQIECAAAECBAgQmfVksgAAQABJREFUGIUCS5cujV/96ldJKysrK7t1bMlv/dZvJeWFCBAgQIAAAQIECIwEAU0ZI2EXzIEAAQIECBAgQIAAAQIECBAgQIAAAQKjTOCTTz6JQ4cOJa9qzZo1MWPGjOS8IAECBAgQIECAAIGRIKApYyTsgjkQIECAAAECBAgQIECAAAECBAgQIEBgFAn89V//dWRNFanXN7/5zXjrrbdS43IECBAgQIAAAQIERoyApowRsxUmQoAAAQIECBAgQIAAAQIECBAgQIAAgeIX6O7ujvr6+ujp6UlaTHZsyb//9/8+/sk/+SdJeSECBAgQIECAAAECI0lAU8ZI2g1zIUCAAAECBAgQIECAAAECBAgQIECAQJELLF26NH71q18lr+L73/9+zJo1KzkvSIAAAQIECBAgQGAkCWjKGEm7YS4ECBAgQIAAAQIECBAgQIAAAQIECBAoYoEPP/wwDh06lLyC7NiS9957LzkvSIAAAQIECBAgQGCkCWjKGGk7Yj4ECBAgQIAAAQIECBAgQIAAAQIECBAoQoG/+qu/irVr1ybP/Bvf+Ebs27fPsSXJgoIECBAgQIAAAQIjUUBTxkjcFXMiQIAAAQIECBAgQIAAAQIECBAgQIBAEQn87d/+bTQ0NMSvf/3r5FlnT8j49re/nZwXJECAAAECBAgQIDASBTRljMRdMScCBAgQIECAAAECBAgQIECAAAECBAgUkcCSJUviv/7X/5o847q6ulizZk1yXpAAAQIECBAgQIDASBXQlDFSd8a8CBAgQIAAAQIECBAgQIAAAQIECBAgUAQC7777bvyH//Afkmf6zW9+M3bu3JmcFyRAgAABAgQIECAwkgU0ZYzk3TE3AgQIECBAgAABAgQIECBAgAABAgQIjGCBn//857Fu3brkGY4bNy4OHjwYlZWVyTUECRAgQIAAAQIECIxkgW+M5MmZGwECBIpNYOLEicU2ZfMlQOD/F7h48WKcOHEi2eO3fuu3Yv78+fE7v/M7yTUEi1ugrKysuBdg9gQIECBAgAABAgQGKPDf//t/jz/+4z+O3t7eASb/8fYtW7bEH/7hH/7jG74iQIAAAQIECBAgMMoENGWMsg21HAIEhlfgwQcfHN4JGJ0AgWSB7H+/LS0tsXXr1uQaWT77LbGKiorkGoIECBAgQIAAAQIECBAoBoG+vr5YtGhR/Lf/9t+Sp5vlV69enZwXJECAAAECBAgQIFAMAo4vKYZdMkcCBAgQIECgIAKffPJJfPvb304e65e//GU8+uijkf1w0kWAAAECBAgQIECAAIHRLPDmm2/Gf/yP/zF5id/61reiqakpOS9IgAABAgQIECBAoFgENGUUy06ZJwECBAgQIDDkAtlZxgcOHIjf/d3fTR4rOwv5lVdeSc4LEiBAgAABAgQIECBAYKQLHDt2LNavX588zezpgtm/vX77t387uYYgAQIECBAgQIAAgWIR0JRRLDtlngQIECBAgEBBBH7/938/du3aFWVlZcnjvf3227Fnz57kvCABAgQIECBAgAABAgRGqsCFCxdi4cKFuab3s5/9LKZNm5arhjABAgQIECBAgACBYhHQlFEsO2WeBAgQIECAQMEE/uW//JexcePGXOMtX748Tp06lauGMAECBAgQIECAAAECBEaSQHd3d9TU1MTVq1eTp/XYY4/FokWLkvOCBAgQIECAAAECBIpNQFNGse2Y+RIgQIAAAQIFEXj55Zcja85IvW7cuBG1tbXR2dmZWkKOAAECBAgQIECAAAECI0agr68vFi9eHOfOnUue0x/8wR/Exx9/nJwXJECAAAECBAgQIFCMApoyinHXzJkAAQIECBAYcoHs+JLsGJNvfvObyWNdvnw5qqur4/r168k1BAkQIECAAAECBAgQIDASBLLG9SNHjiRPpbKyMg4ePBjl5eXJNQQJECBAgAABAgQIFKOApoxi3DVzJkCAAAECBAoi8Lu/+7u5f2h49uzZePTRRyP7rTIXAQIECBAgQIAAAQIEilEga1h/5513ck19586d8a1vfStXDWECBAgQIECAAAECxSigKaMYd82cCRAgQIAAgYIJ/OEf/mFs27Yt13jZb4Nlv1XmIkCAAAECBAgQIECAQLEJnD59OlasWJFr2k8++WTU1dXlqiFMgAABAgQIECBAoFgFNGUU686ZNwECBAgQIFAwgWXLlsVzzz2Xa7zst8p27NiRq4YwAQIECBAgQIAAAQIECimQHclYW1sbN27cSB7229/+dmzevDk5L0iAAAECBAgQIECg2AU0ZRT7Dpo/AQIECBAgUBCBd999N+bOnZtrrJUrV8apU6dy1RAmQIAAAQIECBAgQIBAoQTq6+vj0qVLycNNmDAh9u3bF+PGjUuuIUiAAAECBAgQIECg2AU0ZRT7Dpo/AQIECBAgUBCBMWPGxN69e2PatGnJ4/X09Nz6LbPOzs7kGoIECBAgQIAAAQIECBAohMC/+Tf/Jj7//PPkocaOHRvZUY7f/OY3k2sIEiBAgAABAgQIEBgNApoyRsMuWgMBAgQIECBQEIHKyspob2+P7Le9Uq/s8b/V1dXR3d2dWkKOAAECBAgQIECAAAECQyrw0UcfRVNTU64xfvSjH8Xs2bNz1RAmQIAAAQIECBAgMBoENGWMhl20BgIECBAgQKBgApMnT45Dhw7levzu2bNnY9GiRdHX11eweRuIAAECBAgQIECAAAEC9yNw7NixePbZZ+/n1rves2LFinjiiSfu+rkPCBAgQIAAAQIECJSSgKaMUtptayVAgAABAgQGRWDWrFmxffv2XLWOHj0aL7zwQq4awgQIECBAgAABAgQIEBhMgQsXLsTChQujt7c3uWz2dIytW7cm5wUJECBAgAABAgQIjDYBTRmjbUethwABAgQIECiIwLJly+L555/PNdb7778fO3bsyFVDmAABAgQIECBAgAABAoMhkB2xWFNTE1evXk0ulz1Z8MCBA7meLJg8uCABAgQIECBAgACBESqgKWOEboxpESBAgAABAiNfYNOmTTF37txcE125cmV0dHTkqiFMgAABAgQIECBAgACBPALZkzEWL14c586dSy5TUVERbW1tMXHixOQaggQIECBAgAABAgRGo4CmjNG4q9ZEgAABAgQIFERgzJgxsXfv3pg2bVryeD09PVFXVxednZ3JNQQJECBAgAABAgQIECCQR+Cll16KI0eO5CkRO3fujIcffjhXDWECBAgQIECAAAECo1FAU8Zo3FVrIkCAAAECBAomUFlZGe3t7VFVVZU8ZldXV1RXV0f2uGAXAQIECBAgQIAAAQIECimwa9eueO+993IN+eqrr8aCBQty1RAmQIAAAQIECBAgMFoFNGWM1p21LgIECBAgQKBgAtm5ya2trbnOTT579mwsWrQosscGuwgQIECAAAECBAgQIFAIgdOnT8eKFStyDZU1Y7z++uu5aggTIECAAAECBAgQGM0CmjJG8+5aGwECBAgQIFAwgRkzZsT27dtzjXf06NFYu3ZtrhrCBAgQIECAAAECBAgQuB+By5cvR21tbdy4ceN+br/jPdlxJdmxJWVlZXf83JsECBAgQIAAAQIECERoyvCngAABAgQIECAwSALLli3L3VSxefPm2LFjxyDN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/4gdPXo01q5dm6uGMAECBAgQIECAwPAL7N+/P954441cE3nhhRdiyZIluWoIEyBAgAABAgQIECBAYCACmjIGouVeAgQIECBAgACBggs888wzsWzZslzjbt68OXbs2JGrhjABAgQIECBAgMDwCWTHlSxdujTXBObNmxdvv/12rhrCBAgQIECAAAECBAgQGKiApoyBirmfAAECBAgQIECg4ALbt2+PWbNm5Rp35cqV0dHRkauGMAECBAgQIECAQOEFrly5EjU1NXH9+vXkwadOnRq7d+/OdTRe8uCCBAgQIECAAAECBAiUtICmjJLefosnQIAAAQIECBSHwLhx4+LQoUPx4IMPJk+4p6cn6urq4uLFi8k1BAkQIECAAAECBAorkP0drqGhITo7O5MHHj9+fLS1tUVlZWVyDUECBAgQIECAAAECBAikCmjKSJWTI0CAAAECBAgQKKjAhAkTor29PR544IHkcbu6uqK6ujq6u7uTawgSIECAAAECBAgUTmDVqlVx4sSJ5AHHjBkT+/btiylTpiTXECRAgAABAgQIECBAgEAeAU0ZefRkCRAgQIAAAQIECirw0EMPxf79+3M9dvr8+fOxaNGi6O3tLejcDUaAAAECBAgQIDAwgU8//TSampoGFvrS3T/84Q9jzpw5X3rXtwQIECBAgAABAgQIECicgKaMwlkbiQABAgQIECBAYBAEvvOd78QHH3yQq9LRo0fjueeey1VDmAABAgQIECBAYOgEsqdj/OAHP8g1wIoVK2LNmjW5aggTIECAAAECBAgQIEAgr4CmjLyC8gQIECBAgAABAgUXeOqpp+Kxxx7LNe6HH34YO3bsyFVDmAABAgQIECBAYPAFOjs7o6GhIW7evJlcfObMmbF169bkvCABAgQIECBAgAABAgQGS0BTxmBJqkOAAAECBAgQIFBQgZ/+9Kcxa9asXGOuXLkyOjo6ctUQJkCAAAECBAgQGDyB69evR01NTVy5ciW56KRJk+Lw4cMxbty45BqCBAgQIECAAAECBAgQGCwBTRmDJakOAQIECBAgQIBAQQXGjh0bhw4digcffDB53J6enqirq4uLFy8m1xAkQIAAAQIECBAYHIG+vr5YunRpnDlzJrlgeXl5tLa2RlVVVXINQQIECBAgQIAAAQIECAymgKaMwdRUiwABAgQIECBAoKACEyZMiPb29njggQeSx+3q6orq6uro7u5OriFIgAABAgQIECCQX+D111+P/fv35yrU1NQU06dPz1VDmAABAgQIECBAgAABAoMpoCljMDXVIkCAAAECBAgQKLjAQw89dOuH99mTM1Kv8+fPx6JFi6K3tze1hBwBAgQIECBAgEAOgawZI2vKyHO9+OKLsWTJkjwlZAkQIECAAAECBAgQIDDoApoyBp1UQQIECBAgQIAAgUILfOc734kf/ehHuYY9evRoPPPMM7lqCBMgQIAAAQIECAxcIDuuJDu2JM81b968ePvtt/OUkCVAgAABAgQIECBAgMCQCGjKGBJWRQkQIECAAAECBAot8MQTT8Rjjz2Wa9issWPbtm25aggTIECAAAECBAjcv8CVK1eipqYmrl+/fv+hL905derU2L17d5SVlX3pE98SIECAAAECBAgQIEBg+AU0ZQz/HpgBAQIECBAgQIDAIAn89Kc/jeypGXmu1atXR0dHR54SsgQIECBAgAABAvchcPPmzWhoaIjOzs77uPvOt4wfPz7a2tqisrLyzjd4lwABAgQIECBAgAABAsMsoCljmDfA8AQIECBAgAABAoMnMHbs2MjOI3/ooYeSi2b/caCuri4uXryYXEOQAAECBAgQIEDg3gKPP/54nDhx4t433uWOMWPGxL59+2LKlCl3ucPbBAgQIECAAAECBAgQGH4BTRnDvwdmQIAAAQIECBAgMIgCDzzwQLS3t0f2mnp1dXVFdXV1XLt2LbWEHAECBAgQIECAwNcIZE84++yzz77mjnt/tHnz5pgzZ869b3QHAQIECBAgQIAAAQIEhlFAU8Yw4huaAAECBAgQIEBgaAQefPDBW0/MyJ6ckXqdP38+FixYEL29vakl5AgQIECAAAECBO4gkD0d4/vf//4dPrn/t1asWBFPP/30/QfcSYAAAQIECBAgQIAAgWES0JQxTPCGJUCAAAECBAgQGFqB73znO5H9Bmae6/jx47FmzZo8JWQJECBAgAABAgRuE+js7IyGhobIjoxLvWbOnBlbt25NjcsRIECAAAECBAgQIECgoAKaMgrKbTACBAgQIECAAIFCCjz22GPx5JNP5hpyy5YtsW3btlw1hAkQIECAAAECBCKuX78eNTU1ceXKlWSOSZMmxeHDh2PcuHHJNQQJECBAgAABAgQIECBQSAFNGYXUNhYBAgQIECBAgEDBBT766KPInpqR51q9enV0dHTkKSFLgAABAgQIECh5gaVLl8aZM2eSHcrLy6O1tTWqqqqSawgSIECAAAECBAgQIECg0AKaMgotbjwCBAgQIECAAIGCCowZMyb2798fDz30UPK42eO16+rq4uLFi8k1BAkQIECAAAECpSzw+uuv3/o7WR6DpqammD59ep4SsgQIECBAgAABAgQIECi4gKaMgpMbkAABAgQIECBAoNACDzzwQLS3t8eECROSh+7q6orq6uq4du1acg1BAgQIECBAgEApCmQNshs2bMi19JdeeimWLFmSq4YwAQIECBAgQIAAAQIEhkNAU8ZwqBuTAAECBAgQIECg4AIPPvhgHDp0KNf54+fPn48FCxZE9uQMFwECBAgQIECAwL0FsuNKsmNL8lzz5s2Lt956K08JWQIECBAgQIAAAQIECAybgKaMYaM3MAECBAgQIECAQKEFZs2aFdu3b8817PHjx+Ppp5/OVUOYAAECBAgQIFAKAleuXImampq4fv168nKnTp0au3fvjrKysuQaggQIECBAgAABAgQIEBhOAU0Zw6lvbAIECBAgQIAAgYILLFu2LNasWZNr3I8//ji2bduWq4YwAQIECBAgQGA0C2RPFmtoaIjOzs7kZY4fPz7a2tqisrIyuYYgAQIECBAgQIAAAQIEhltAU8Zw74DxCRAgQIAAAQIECi7wwx/+MObOnZtr3NWrV0f21AwXAQIECBAgQIDAVwWefPLJOHHixFc/uM93xo4dG/v27YspU6bcZ8JtBAgQIECAAAECBAgQGJkCmjJG5r6YFQECBAgQIECAwBAKjBkzJvbu3RsPPfRQ8ijZb38uWLAgLl68mFxDkAABAgQIECAwGgU+++yz+MlPfpJraZs3b445c+bkqiFMgAABAgQIECBAgACBkSCgKWMk7II5ECBAgAABAgQIFFwgewx2e3t7TJgwIXnsa9euRXV1dWSvLgIECBAgQIAAgbj1dIzHH388F8WKFSviqaeeylVDmAABAgQIECBAgAABAiNFQFPGSNkJ8yBAgAABAgQIECi4wIMPPhiHDh2KcePGJY99/vz5W0/MyJ6c4SJAgAABAgQIlLJAZ2dnNDQ0RJ6/F82cOTO2bt1ayozWToAAAQIECBAgQIDAKBPQlDHKNtRyCBAgQIAAAQIEBiYwa9as2L59+8BCX7r7+PHj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2FZAKGNbDV8TIECAAAECBAgQIJBJYM8994x//ud/jj322CNTnUYsTv419Msvv9yIrSq1R96BjIMPPrBSPg5TTgHBjHLOLa+uP/nJT8aiRYtSl+vdu/eWO2QMHz48dQ0LCRAgQIAAAQIECBAgQKC6AkIZ1Z2tkxEgQIAAAQIECBBoisBb3/rWOOigg5qyd62bultGbWICGbV5ubpcAoIZ5ZpXXt3OmjUrrrzyykzlkvWjR4/OVMNiAgQIECBAgAABAgQIEKiugFBGdWfrZAQIECBAgAABAgSaInDAAQfET3/606bsXeumQhk9FxPI6LmVK8srIJhR3tml6XzZsmUxZcqUNEvfWDNp0qQ499xz3/jeFwQIECBAgAABAgQIECBAYHsBoYztRXxPgAABAgQIECBAgEBqgd133z2ef/751OsbvVAoo2fiAhk9c3JVNQQEM6oxx+5OsXr16hg7dmysX7++u0s7fX3EiBExY8aMTl/3AgECBAgQIECAAAECBAgQSASEMrwPCBAgQIAAAQIECBDIRaB3797x2muv5VKrUUWEMrqXFsjo3sgV1RMQzKjeTLc90YYNG2LcuHGxatWqbZ+u6etBgwbFggULol+/fjWtczEBAgQIECBAgAABAgQItJ6AUEbrzdyJCRAgQIAAAQIECOQqcOqpp0avXr2ivb0917qNKLZy5cpGbFPaPQQySjs6jecgIJiRA2JBS5x22mmxdOnS1N0lQYz58+fH4MGDU9ewkAABAgQIECBAgAABAgRaR0Aoo3Vm7aQECBAgQIAAAQIEchcYNWpU3H333bnXbVTBFStWRFtbW6O2K9U+AhmlGpdm6yQgmFEn2CaWvf766zN/5EjykSUjR45s4ilsTYAAAQIECBAgQIAAAQJlEhDKKNO09EqAAAECBAgQIECgQAJDhw6NJUuWFKij2lt59dVX49lnn619YcVXCGRUfMCOV5OAYEZNXIW+ePHixXHOOedk6vGss86KSZMmZaphMQECBAgQIECAAAECBAi0loBQRmvN22kJECBAgAABAgQI5CIwYMCAqMpHf1TlHLkMdnMRgYy8JNWpkoBgRvmnmdwZacKECbFp06bUhxk9enRcc801qddbSIAAAQIECBAgQIAAAQKtKSCU0Zpzd2oCBAgQIECAAAECqQV69+4dr7zySur1RVv45JNPFq2lpvUjkNE0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ECDQEAGhjIYw24QAAQIECBAgQIAAgXoLJP9S+l3veld85StfiRdffDHmz58fkydPjj322KPmrRsZyhDIqHk8FhBoeYGyBzPOOeecWLx4ceo5JoG82bNnx/Dhw1PXsJAAAQIECBAgQIAAAQIECDRKQCijUdL2IUCAAAECBAgQIECgYQJ9+/aNsWPHxh133BGrV6+Ob3zjGzFp0qTYbbfdetRDo0IZAhk9GoeLCBDoQKCswYwZM2bE9ddf38GJev7U9OnT44QTTuj5AlcSIECAAAECBAgQIECAAIEmCghlNBHf1gQIECBAgAABAgQI1F+gX79+MX78+Jg5c+aWgMasWbPife97X+yyyy6dbr5y5cpOX8vrBYGMvCTVIdC6AmULZixdujROO+20TANLAnbnn39+phoWEyBAgAABAgQIECBAgACBRgoIZTRS214ECBAgQIAAAQIECDRVILlTxsSJE2Pu3LlbAhq33357HH/88bHTTju9qa81a9bECy+88Kbn8vxGICNPTbUItLZAWYIZq1atinHjxsWGDRtSD2zEiBGR3GnDgwABAgQIECBAgAABAgQIlElAKKNM09IrAQIECBAgQIAAAQK5CfTv3z/+9m//Nh544IF48cUX40tf+lIcd9xx0bv37/6YVK+PMBHIyG2EChEg8HuBogcz1q9fv+UjpZKPk0r7GDRoUCxYsCCSux95ECBAgAABAgQIECBAgACBMgkIZZRpWnolQIAAAQIECBAgQKAuAgMHDozTTz89vv3tb0fyr7lvvPHGuuwjkFEXVkUJENgsUORgxpQpU2LZsmWp59S3b9+YP39+DB48OHUNCwkQIECAAAECBAgQIECAQLMEhDKaJW9fAgQIECBAgAABAgQKKfCWt7wlzjjjjDj22GNz7U8gI1dOxQgQ6ECgiMGMq666KmbNmtVBtz1/6rbbbouRI0f2fIErCRAgQIAAAQIECBAgQIBAgQSEMgo0DK0QIECAAAECBAgQIFBNAYGMas7VqQgUUaBIwYxFixbFhRdemIlp6tSpkdxpw4MAAQIECBAgQIAAAQIECJRVQCijrJPTNwECBAgQIECAAAECpRAQyCjFmDRJoFICRQhmLF++PCZOnBjt7e2pbUePHh2f+9znUq+3kAABAgQIECBAgAABAgQIFEFAKKMIU9ADAQIECBAgQIAAAQKVFBDIqORYHYpAKQSaGcxYs2ZNjBkzJtauXZvaatiwYTF37tzo3dtfXaVGtJAAAQIECBAgQIAAAQIECiHgT7aFGIMmCBAgQIAAAQIECBComoBARtUm6jwEyifQjGDGpk2bYsKECbFixYrUYP3794+FCxfGgAEDUtewkAABAgQIECBAgAABAgQIFEVAKKMok9AHAQIECBAgQIAAAQKVERDIqMwoHYRA6QUaHcw4++yzY/HixandevXqFbNnz47hw4enrmEhAQIECBAgQIAAAQIECBAokoBQRpGmoRcCBAgQIECAAAECBEovIJBR+hE6AIHKCTQqmDFjxoy44YYbMvldccUVccIJJ2SqYTEBAgQIECBAgAABAgQIECiSgFBGkaahFwIECBAgQIAAAQIESi0gkFHq8WmeQKUF6h3MWLp0aZx22mmZDCdNmhQXXHBBphoWEyBAgAABAgQIECBAgACBogkIZRRtIvohQIAAAQIECBAgQKCUAgIZpRybpgm0lEC9ghmrVq2KcePGxYYNG1J7jhgxIpI7bXgQIECAAAECBAgQIECAAIGqCQhlVG2izkOAAAECBAgQIECAQMMFBDIaTm5DAgRSCuQdzFi48KEYO3ZsrF69OmVHEYMGDYoFCxZEv379UtewkAABAgQIECBAgAABAgQIFFVAKKOok9EXAQIECBAgQIAAAQKlEBDIKMWYNEmAwDYCeQYzzjrrknjyyWe3qV7bl3379o358+fH4MGDa1voagIECBAgQIAAAQIECBAgUBIBoYySDEqbBAgQIECAAAECBAgUT0Ago3gz0REBAj0TyCuY0d7eHkOGHBq77rpPzzbe7qrbbrstRo4cud2zviVAgAABAgQIECBAgAABAtUREMqoziydhAABAgQIECBAgACBBgoIZDQQ21YECNRFIK9gRtJcmmDGJz7xiZgyZUpdzqYoAQIECBAgQIAAAQIECBAoioBQRlEmoQ8CBAgQIECAAAECBEojIJBRmlFplACBbgSaFcwYPXp0XHfddd1052UCBAgQIECAAAECBAgQIFB+AaGM8s/QCQgQIECAAAECBAgQaKCAQEYDsW1FgEBDBBodzBg2bFjMnTs3evf211INGbBNCBAgQIAAAQIECBAgQKCpAv7021R+mxMgQIAAAQIECBAgUCYBgYwyTUuvBAjUItCoYMbuu+8e8+fPjwEDBtTSnmsJECBAgAABAgQIECBAgEBpBYQySjs6jRMgQIAAAQIECBAg0EgBgYxGatuLAIFmCNQ7mNGrV6+YM2dOHHrooc04nj0JECBAgAABAgQIECBAgEBTBIQymsJuUwIECBAgQIAAAQIEyiQgkFGmaemVAIEsAvUMZnz2s5+NE044IUt71hIgQIAAAQIECBAgQIAAgdIJCGWUbmQaJkCAAAECBAgQIECgkQICGY3UthcBAkUQqEcwY9KkSXHhhRcW4Xh6IECAAAECBAgQIECAAAECDRUQymgot80IECBAgAABAgQIECiTgEBGmaalVwIE8hTIM5hx0EGHxQc+8JE821OLAAECBAgQIECAAAECBAiURkAoozSj0igBAgQIECBAgAABAo0UEMhopLa9CBAookBewYz29vY499xp8eCDS4p4TD0RIECAAAECBAgQIECAAIG6Cghl1JVXcQIECBAgQIAAAQIEyiggkFHGqemZAIF6COQVzNi4cWNMnXqRYEY9hqQmAQIECBAgQIAAAQIECBRaQCij0OPRHAECBAgQIECAAAECjRYQyGi0uP0IECi6gGBG0SekPwIECBAgQIAAAQIECBAosoBQRpGnozcCBAgQIECAAAECBBoqIJDRUG6bESBQIgHBjBINS6sECBAgQIAAAQIECBAgUCgBoYxCjUMzBAgQIECAAAECBAg0SyCvQMbAAXvGnTNvjoMPPrBZR7EvAQIE6iIgmFEXVkUJECBAgAABAgQIECBAoOICQhkVH7DjESBAgAABAgQIECDQvUCegYy7v/YFgYzuyV1BgEBJBfIOZnxrwXdLKqFtAgQIECBAgAABAgQIECDQMwGhjJ45uYoAAQIECBAgQIAAgYoK5B3IGHbIwRWVciwCBAj8TiDXYMa500IwwzuLAAECBAgQIECAAAECBKosIJRR5ek6GwECBAgQIECAAAECXQoIZHTJ40UCBAh0KpBXMKOtrS2mCmZ06uwFAgQIECBAgAABAgQIECi/gFBG+WfoBAQIECBAgAABAgQIpBAQyEiBZgkBAgS2ERDM2AbDlwQIECBAgAABAgQIECBAoBMBoYxOYDxNgAABAgQIECBAgEB1BQQyqjtbJyNAoLECghmN9bYbAQIECBAgQIAAAQIECJRPQCijfDPTMQECBAgQIECAAAECGQQEMjLgWUqAAIEOBAQzOkDxFAECBAgQ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MwmIMmYSsk6AAAECBAgQIECAQC4gzLARCBAgQKCtAoKMeLKCjNjGCgECBAgQIECAAAECBGYjIMqYjZLXECBAgAABAgQIECCQCwgzbAQCBAgQaJuAICOeqCAjtrFCgAABAgQIECBAgACB2QqIMmYr5XUECBAgQIAAAQIECOQCwgwbgQABAgTaIiDIiCcpyIhtrBAgQIAAAQIECBAgQGAuAqKMuWh5LQECBAgQIECAAAECuYAww0YgQIAAgaYLCDLiCe63335p4cKFKfvvez8ECBAgQIAAAQIECBAgUE5AlFHOz7sJECBAgAABAgQIdFZAmNHZ0btxAgQINF5AkBGPMAsyFixYIMiIiawQIECAAAECBAgQIEBgTgKijDlxeTEBAgQIECBAgAABArsLCDN21/CYAAECBJogIMiIpyTIiG2sECBAgAABAgQIECBAoF8BUUa/ct5HgAABAgQIECBAgEAuIMywEQgQIECgKQKCjHhSgozYxgoBAgQIECBAgAABAgTKCIgyyuh5LwECBAgQIECAAAECuYAww0YgQIAAgboLCDLiCQkyYhsrBAgQIECAAAECBAgQKCsgyigr6P0ECBAgQIAAAQIECOQCwgwbgQABAgTqKiDIiCcjyIhtrBAgQIAAAQIECBAgQGAQAqKMQSg6BgECBAgQIECAAAECuYAww0YgQIAAgboJCDLiiQgyYhsrBAgQIECAAAECBAgQGJSAKGNQko5DgAABAgQIECBAgEAuIMywEQgQIECgLgKCjHgSgozYxgoBAgQIECBAgAABAgQGKSDKGKSmYxEgQIAAAQIECBAgkAsIM2wEAgQIEBi1gCAjnoAgI7axQoAAAQIECBAgQIAAgUELiDIGLep4BAgQIECAAAECBAjkAsIMG4EAAQIERiUgyIjlBRmxjRUCBAgQIECAAAECBAhUISDKqELVMQkQIECAAAECBAgQyAWEGTYCAQIECAxbQJARiwsyYhsrBAgQIECAAAECBAgQqEpAlFGVrOMSIECAAAECBAgQIJALCDNsBAIECBAYloAgI5YWZMQ2VggQIECAAAECBAgQIFClgCijSl3HJkCAAAECBAgQIEAgFxBm2AgECBAgULWAICMWFmTENlYIECBAgAABAgQIECBQtYAoo2phxydAgAABAgQIECBAIBcQZtgIBAgQIFCVgCAjlhVkxDZWCBAgQIAAAQIECBAgMAwBUcYwlJ2DAAECBAgQIECAAIFcQJhhIxAgQIDAoAUEGbGoICO2sUKAAAECBAgQIECAAIFhCYgyhiXtPAQIECBAgAABAgQI5ALCDBuBAAECBAYlsGbNujSxdHnasGFTqUMuXDiWrrjibWnJkqNLHadObxZk1GkaroUAAQIECBAgQIAAgS4LiDK6PH33ToAAAQIECBAgQGBEAsKMEcE7LQECBFokkAUZS48/XZAxxUwFGVOgeIoAAQIECBAgQIAAAQIjEhBljAjeaQkQIECAAAECBAh0XUCY0fUd4P4JECDQv0AvyNi4eUv/B9n5Tp+QUYrPmwkQIECAAAECBAgQIEBgFgKijFkgeQkBAgQIECBAgAABAtUICDOqcXVUAgQItFlAkBFP1ydkxDZWCBAgQIAAAQIECBAgMCoBUcao5J2XAAECBAgQIECAAIFcQJhhIxAgQIDAbAUEGbGUICO2sUKAAAECBAgQIECAAIFRCogyRqnv3AQIECBAgAABAgQI5ALCDBuBAAECBGYSEGTEQoKM2MYKAQIECBAgQIAAAQIERi0gyhj1BJyfAAECBAgQIECAAIFcQJhhIxAgQIBAJCDIiGRSEmTENlYIECBAgAABAgQIECBQBwFRRh2m4BoIECBAgAABAgQIEMgFhBk2AgECBAhMFhBkTBbZ9bcgY5eFRwQIECBAgAABAgQIEKirgCijrpNxXQQIECBAgAABAgQ6KiDM6Ojg3TYBAgSmEBBkTIHy46cEGbGNFQIECBAgQIAAAQIECNRJQJRRp2m4FgIECBAgQIAAAQIEcgFhho1AgAABAoKMeA8IMmIbKwQIECBAgAABAgQIEKibgCijbhNxPQQIECBAgAABAgQI5ALCDBuBAAEC3RUQZMSzF2TENlYIECBAgAABAgQIECBQRwFRRh2n4poIECBAgAABAgQIEMgFhBk2AgECBLonIMiIZy7IiG2sECBAgAABAgQIECBAoK4Cooy6TsZ1ESBAgAABAgQIECCQCwgzbAQCBAh0R0CQEc9akBHbWCFAgAABAgQIECBAgECdBUQZdZ6OayNAgAABAgQIECBAIBcQZtgIBAgQaL+AICOesSAjtrFCgAABAgQIECBAgACBuguIMuo+IddHgAABAgQIECBAgEAuIMywEQgQINBeAUFGPFtBRmxjhQABAgQIECBAgAABAk0QEGU0YUqukQABAgQIECBAgACBXECYYSMQIECgfQKCjHimgozYxgoBAgQIECBAgAABAgSaIiDKaMqkXCcBAgQIECBAgAABArmAMMNGIECAQHsEBBnxLAUZsY0VAgQIECBAgAABAgQINElAlNGkablWAgQIECBAgAABAgRyAWGGjUCAAIHmCwgy4hkKMmIbKwQIECBAgAABAgQIEGiagCijaRNzvQQIECBAgAABAgQI5ALCDBuBAAECzRUQZMSzE2TENlYIECBAgAABAgQIECDQRAFRRhOn5poJECBAgAABAgQIEMgFhBk2AgECBJonIMiIZybIiG2sECBAgAABAgQIECBAoKkCooymTs51EyBAgAABAgQIECCQCwgzbAQCBAg0R0CQEc9KkBHbWCFAgAABAgQIECBAgECTBUQZTZ6eaydAgAABAgQIECBAIBcQZtgIBAgQqL+AICOekSAjtrFCgAABAgQIECBAgACBpguIMpo+QddPgAABAgQIECBAgEAuIMywEQgQIFBfAUFGPBtBRmxjhQABAgQIECBAgAABAm0QEGW0YYrugQABAgQIECBAgACBXECYYSMQIECgfgKCjHgmgozYxgoBAgQIECBAgAABAgTaIiDKaMsk3QcBAgQIECBAgAABArmAMMNGIECAQH0EBBnxLAQZsY0VAgQIECBAgAABAgQItElAlNGmaboXAgQIECBAgAABAgRyAWGGjUCAAIHRCwgy4hkIMmIbKwQIECBAgAABAgQIEGibgCijbRN1PwQIECBAgAABAgQI5ALCDBuBAAECoxMQZMT2gozYxgoBAgQIECBAgAABAgTaKCDKaONU3RMBAgQIECBAgAABArmAMMNGIECAwPAFBBmxuSAjtrFCgAABAgQIECBAgACBtgqIMto6WfdFgAABAgQIECBAgEAuIMywEQgQIDA8AUFGbC3IiG2sECBAgAABAgQIECBAoM0Coow2T9e9ESBAgAABAgQIECCQCwgzbAQCBAhULyDIiI0FGbGNFQIECBAgQIAAAQIECLRdQJTR9gm7PwIECBAgQIAAAQIEcgFhho1AgACB6gQEGbGtICO2sUKAAAECBAgQIECAAIEuCIgyujBl90iAAAECBAgQIECAQC4gzLARCBAgMHgBQUZsKsiIbawQIECAAAECBAgQIECgKwKijK5M2n0SIECA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AAECBAgQIECAAAECBJojIMpozqxcKQECBAgQIECAAAECBHIBYYaNQKAdAoKMeI6CjNjGCgECBAgQIECAAAECBAg0S0CU0ax5uVoCBAgQIECAAAECBAjkAsIMG4FAswUEGfH8BBmxjRUCBAgQIECAAAECBAgQaJ6AKKN5M3PFBAgQIECAAAECBAgQyAWEGTYCgWYKCDLiuQkyYhsrBAgQIECAAAECBAgQINBMAVFGM+fmqgkQIECAAAECBAgQIJALCDNsBALNEhBkxPMSZMQ2VggQIECAAAECBAgQIECguQKijObOzpUTIECAAAECBAgQIEAgFxBm2AgEmiEgyIjnJMiIbawQIECAAAECBAgQIECAQLMFRBnNnp+rJ0CAAAECBAgQIECAQC4gzLARCNRbQJARz0eQEdtYIUCAAAECBAgQIECAAIHmC4gymj9Dd0CAAAECBAgQIECAAIFcQJhhIxCop4AgI56LICO2sUKAAAECBAgQIECAAAEC7RAQZbRjju6CAAECBAgQIECAAAECuYAww0YgUC8BQUY8D0FGbGOFAAECBAgQIECAAAECBNojIMpozyzdCQECBAgQIECAAAECBHIBYYaNQKAeAoKMeA6CjNjGCgECBAgQIECAAAECBAi0S0CU0a55uhsCBAgQIECAAAECBAjkAsIMG4HAaAUEGbG/ICO2sUKAAAECBAgQIECAAAEC7RMQZbRvpu6IAAECBAgQIECAAAECuYAww0YgMBoBQUbsLsiIbawQIECAAAECBAgQIECAQDsFRBntnKu7IkCAAAECBAgQIECAQC4gzLARCAxXQJARewsyYhsrBAgQIECAAAECBAgQINBeAVFGe2frzggQIECAAAECBAgQIJALCDNsBALDERBkxM6CjNjGCgECBAgQIECAAAECBAi0W0CU0e75ujsCBAgQIECAAAECBAjkAsIMG4FAtQKCjNhXkBHbWCFAgAABAgQIECBAgACB9guIMto/Y3dIgAABAgQIECBAgACBXECYYSMQqEZAkBG7CjJiGysECBAgQIAAAQIECBAg0A0BUUY35uwuCRAgQIAAAQIECBAgkAsIM2wEAoMVEGTEnoKM2MYKAQIECBAgQIAAAQIECHRHQJTRnVm7UwIECBAgQIAAAQIECOQCwgwbgcBgBAQZsaMgI7axQoAAAQIECBAgQIAAAQLdEhBl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IECAAAECBAgQIECAQBcFRBldnLp7JkCAAAECBAgQIECAAIFcYJBhxtp161MXwgxBRvz/PIKM2MYKAQIECBAgQIAAAQIECBDoqoAoo6uTd98ECBAgQIAAAQIECBAgkAsIM2a/EQQZsZUgI7axQoAAAQIECBAgQIAAAQIEuiwgyujy9N07AQIECBAgQIAAAQIECOQCwoyZN4IgIzYSZMQ2VggQIECAAAECBAgQIECAQNcFRBld3wHunwABAgQIECBAgAABAgRyAWFGvBEEGbGNICO2sUKAAAECBAgQIECAAAECBAikJMqwCwgQIECAAAECBAgQIECAwI8FhBl7bwVBxt4mvWcEGT0JvwkQIECAAAECBAgQIECAAIFIQJQRyXieAAECBAgQIECAAAECBDopIMzYNXZBxi6LyY8EGZNF/E2AAAECBAgQIECAAAECBAhMJSDKmErFcwQIECBAgAABAgQIECDQaQFhRkqCjPj/BQQZsY0VAgQIECBAgAABAgQIECBAYE8BUcaeHv4iQIAAAQIECBAgQIAAAQK5QJfDjBtuuDGdMLEibd2+vdRuGBsbS6tXr0rj44tLHadObxZk1GkaroUAAQIECBAgQIAAAQIECNRfQJRR/xm5QgIECBAgQIAAAQIECBAYkUAXw4wsyDjxxDMFGVPsOUHGFCieIkCAAAECBAgQIECAAAECBKYVEGVMy2ORAAECBAgQIECAAAECBLou0KUwoxdkbN/+/VJj9wkZpfi8mQABAgQIECBAgAABAgQIEGiRgCijRcN0KwQIECBAgAABAgQIECBQjUAXwgxBRrx3fEJGbGOFAAECBAgQIECAAAECBAgQmF5AlDG9j1UCBAgQIECAAAECBAgQIJALtDnMEGTEm1yQEdtYIUCAAAECBAgQIECAAAECBGYWEGXMbOQVBAgQIECAAAECBAgQIEAgF2hjmCHIiDe3ICO2sUKAAAECBAgQIECAAAECBAjMTkCUMTsnryJAgAABAtr3mcoAAEAASURBVAQIECBAgAABArlAm8IMQUa8qQUZsY0VAgQIECBAgAABAgQIECBAYPYCoozZW3klAQIECBAgQIAAAQIECBDIBdoQZggy4s0syIhtrBAgQIAAAQIECBAgQIAAAQJzExBlzM3LqwkQIECAAAECBAgQIECAQC7Q5DBDkBFvYkFGbGOFAAECBAgQIECAAAECBAgQmLuAKGPuZt5BgAABAgQIECBAgAABAgRygSaGGYKMePMKMmIbKwQIECBAgAABAgQIECBAgEB/AqKM/ty8iwABAgQIECBAgAABAgQI5AJNCjMEGfGmFWTENlYIECBAgAABAgQIECBAgACB/gVEGf3beScBAgQIECBAgAABAgQIEMgFmhBmCDLizSrIiG2sECBAgAABAgQIECBAgAABAuUERBnl/LybAAECBAgQIECAAAECBAjkAnUOMwQZ8SYVZMQ2VggQIECAAAECBAgQIECAAIHyAqKM8oaOQIAAAQIECBAgQIAAAQIEcoE6hhmCjHhzCjJiGysECBAgQIAAAQIECBAgQIDAYAREGYNxdBQCBAgQIECAAAECBAgQIJAL1CnMEGTEm1KQEdtYIUCAAAECBAgQIECAAAECBAYnIMoYnKUjESBAgAABAgQIECBAgACBXKAOYYYgI96MgozYxgoBAgQIECBAgAABAgQIECAwWAFRxmA9HY0AAQIECBAgQIAAAQIECOQCVYQZN33nllnpCjJiJkFGbGOFAAECBAgQIECAAAECBAgQGLyAKGPwpo5IgAABAgQIECBAgAABAgRygV6Y8eaVp5QWWbtufVo2sTzNFGYIMmJqQUZsY4UAAQIECBAgQIAAAQIECBCoRkCUUY2roxIgQIAAAQIECBAgQIAAgVwgCzNOX3lSWvGm15cW2bx127RhhiAjJhZkxDZWCBAgQIAAAQIECBAgQIAAgeoERBnV2ToyAQIECBAgQIAAAQIECBDIBbIw48y3nJpOO3mitEgUZggyYlpBRmxjhQABAgQIECBAgAABAgQIEKhWQJRRra+jEyBAgAABAgQIECBAgACBXCALM956/hmVhBmCjHiTCTJiGysECBAgQIAAAQIECBAgQIBA9QKijOqNnYEAAQIECBAgQIAAAQIECOQCVYQZf/Hpz6aTTjw7bd/+/dLKl11ybhofX1z6OHU5gCCjLpNwHQQIECBAgAABAgQIECBAoLsC87t76+6cAAECBAgQIECAAAECBAgMX6AXZmRnvvr3ryl1AdlXmZx73mWljpG9eWxsLK1evUqQUVrSAQgQIECAAAECBAgQIECAAAECewr4pIw9PfxFgAABAgQIECBAgAABAgQqF+iFGaedPFH5uWY6gSBjJiHrBAgQIECAAAECBAgQIECA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uzABAgQIECBAgAABAgQIECDQjQKCMroR36UJECBAgAABAgQIECBAgEChAsUGZfTr1y/EwIjuCMwQkFHoKqtPgAABAgQIECBAgAABAgQINIpAbWwo2yia5kGAAAECBAgQIECAAAECBCosUGxQRty+ZF1ARltbW1i7dm2FR/pG9wIyqsLsIgQIECBAgAABAgQIECBAgECNCgjKqNGFMSwCBAgQIECAAAECBAgQINCZQHt7e2eHM4/17PlGssxevXrl6lYjMENARuayqECAAAECBAgQIECAAAECBAg0uIDtSxp8gU2PAAECBAgQIECAAAECBBpLoJhMGesCMaJEzJYRH1d6KxMBGY31vDMbAgQIECBAgAABAgQIECBAoDgBQRnFuWlFgAABAgQIECBAgAABAgS6RaCYTBktLS0bjLXSgRkCMjbg9oAAAQIECBAgQIAAAQIECBBoYgFBGU28+KZOgAABAgQIECBAgAABAvUnUEymjNbWjXcvjYEZvXv3Dv369ctlzyiXhICMcknqhwABAgQIECBAgAABAgQIEGgEAUEZjbCK5kCAAAECBAgQIECAAAECTSNQjkwZ62PFrUzKFZghIGN9Wd8TIECAAAECBAgQIECAAAECBEIQlOFZQIAAAQIECBAgQIAAAQIE6kigXJky1p9yOQIzBGSsL+p7AgQIECBAgAABAgQIECBAgMAbAoIyPBMIECBAgAABAgQIECBAgEAdCVQiKCNOv5TADAEZdfQEMlQCBAgQIECAAAECBAgQIECgqgKCMqrK7WIECBAgQIAAAQIECBAgQKA0gXJvX7L+aIoJzBCQsb6g7wkQIECAAAECBAgQIECAAAECGwoIytjQwyMCBAgQIECAAAECBAgQIFDTApXKlLFu0oUEZgjIWKfmXwIECBAgQIAAAQIECBAgQIBA5wKCMjp3cZQAAQIECBAgQIAAAQIECNSkQKWDMuKk8wnMEJBRk08PgyJAgAABAgQIECBAgAABAgRqTKC1xsZjOAQIECBAgAABAgQIECBAgECKQCW3L1n/sjEwI5bly5eHtWvXrn8q9OvXLxe40aNHjw2Oe0CAAAECBAgQIECAAAECBAgQILChgEwZG3p4RIAAAQIECBAgQIAAAQIEalqgGpky1gF0ljFDQMY6Hf8SIECAAAECBAgQIECAAAECBLIFZMrINlKDAAECBAgQIECAAAECBAjUjEA1gzLipNdlzGhrawt9+vSRIaNmngkGQoAAAQIECBAgQIAAAQIECNSDgKCMelglYyRAgAABAgQIECBAgAABAn8TqNb2JeuDx8CMlpaWELcrsWXJ+jK+J0CAAAECBAgQIECAAAECBAikCwjKSPdxlgABAgQIECBAgAABAgQI1JRAtTNlrJt8z552QF1n4V8CBAgQIECAAAECBAgQIECAQL4C/qKSr5R6BAgQIECAAAECBAgQIECgBgS6KyijBqZuCAQIECBAgAABAgQIECBAgACBuhMQlFF3S2bABAgQIECAAAECBAgQINDMAt2xfUkze5s7AQIECBAgQIAAAQIECBAgQKAUAUEZpehpS4AAAQIECBAgQIAAAQIEqiwgU0aVwV2OAAECBAgQIECAAAECBAgQIFCCgKCMEvA0JUCAAAECBAgQIECAAAEC1RYQlFFtcdcjQIAAAQIECBAgQIAAAQIECBQvICijeDstCRAgQIAAAQIECBAgQIBA1QUEZVSd3AUJECBAgAABAgQIECBAgAABAkULCMoomk5DAgQIECBAgAABAgQIECBQfYH29vaCL9rS0lJwGw0IECBAgAABAgQIECBAgAABAgRKFxCUUbqhHggQIECAAAECBAgQIECAQNUEZMqoGrULESBAgAABAgQIECBAgAABAgRKFhCUUTKhDggQIECAAAECBAgQIECAQPUEBGVUz9qVCBAgQIAAAQIECBAgQIAAAQKlCgjKKFVQewIECBAgQIAAAQIECBAgUEUB25dUEdulCBAgQIAAAQIECBAgQIAAAQIlCgjKKBFQcwIECBAgQIAAAQIECBAgUE0BmTKqqe1aBAgQIECAAAECBAgQIECAAIHSBARllOanNQECBAgQIECAAAECBAgQqKqAoIyqcrsYAQIECBAgQIAAAQIECBAgQKAkAUEZJfFpTIAAAQIECBAgQIAAAQIEqitg+5LqersaAQIECBAgQIAAAQIECBAgQKAUAUEZpehpS4AAAQIECBAgQIAAAQIEqiwgU0aVwV2OAAECBAgQIECAAAECBAgQIFCCgKCMEvA0JUCAAAECBAgQIECAAAEC1RYQlFFtcdcjQIAAAQIECBAgQIAAAQIECBQvICijeDstCRAgQIAAAQIECBAgQIBA1QVsX1J1chckQIAAAQIECBAgQIAAAQIECBQtICijaDoNCRAgQIAAAQIECBAgQIBA9QVkyqi+uSsSIECAAAECBAgQIECAAAECBIoVEJRRrJx2BAgQIECAAAECBAgQIECgGwQEZXQDuksSIECAAAECBAgQIECAAAECBIoUEJRRJJxmBAgQIECAAAECBAgQIECgOwRsX9Id6q5JgAABAgQIECBAgAABAgQIEChOQFBGcW5aESBAgAABAgQIECBAgACBbhGQKaNb2F2UAAECBAgQIECAAAECBAgQIFCUgKCMotg0IkCAAAECBAgQIECAAAEC3SMgKKN73F2VAAECBAgQIECAAAECBAgQIFCMgKCMYtS0IUCAAAECBAgQIECAAAEC3SRg+5JugndZAgQIECBAgAABAgQIECBAgEARAoIyikDThAABAgQIECBAgAABAgQIdJeATBndJe+6BAgQIECAAAECBAgQIECAAIHCBQRlFG6mBQECBAgQIECAAAECBAgQ6DYBQRndRu/CBAgQIECAAAECBAgQIECAAIGCBQRlFEymAQECBAgQIECAAAECBAgQ6D4B25d0n70rEyBAgAABAgQIECBAgAABAgQKFRCUUaiY+gQIECBAgAABAgQIECBAoBsFZMroRnyXJkCAAAECBAgQIECAAAECBAgUKCAoo0Aw1QkQIECAAAECBAgQIECAQHcKCMroTn3XJkCAAAECBAgQIECAAAECBAgUJiAoozAvtQkQIECAAAECBAgQIECAQLcK2L6kW/ldnAABAgQIECBAgAABAgQIECBQkICgjIK4VCZAgAABAgQIECBAgAABAt0rIFNG9/q7OgECBAgQIECAAAECBAgQIECgEAFBGYVoqUuAAAECBAgQIECAAAECBLpZQFBGNy+AyxMgQIAAAQIECBAgQIAAAQIEChAQlFEAlqoECBAgQIAAAQIECBAgQKC7BWxf0t0r4PoECBAgQIAAAQIECBAgQIAAgfwFBGXkb6UmAQIECBAgQIAAAQIECBDodgGZMrp9CQyAAAECBAgQIECAAAECBAgQIJC3gKCMvKlUJECAAAECBAgQIECAAAEC3S8gKKP718AICBAgQIAAAQIECBAgQIAAAQL5CgjKyFdKPQIECBAgQIAAAQIECBAgUAMCti+pgUUwBAIECBAgQIAAAQIECBAgQIBAngKCMvKEUo0AAQIECBAgQIAAAQIECNSCgEwZtbAKxkCAAAECBAgQIECAAAECBAgQyE9AUEZ+TmoRIECAAAECBAgQIECAAIGaEBCUURPLYBAECBAgQIAAAQIECBAgQIAAgbwEBGXkxaQSAQIECBAgQIAAAQIECBCoDQHbl9TGOhgFAQIECBAgQIAAAQIECBAgQCAfAUEZ+SipQ4AAAQIECBAgQIAAAQIEakRApowaWQjDIECAAAECBAgQIECAAAECBAjkISAoIw8kVQgQIECAAAECBAgQIECAQK0ICMqolZUwDgIECBAgQIAAAQIECBAgQIBAtoCgjGwjNQgQIECAAAECBAgQIECAQM0I2L6kZpbCQAgQIECAAAECBAgQIECAAAECmQKCMjKJVCBAgAABAgQIECBAgAABArUjIFNG7ayFkRAgQIAAAQIECBAgQIAAAQIEsgQEZWQJOU+AAAECBAgQIECAAAECBGpIQFBGDS2GoRAgQIAAAQIECBAgQIAAAQIEMgQEZWQAOU2AAAECBAgQIECAAAECBGpJQFBGLa2GsRAgQIAAAQIECBAgQIAAAQIE0gUEZaT7OEuAAAECBAgQIECAAAECBGpKoL29veDxtLS0FNxGAwIECBAgQIAAAQIECBAgQIAAgdIFBGWUbqgHAgQIECBAgAABAgQIECBQNQGZMqpG7UIECBAgQIAAAQIECBAgQIAAgZIFBGWUTKgDAgQIECBAgAABAgQIECBQPQFBGdWzdiUCBAgQIECAAAECBAgQIECAQKkCgjJKFdSeAAECBAgQIECAAAECBAhUUcD2JVXEdikCBAgQIECAAAECBAgQIECAQIkCgjJKBNScAAECBAgQIECAAAECBAhUU0CmjGpquxYBAgQIECBAgAABAgQIECBAoDQBQRml+WlNgAABAgQIECBAgAABAgSqKiAoo6rcLkaAAAECBAgQIECAAAECBAgQKElAUEZJfBoTIECAAAECBAgQIECAAIHqCti+pLrerkaAAAECBAgQIECAAAECBAgQKEVAUEYpetoSIECAAAECBAgQIECAAIEqC8iUUWVwlyNAgAABAgQIECBAgAABAgQIlCAgKKMEPE0JECBAgAABAgQIECBAgEC1BQRlVFvc9QgQIECAAAECBAgQIECAAAECxQsIyijeTksCBAgQIECAAAECBAgQIFB1gbVr1xZ8zZaWloLbaECAAAECBAgQIECAAAECBAgQIFC6gKCM0g31QIAAAQIECBAgQIAAAQIEqiKwatWqoq7T2tpaVDuNCBAgQIAAAQIECBAgQIAAAQIEShMQlFGan9YECBAgQIAAAQIECBAgQKBqAsVsXRIHJyijakvkQgQIECBAgAABAgQIECBAgACBDQQEZWzA4QEBAgQIECBAgAABAgQIEKhdgfb29qIGJyijKDaNCBAgQIAAAQIECBAgQIAAAQIlCwjKKJlQBwQIECBAgAABAgQIECBAoDoCxWTK6NnTf/2rszquQoAAAQIECBAgQIAAAQIECBDYWMBfZjY2cYQAAQIECBAgQIAAAQIECNSkQDFBGbJk1ORSGhQBAgQIECBAgAABAgQIECDQJAKCMppkoU2TAAECBAgQIECAAAECBOpfoJjtS1paWup/4mZAgAABAgQIECBAgAABAgQIEKhTAUEZdbpwhk2AAAECBAgQIECAAAECzScgU0bzrbkZEyBAgAABAgQIECBAgAABAvUtICijvtfP6AkQIECAAAECBAgQIECgiQQEZTTRYpsqAQIECBAgQIAAAQIECBAg0BACgjIaYhlNggABAgQIECBAgAABAgSaQcD2Jc2wyuZIgAABAgQIECBAgAABAgQINJKAoIxGWk1zIUCAAAECBAgQIECAAIGGFpApo6GX1+QIECBAgAABAgQIECBAgACBBhQQlNGAi2pKBAgQIECAAAECBAgQINCYAoIyGnNdzYoAAQIECBAgQIAAAQIECBBoXAFBGY27tmZGgAABAgQIECBAgAABAg0mYPuSBltQ0yFAgAABAgQIECBAgAABAgQaXkBQRsMvsQkSIECAAAECBAgQIECAQKMIyJTRKCtpHgQIECBAgAABAgQIECBAgECzCAjKaJaVNk8CBAgQIECAAAECBAgQqHsBQRl1v4QmQIAAAQIECBAgQIAAAQIECDSZgKCMJltw0yVAgAABAgQIECBAgACB+hWwfUn9rp2REyBAgAABAgQIECBAgAABAs0pICijOdfdrAkQIECAAAECBAgQIECgDgVkyqjDRTNkAgQIECBAgAABAgQIECBAoKkFBGU09fKbPAECBAgQIECAAAECBAjUk4CgjHpaLWMlQIAAAQIECBAgQIAAAQIECIQgKMOzgAABAgQIECBAgAABAgQI1ImA7UvqZKEMkwABAgQIECBAgAABAgQIECDwNwFBGZ4KBAgQIECAAAECBAgQIECgTgRkyqiThTJMAgQIECBAgAABAgQIECBAgMDfBARleCoQIECAAAECBAgQIECAAIE6ERCUUScLZZgECBAgQIAAAQIECBAgQIAAgb8JCMrwVCBAgAABAgQIECBAgAABAnUiYPuSOlkowyRAgAABAgQIECBAgAABAgQI/E1AUIanAgECBAgQIECAAAECBAgQqBMBmTLqZKEMkwABAgQIECBAgAABAgQIECDwNwFBGZ4KBAgQIECAAAECBAgQIECgTgQEZdTJQhkmAQIECBAgQIAAAQIECBAgQOBvAoIyPBUIECBAgAABAgQIECBAgECdCNi+pE4WyjAJECBAgAABAgQIECBAgAABAn8TEJThqUCAAAECBAgQIECAAAECBOpEQKaMOlkowyRAgAABAgQIECBAgAABAgQI/E1AUIanAgECBAgQIECAAAECBAgQqBMBQRl1slCGSYAAAQIECBAgQIAAAQIECBD4m4CgDE8FAgQIECBAgAABAgQIECBQJwKCMupkoQyTAAECBAgQIECAAAECBAgQIPA3AUEZngoECBAgQIAAAQIECBAgQKBOBNrb2wseaUtLS8FtNCBAgAABAgQIECBAgAABAgQIECiPgKCM8jjqhQABAgQIECBAgAABAgQIVFxApoyKE7sAAQIECBAgQIAAAQIECBAgQKCsAoIyysqpMwIECBAgQIAAAQIECBAgUDkBQRmVs9UzAQIECBAgQIAAAQIECBAgQKASAoIyKqGqTwIECBAgQIAAAQIECBAgUAEB25dUAFWXBAgQIECAAAECBAgQIECAAIEKCgjKqCCurgkQIECAAAECBAgQIECAQDkFZMoop6a+CBAgQIAAAQIECBAgQIAAAQKVFxCUUXljVyBAgAABAgQIECBAgAABAmUREJRRFkadECBAgAABAgQIECBAgAABAgSqJiAoo2rULkSAAAECBAgQIECAAAECBEoTsH1JaX5aEyBAgAABAgQIECBAgAABAgSqLSAoo9rirkeAAAECBAgQIECAAAECBIoUkCmjSDjNCBAgQIAAAQIECBAgQIAAAQLdJCAoo5vgXZYAAQIECBAgQIAAAQIECBQqICijUDH1CRAgQIAAAQIECBAgQIAAAQLdKyAoo3v9XZ0AAQIECBAgQIAAAQIECOQtYPuSvKlUJECAAAECBAgQIECAAAECBAjUhICgjJpYBoMgQIAAAQIECBAgQIAAAQLZAjJlZBupQYAAAQIECBAgQIAAAQIECBCoJQFBGbW0GsZCgAABAgQIECBAgAABAgRSBARlpOA4RYAAAQIECBAgQIAAAQIECBCoQQFBGTW4KIZEgAABAgQIECBAgAABAgQ6E7B9SWcqjhEgQIAAAQIECBAgQIAAAQIEaldAUEbtro2RESBAgAABAgQIECBAgACBDQRkytiAwwMCBAgQIECAAAECBAgQIECAQM0LCMqo+SUyQAIECBAgQIAAAQIECBAg8IaAoAzPBAIECBAgQIAAAQIECBAgQIBAfQkIyqiv9TJaAgQIECBAgAABAgQIEGhiAduXNPHimzoBAgQIECBAgAABAgQIECBQlwKCMupy2QyaAAECBAgQIECAAAECBJpRQKaMZlx1cyZAgAABAgQIECBAgAABAgTqWUBQRj2vnrETIECAAAECBAgQIECAQFMJCMpoquU2WQIECBAgQIAAAQIECBAgQKABBARlNMAimgIBAgQIECBAgAABAgQINIeA7UuaY53NkgABAgQIECBAgAABAgQIEGgcAUEZjbOWZkKAAAECBAgQIECAAAECDS4gU0aDL7DpESBAgAABAgQIECBAgAABAg0nICij4ZbUhAgQIECAAAECBAgQIECgUQUEZTTqypoXAQIECBAgQIAAAQIECBAg0KgCgjIadWXNiwABAgQIECBAgAABAgQaTsD2JQ23pCZEgAABAgQIECBAgAABAgQINLiAoIwGX2DTI0CAAAECBAgQIECAAIHGEVi7dm3Bk2ltbS24jQYECBAgQIAAAQIECBAgQIAAAQLlERCUUR5HvRAgQIAAAQIECBAgQIAAgYoLFLN9Sc+e/utf8YVxAQIECBAgQIAAAQIECBAgQIBAFwL+MtMFjMMECBAgQIAAAQIECBAgQKDWBIrZvqRXr161Ng3jIUCAAAECBAgQIECAAAECBAg0jYCgjKZZahMlQIAAAQIECBAgQIAAgXoXWLNmTcFTKKZNwRfRgAABAgQIECBAgAABAgQIECBAoFMBQRmdsjhIgAABAgQIECBAgAABAgRqT6C1tbXgQQnKKJhMAwIECBAgQIAAAQIECBAgQIBA2QQEZZSNUkcECBAgQIAAAQIECBAgQKCyAn379i34AsuXLy+4jQYECBAgQIAAAQIECBAgQIAAAQLlERCUUR5HvRAgQIAAAQIECBAgQIAAgYoLCMqoOLELECBAgAABAgQIECBAgAABAgTKKiAoo6ycOiNAgAABAgQIECBAgAABApUT6NevX8Gdy5RRMJkGBAgQIECAAAECBAgQIECAAIGyCQjKKBuljggQIECAAAECBAgQIECAQGUFZMqorK/eCRAgQIAAAQIECBAgQIAAAQLlFhCUUW5R/REgQIAAAQIECBAgQIAAgQoJyJRRIVjdEiBAgAABAgQIECBAgAABAgQqJCAoo0KwuiVAgAABAgQIECBAgAABAuUWKCZTRltbW7mHoT8CBAgQIECAAAECBAgQIECAAIE8BQRl5AmlGgECBAgQIECAAAECBAgQ6G6BYoIyli9f3t3Ddn0CBAgQIECAAAECBAgQIECAQNMKCMpo2qU3cQIECBAgQIAAAQIECBCoNwHbl9TbihkvAQIECBAgQIAAAQIECBAg0OwCgjKa/Rlg/gQIECBAgAABAgQIECBQNwIyZdTNUhkoAQIECBAgQIAAAQIECBAgQCAnICjDE4EAAQIECBAgQIAAAQIECNSJQDFBGW1tbXUyO8MkQIAAAQIECBAgQIAAAQIECDSegKCMxltTMyJAgAABAgQIECBAgACBBhWwfUmDLqxpESBAgAABAgQIECBAgAABAg0rICijYZfWxAgQIECAAAECBAgQIECg0QSKyZSxfPnyRmMwHwIECBAgQIAAAQIECBAgQIBA3QgIyqibpTJQAgQIECBAgAABAgQIEGh2AZkymv0ZYP4ECBAgQIAAAQIECBAgQIBAvQkIyqi3FTNeAgQIECBAgAABAgQIEGhagWIyZbS1tTWtl4kTIECAAAECBAgQIECAAAECBLpbQFBGd6+A6xMgQIAAAQIECBAgQIAAgTwFignKsH1JnriqESBAgAABAgQIECBAgAABAgQqICAoowKouiRAgAABAgQIECBAgAABApUQsH1JJVT1SYAAAQIECBAgQIAAAQIECBConICgjMrZ6pkAAQIECBAgQIAAAQIECJRVQKaMsnLqjAABAgQIECBAgAABAgQIECBQcQFBGRUndgECBAgQIECAAAECBAgQIFAegWKCMtra2spzcb0QIECAAAECBAgQIECAAAECBAgULCAoo2AyDQgQIECAAAECBAgQIECAQPcIDBkypOALL1q0KKxatargdhoQIECAAAECBAgQIECAAAECBAiULiAoo3RDPRAgQIAAAQIECBAgQIAAgaoIDB06NPTq1auga61duzbMmTOnoDYqEyBAgAABAgQIECBAgAABAgQIlEegtTzd6IUAAQIECBAgQKBQgWXLloVXXnml0GbqEyBAgECTC5x33nlh6dKlBSnMnj07rF69uqA2KhMgQIBA8wn07NkzjB07tvkmbsYECBAgQIAAAQIEKiggKKOCuLomQIAAAQIECKQJrFy5MsyfPz+tinMECBAgQGAjgbe+9a0bHcvngN85+SipQ4AAgeYWEJTR3Otv9gQIECBAgAABApURsH1JZVz1SoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECDS5gKCMJn8CmD4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQGQFBGZVx1SsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQ5AKCMpr8CWD6BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQGUEBGVUxlWvBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgKQV69OhR8LyLaVPwRbqhgaCMbkB3SQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0KgCK1euLHhqvXr1KrhNPTQQlFEPq2SMBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgTgRWrFhR8Ej79OlTcJt6aCAoox5WyRgJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECdCLS1tRU80r59+xbcph4aCMqoh1UyRgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCcCMmW8uVCCMt608B0BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQokAxQRkyZZSIrjkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQ+ALFBGX06dOnIWFkymjIZTUpAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQPQJtbW0FX1hQRsFkGhAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLNJLB27dqwdOnSgqds+5KCyTQgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEmknglVdeCTEwo9AyZMiQQpvURX3bl9TFMhkkAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCofYEYlFFoiQEZPXr0KLRZXdQXlFEXy2SQBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECg9gWKCcoYOXJk7U+syBEKyigSTjMCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgQwFBGRt6CMrY0MMjAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoEiBOXPmFNxyxIgRBbeplwaCMuplpYyTAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjUuIBMGRsukKCMDT08IkCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBIoUmDFjRsEtR44cWXCbemkgKKNeVso4CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAjQtMmTKl4BEKyiiYTAMCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECg2QSmTp1a8JTHjh1bcJt6aSBTRr2slHESIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEaFnjuuefCypUrCx7hzjvvXHCbemkgKKNeVso4CRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBADQsUs3VJ7969w+jRo2t4VqUNTVBGaX5aEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAonA5MmTC3YYP358wW3qqUFrPQ3WWAkQIECAAAECBAgQqJ7AtX+8JVzx2+vzuuDoMaPCd87/cujRo0de9Rux0vnfuTg88fiUvKb2tqMPCR/7xHF51VWJAAECBAgQIECAAAECBAgQIECAQL0IPPbYYwUPtZG3LokYgjIKfkpoQIAAAQIECBAgQIBAR4FZM2eHFStWhr59+3Q85XEnAs8+O6OTow4R6B6B6dNnhSefeCY8lzwv58yeFxYuXBza2laENWvWht69e4X+/fuFocMGhy22GBG23W5M2GvvXcOQIZt1z2BdlQABAgQIECBAgAABAgQIEKhpgfvuu6/g8e20004Ft6mnBoIy6mm1jJUAAQIECBAgQIBALQusreXB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tBbZN6p96lOfiq9//euZN5vsXJY2NVKYgO1LCvNTmgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQaTKDcYYzEe9RRR8U3v/nN2GuvvRpMu7aHK5RR2/On9wQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJoFKhDGGDBkS3/72t2Po0KFlGqVmiinQsZiVqYsAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNSbwJQpU+KAAw6IkSNHlm2rkj333DNuuummeOCBBwQyaviCslJGDU+erhMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA6QQqsTLGwIEDY/z48fHxj3+8dANTc9kEhDLKRq0hAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKgFgXvvvTcuu+yysq2KkUwGDRoUEyZMiNGjR9cCkT62UUAoo41QTiNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB+hZIYYxJkybFjBkzyjbQIUOGZMMYRx99dNna1FD5BIQyymetJQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoQoFKhDEOOuigbBhj1KhRVSiiS8USEMoolqR6CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCmBKZMmZJdGePBBx8sW78PPvjgbBjj0EMPLVubGqqcgFBG5ey1TIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVEKjEyhjDhg2Lyy67LEaMGFGBEWuyUgJCGZWS1y4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlFWgEmGMtE1JCmMcdthhZR2rxqpDQCijOuZBLwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgRAL33HNPNhgxY8aMErXw3moPOOCAbJtHHHHEe5/0SMMICGU0zFQbKAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBBpLIIUxJk2aFPfff3/ZBv6hD30oG8Y48sgjy9amhqpXQCijeudGzwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEAgD4FKhDH233//bADkmGOOyaPHitSrgFBGvc6scREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKDBBCoRxthvv/2yYYxjjz22wbQNty0CQhltUXIOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFStQCXCGB/84AezYYzjjjuual10rPICQhmVnwM9IECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIE8BO69995sMGLGjBl5lM6vyF577RUTJ06MT3ziE9GhQ4f8KlGqYQSEMhpmqg2UAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC9SFQqTDGhAkT4vjjjxfGqI/LqCyjEMooC7NGCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBQAWGMQgWVL7eAUEa5xbVHgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAu0SqEQY4wMf+ECklTFOOOEEK2O0a7acvKGAUMaGGr4mQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgaoREMaomqnQkTwFhDLyhFOMAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBEojIIxRGle1ll9AKKP85lokQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgWYEKhHGGDBgQHabkhNPPNE2Jc3MiYcKExDKKMxPaQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoUEAYo0BAxatWQCijaqdGxwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFDfAsIY9T2/RhchlOEqIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGyClQqjDF+/Pg46aSTbFNS1tlu7MaEMhp7/o2eAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZROoRBhjzz33jAkTJsSJJ54YHTt2LNtYNUQgCQhluA4IECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoKQCwhgl5VV5FQsIZVTx5OgaAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEalmgUmGMt7YpsTJGLV899dF3oYz6mEejIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQNUIVCKMsccee2S3KTnppJNsU1I1V4KOCGW4BggQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgKALCGEVhVEkdCQhl1NFkGgoBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQqIVCpMEbapuTkk0+2MkYlJl2bbRIQymgTk5MIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAYGMBYYyNRXxP4N0CQhnv9vAdAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECOQQqEQYY/fdd48JEyZYGSPH3Hi6ugSEMqprPvSGAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECVStQqTBG2qbkk5/8pG1KqvbK0LGWBIQyWpLxOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkBYQxXAgE8hMQysjPTSkCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjUvYAwRt1PsQGWWEAoo8TAqidAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECtCVQijNG/f/94a5uSTp061RqZ/hJoVqDD+szR7DMeJECAAAECBAgQKKnAunXrYtWqVSVtQ+UECBAgQKAWBJ577rn40pe+FK+99lre3T3llFPipJNOyru8ggQIECDwfwK9evVCQYAAAQIECDS4gDBGg18Ahl90AaGMopOqkAABAgQIECBAgAABAgQIEGivwB133BHHHnts5PvekQ4dOsTkyZNj9OjR7W3a+QQIECBAgAABAgQIECCQERDGcBkQKI1Ax9JUq1YCBAgQIECAAAECBAgQIECAQNsFjjnmmOwStW0v8e4zU5jj5JNPjnnz5r37Cd8RIECAAAECBAgQIECAQKsCKYwxfPjwOPTQQ2PGjBmtnlusJ3fbbbf46U9/GvPnz48xY8aErUqKJaueahSwUkY1zoo+ESBAgAABAgQIECBAgACBBhRIwYojjjgipk6dmvfot91225gzZ06kzw4CBAgQIECAAAECBAgQaFlg2rRpMWnSpJg+fXrLJxX5mRTGGD9+fHzqU58SxCiyreqqV0Aoo3rnRs8IECBAgAABAgQIECBAgEDDCbz00kuxzz77xJIlS/Ie+8CBA2PWrFnRrVu3vOtQkAABAgQIECBAgAABAvUqIIxRrzNrXNUqYPuSap0Z/SJAgAABAgQIECBAgAABAg0o0KtXr/j1r38dm222Wd6jT1uYpK1M0sobDgIECBAgQIAAAQIECBD4P4EUxkjblIwcObJsq2OklTF+8pOfxF//+tc45ZRTrI7hYmxIAaGMhpx2gyZAgAABAgQIECBAgAABAtUrsPfee8f1119fUAdvu+22mDBhQkF1KEyAAAECBAgQIECAAIF6EKhEGOP973//22GMT3/608IY9XAhGUPeArYvyZtOQQIECBAgQIAAAQIECBAgQKCUAuPGjYvvfOc7BTXx85//PD75yU8WVIfCBAgQIECAAAECBAgQqEWBSmxTksIYl156aYwZM0YQoxYvGn0uiYBQRklYVUqAAAECBAgQIECAAAECBAgUKvDmm2/G0UcfHXfddVfeVXXt2jVmzJgRgwcPzrsOBQkQIECAAAECBAgQIFBLAsIYtTRb+toIAkIZjTDLxkiAAAECBAgQIECAAAECBGpUYNWqVTFo0KBYuHBh3iPo06dPzJkzJ/r27Zt3HQoSIECAAAECBAgQIECg2gWEMap9hvSvUQWEMhp15o2bAAECBAgQIECAAAECBAjUiMCiRYuywYzly5fn3eMBAwbEgw8+GN27d8+7DgUJECBAgAABAgQIECBQjQKVCGPsuuuu2W1KTjnlFNuUVONFoU9VJdCxqnqjMwQIECBAgAABAgQIECBAgACBjQT69esXt99+e3Tp0mWjZ9r+7fz58+P444+PtCWKgwABAgQIECBAgAABAvUgkMIYw4cPj5EjR8b06dPLMqQUxrjuuutiwYIFcdpppwlklEVdI7UuIJRR6zOo/wQIECBAgAABAgQIECBAoAEEhg4dGtdee21BI73rrrviggsuKKgOhQkQIECAAAECBAgQIFBpgWoIY3Tu3LnSDNonUDMCti+pmanSUQIECBAgQIAAAQIECBAgQCCFKr75zW8WBHH99dfHmDFjCqpDYQIECBAgQIAAAQIECJRboFLblFxyySWRtikRxCj3jGuvXgSEMuplJo2DAAECBAgQIECAAAECBAg0gMD69evjuOOOi9tuuy3v0aZtUNIfM9PqGw4CBAgQIECAAAECBAhUu0Alwhi77LJLXHrppcIY1X5x6F9NCAhl1MQ06SQBAgQIECBAgAABAgQIECDwlsDq1atj//33j/nz57/1ULs/NzU1xZw5c6Jfv37tLqsAAQIECBAgQIAAAQIEyiEgjFEOZW0QKL2AUEbpjbVAgAABAgQIECBA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Y489FmeeeWZYHaMs7BohUBUCQhlVMQ06QYAAAQIECBAgQIAAAQIECJRbYMSIEXH11VcX1Oxtt90W48ePL6gOhQkQIECAAAECBAgQKI9ApcMYZ511ljBGeaZaKwSqSsD2JVU1HTpDgAABAgQIECBAgAABAgQIlFvgy1/+cnz3u98tqNmbbropTjjhhILqUJgAAQIECBAgQIAAgdIIVGKbkh122CEuvvji+OxnPyuIUZppVSuBmhEQyqiZqdJRAgQIECBAgAABAgQIECBAoBQCb775Zhx22GGR/lCb79G1a9eYMWNGDB48ON8qlCNAgAABAgQIECBAoMgClQpjXHTRRfG5z31OGKPI86k6ArUqIJRRqzOn3wQIECBAgAABAgQIECBAgEDRBFauXJkNVCxcuDDvOvv06RNz5syJtFe0gwABAgQIECBAgACBygkIY1TOXssECLxXQCjjvSYeIUCAAAECBAgQIECAAAECBBpQYNGiRTFo0KBYvnx53qMfMGBAPPjgg9G9e/e861CQAAECBAgQIECAAIH8BIQx8nNTigCB0gp0LG31aidAgAABAgQIECBAgAABAgQI1IZAv3794vbbb48uXbrk3eH58+fH8ccfH2lLFAcBAgQIECBAgAABAuURSGGM4cOHx8iRI2P69OllaXSHHXaI733ve/H444/H2WefbauSsqhrhEBtCghl1Oa86TUBAgQIECBAgAABAgQIECBQAoGhQ4fGtddeW1DNd911V1xwwQUF1aEwAQIECBAgQIAAAQK5BSoRxth+++3fDmN88YtfFMbIPU3OINDwArYvafhLAAABAgQIECBAgAABAgQIECCwscD5558fV1555cYPt+v766+/PsaMGdOuMk4mQIAAAQIECBAgQCC3QCW2KUlhjIsuuijOOOMMQYzcU+QMAgQ2EBDK2ADDlwQIECBAgAABAgQIECBAgACBJLB+/fo46qijIq16ke+RtkFJfyxOq284CBAgQIAAAQIECBAoXKBSYYwLL7wwG8bo2rVr4YNQAwECDScglNFwU27ABAgQIECAAAECBAgQIECAQFsEVq1aFUOGDIn58+e35fRmz2lqaoo5c+ZEv379mn3egwQIECBAgAABAgQI5BYQxsht5AwCBKpXQCijeudGzwgQIECAAAECBAgQIECAAIEKCyxdujQGDRoUy5Yty7sn/fv3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Z84sqI7mCp911lnRoUOHWLduXXNP53xMKCMnkRMIECBAgAABAnkLVCKMseWWW8ZbK2MIY+Q9dQoSINCAAh3WZ44GHLchEyBAgAABAgQIECBAgAABAgSqQmDVqlUxZMiQmD9/ft79aWpqijlz5kS/fv3yrmPDgocffnhMnTp1w4fa/XWPHj1i+fLl0alTp3aXVYAAAQIECBAgQKB5gRTGmDRpUtx3333Nn1CCR1MY46tf/Wqcc845VsUoga8qCRCofwGhjPqfYyMkQIAAAQIECBAgQIAAAQIEqlxg6dKlMWjQoFi2bFnePe3fv3/Mnj07evbsmXcdqWAKdixevLigOt4qvGjRoth5553f+tZnAgQIECBAgACBPAWEMfKEU4wAAQJVIGD7kiqYBF0gQIAAAQIECBAgQIAAAQIEGlugb9++cccdd0TXrl3zhli4cGEcd9xx8eabb+ZdR69evYoWyEidsIVJ3lOhIAECBAgQIEAgK1CpbUr++Z//OZ588sm44IILrI7hWiRAgECBAkIZBQIqToAAAQIECBAgQIAAAQIECBAohsDgwYPj+uuvL6iqadOmxdixY/Oqo3PnzrFy5cq8yrZUSCijJRmPEyBAgAABAgRaF6hUGOOKK66ItNqZMEbr8+NZAgQItEdAKKM9Ws4lQIAAAQIECBAgQIAAAQIECJRQ4IQTTojx48cX1MJ3v/vduOaaa9pcx1lnnRUdOnSIdevWtblMW08s1jYobW3PeQQIECBAgACBWheoRBhjiy22iLfCGBdeeGF079691hn1nwABAlUl0GF95qiqHukMAQIECBAgQIAAAQIECBAgQKDBBT760Y/GbbfdlrdCp06d4u67744RI0a0Wsfhhx8eU6dObfWcQp4cNWpUTJkypZAqlCVAgAABAgQINIRACmNMmjQp7rvvvrKNN4UxvvrVr8aXvvQlQYyyqWuIAIFGFBDKaMRZN2YCBAgQIECAAAECBAgQIECgqgVWr14dBx10UMybNy/vfvbs2TNmz54d/fv3b7aOPffcM0q9vchOO+0UVstolt+DBAgQIECAAIGswP333x8XXXRRpM/lOlIY4ytf+Uqcc845whjlQtcOAQINLSCU0dDTb/AECBAgQIAAAQIECBAgQIBAtQo8++yzMWjQoEif8z369esXc+bMiaampndVseWWW8aLL774rsdK8U1a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AAECBAgQIECAAIHiC1x99dXRuXNngYxmaJ9//vlYsWJFM894iAABAgQIEGgEgRTGOPLII+PAAw8sWyAjhTEuu+yyWLJkSUyYMCEEMhrhSjNGAgQIVI+AUEb1zIWeECBAgAABAgQIECBAgAABAnUicMghh8T48eNjwIABdTKi4g7DahnF9VQbAQIECBCoBQFhjFqYJX0kQIAAgVIIdFifOUpRsToJECBAgAABAgQIECBAgAABAgQiUgDhl7/8Zdx0000xf/58JBmB6667Lk477TQWBAgQIECAQAMI2KakASbZEAkQIECgVQErZbTK40kCBAgQIECAAAECBAgQIECAQGECe+yxR0ycODH+8pe/xF//+te45JJLYrfddius0hovvXjx4hofge4TIECAAAECuQT+8Ic/xFFHHWWbklxQnidAgACBuhewUkbdT7EBEiBAgAABAgQIECBAgAABAtUo8Oc//zm7ekZaQePRRx+txi6WrE8nnXRS3HjjjSWrX8UECBAgQIBA5QRSGCNt4zZlypSydaJ3794xbty47EePHj3K1q6GCBAgQIBAWwSEMtqi5BwCBAgQIECAAAECBAgQIECAQAkFGi2g8cEPfjDmzZtXQlFVEyBAgAABAuUWEMYot7j2CBAgQKBWBIQyamWm9JMAAQIECBAgQIAAAQIECBBoCIF0Q+Pmm2/OrqJRr9t8pHewLl++PDp16tQQc2qQBAgQIECgngWEMep5do2NAAECBIohIJRRDEV1ECBAgAABAgQIECBAgAABAgRKIFDPAY1FixbFzjvvXAI1VRIgQIAAAQLlEBDGKIeyNggQIECgHgSEMuphFo2BAAECBAgQIECAAAECBAgQqHuBegto/OY3v4kjjzyy7ufNAAkQIECAQL0JpN9JJkyYEOlnebmO3r17x7hx47IfacUtBwECBAgQqCUBoYxami19JUCAAAECBAgQIECAAAECBAhkBH7/+99ntze55ZZb4qmnnqpJk3/913+NsWPH1mTfdZoAAQIECDSigDBGI866MRMgQIBAMQSEMoqhqA4CBAgQIECAAAECBAgQIECAQAUE1q9fHw888EBNBjTOPffc+M53vlMBNU0SIECAAAEC7REQxmiPlnMJECBAgMB7BYQy3mviEQIECBAgQIAAAQIECBAgQIBAzQnUWkBj1KhRMWXKlJpz1mECBAgQINAoAsIYjTLTxkmAAAECpRYQyii1sPoJECBAgAABAgQIECBAgAABAmUWSAGNGTNmZFfQ+O///u947rnnytyD3M3ttNNOsXjx4twnOoMAAQIECBAoq4AwRlm5NUaAAAECDSAglNEAk2yIBAgQIECAAAECBAgQIECAQOMKvPnmm3H//fdXXUCjU6dOsXz58ujRo0fjTo6REyBAgACBKhJIYYyJEyfGnXfeWbZe9e7dO8aNG5f98DtB2dg1RIAAAQJlFhDKKDO45ggQIECAAAECBAgQIECAAAEClRKotoDG3LlzY+DAgZXi0C4BAgQIECCQERDGcBkQIECAAIHSCghllNZX7QQIECBAgAABAgQIECBAgACBqhSohoDGL37xizj55JOr0kenCBAgQIBAvQsIY9T7DBsfAQIECFSLQMdq6Yh+ECBAgAABAgQIECBAgAABAgQIlE+gY8eOcfDBB8f3vve9ePrpp+Puu++OM844I7bccsuydWLBggVla0tDBAgQIECAwP8JpDDGMcccE4MHDy7bViVNTU1x2WWXxZIlS2LChAm2L3MxEiBAgEBDCVgpo6Gm22AJECBAgAABAgQIECBAgAABAq0LrFu3LqZNmxY33XRT/OpXv4oXXnih9QIFPHvqqafGf/3XfxVQg6IECBAgQIBAWwVSGGPSpElxxx13tLVIweelMMa4ceNi7NixghgFa6qAAAECBGpVQCijVmdOvwkQIECAAAECBAgQIECAAAECJRYodUBjyJAh8cADD5R4FKonQIAAAQKNLSCM0djzb/QECBAgUHkBoYzKz4EeECBAgAABAgQIECBAgAABAgSqXiAFNH77299mV9CYPHlyrFixouA+9+rVqyj1FNwRFRAgQIAAgToUEMaow0k1JAIECBCoSQGhjJqcNp0mQIAAAQIECBAgQIAAAQIECFROYO3atTF16tSiBDSeffbZ2GabbSo3GC0TIECAAIE6ExDGqLMJNRwCBAgQqHkBoYyan0IDIECAAAECBAgQIECAAAECBAhUTqDQgMa0adPikEMOqdwAtEyAAAECBOpEYO7cuTF+/Pi44447yjaipqamGDduXIwdOzZ69OhRtnY1RIAAAQIEaklAKKOWZktfCRAgQIAAAQIECBAgQIAAAQJVLJACGr/5zW+yK2jcdttt8fLLL+fs7Q9+8IM488wzc57nBAIECBAgQKB5gRTGmDBhQvz6179u/oQSPCqMUQJUVRIgQIBA3Qp0rtuRGRgBAgQIECBAgAABAgQIECBAgEBZBbp06RKjR4/OfqxZsybuuuuunAGNBQsWlLWPGiNAgAABAvUiIIxRLzNpHAQIECBQ7wJWyqj3GTY+AgQIECBAgAABAgQIECBAgECFBVoLaIwaNSqmTJlS4R5qngABAgQI1I6AMEbtzJWeEiBAgACBJCCU4TogQIAAAQIECBAgQIAAAQIECBAom0AKaNx+++3ZFTTSnvd9+vSJxYsXl619DREgQIAAgVoVEMao1ZnTbwIECBBodAGhjEa/AoyfAAECBAgQIECAAAECBAgQIFAhgVdffTXuvPPOOP744yvUA80SIECAAIHqFxDGqP450kMCBAgQINCagFBGazqeI0CAAAECBAgQIECAAAECBAgQIECAAAECBAhUQCCFMSZOnJhdYapczTc1NcW4ceNi7Nix0aNHj3I1qx0CBAgQIFDXAp3renQGR4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoIQFhjBqaLF0lQIAAAQJtEBDKaAOSUwgQIECAAAECBAgQIECAAAECBAgQIECAAAECpRQQxiilrroJECBAgEDlBIQyKmevZQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKDBBYQxGvwCMHwCBAgQqHsBoYy6n2IDJECAAAECBAgQIECAAAECBAgQIECAAAECBKpNIIUxJk2aFLfddlvZutbU1BTjxo2LsWPHRo8ePcrWroYIECBAgEAjCwhlNPLsGzsBAgQIECBAgAABAgQIECBAgAABAgQIECBQVgFhjLJya4wAAQIECFRcQCij4lOgAwQIECBAgAABAgQIECBAgAABAgQIECBAgEC9Cwhj1PsMGx8BAgQIEGheQCijeRePEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQKFhDGKJhQBQQIECBAoKYFhDJqevp0ngABAgQIECBAgAABAgQIECBAgAABAgQIEKhGAWGMapwVfSJAgAABAuUXEMoov7kWCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAgToVSGGMyy67LG699dayjbCpqSnOO++8OPfcc6NHjx5la1dDBAgQIECAQG4BoYzcRs4gQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECLQqIIzRKo8n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uK+tDHrzIzl16qytVQOyXL16taVSZeMAHLXTRdunesj0ab8YBh4oSHXUwIrl6+TZZ3paAoHUensyqffB8A/GStNGj1sCTH6Z+7uo3wlHk7px+vpr70uXjn3FloCPp9q2crRL6iPgMoGq1Soatj13ziJLwJthwZsKnD7939/BVg93kg3rt9yUa/zyr5XrZNu2XcYFfaBENYPjwVRwV3vts3OSFkzozH8ffYCGISKAAAIIIICAjwqE+Oi4GTYCCCCAAAIIIIAAAggggAACCASYwPJla0Q96txeQx57/EF58KH7JCoqn8MKiYlJMvOnefLF55PElhvjUfkjpV37xxzq97luz4i66X3o4FHDdmbOmCdztR0tmt5zl7TQdm2oUauqlC9fWoKCbPuujZrf7l0xsnXrTlm2dLXlmBIz3742HGAAFHjuubby5hvDDWcafy1B3nt3lIwfN1maNW8s9erXkVKliktU/nxaoEaKnDsbqwXBnLHsgLJ61fpsgxTKlislb7/TX4a9N1oOHzpm2K8zC6SkpIg62kQ9VCpduoTU1N5z5bQxldKeFywYpe30klfy5MktoWGhEhISIqmpqZKYkCgJCUly/nysHDlyQntvH5G12hEDJ46fsnl4De66Q+rYeSyQzZ24qeA32tET6ua8u5LaWWDosFclODjYXV0GZD/Va1SWAgWiDAOWVMDbJm13mOe6t5OKFctZtUpLS5P9MYdlm/bZvFT7bF6zeqPV47rUzkxjxg6T1wd+IHt277falrqodj/q3KGv3P9AU6lQoayEab+fKj3UspkUL17U8txX/qdGzSry28KlusO9evWajPj4S8sjIiKPZQeqrO//YsUKy5Qfx+q2QSYCCCCAAAIIIOAuAYIy3CVNPwgggAACCCCAAAIIIIAAAggg4BSBrVt2iHoMHTJKqmnfWq6v3citWrWCVNBufJUpXVLUThR6Se1QcOTwcdmzZ79lV4wlS1aJuplua+rTt5vh8SNGbYWGhsqno4dIu6dfsmlXBXU8xtIlqy0P1Xbu3LksN/ryF4iUiIgIyZs3j3YDLkySkpK0R7LExyfI2bPntYcWBKDt4KDmTLJf4JFH75exn38najcMW9K5c7Hy04xfLQ9bymcto24qjhg52BIIcffddd0elJF1LOq5OkpBPVydQkKCZfA7L7u6G7e1v3//YVEPd6YXXupk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oUoCBAgQIFAQAaGMgiyEYRAgQIAAAQIECBAgQIBANoFZs7ZKXfjoo0tS1yggQIAAAQIECBAgQCC7wOLF6X8H33rOltlvqJIAAQIECBREQCijIAthGAQIECBAgAABAgQIECCQTWD27NmpC4UyUpMpIECAAAECBAgQIDAmgSw7ZcycKZQxJnTFBAgQIFAIAaGMQiyDQRAgQIAAAQIECBAgQIBAVoHttts6demjix6PtWvXpq5TQIAAAQIECBAgQIBAeoHOzlWxfMXK1IWzZ89JXaOAAAECBAgUTUAoo2grYjwECBAgQIAAAQIECBAgkEogy+NLuvt645mnl6e6j8YECBAgQIAAAQIECGQTWDQYik57bDZlSnR0NKUt054AAQIECBROQCijcEtiQAQIECBAgAABAgQIECCQRqBUKsU2W89IUzLUdvHi9P8wnPomCggQIECAAAECBAgQiCyPLtl6a48u8dUhQIAAgdoQEMqojXU0CwIECBAgQIAAAQIECNS1wOzZs1PP/49//H+paxQQIECAAAECBAgQIJBe4P77H0xdNGvbrVLXKCBAgAABAkUUEMoo4qoYEwECBAgQIECAAAECBAikEpi93dap2ieN77jj96lrFBAgQIAAAQIECBAgkF5g4e/uTl201VZCGanRFBAgQIBAIQWEMgq5LAZFgAABAgQIECBAgAABAmkEdtxx+zTNh9re/ru7UtcoIECAAAECBAgQIEAgnUBfX38sXPiHdEWDrV/2D9ulrlFAgAABAgSKKCCUUcRVMSYCBAgQIECAAAECBAgQSCWwxx5zU7VPGi99enk8+uiS1HUKCBAgQIAAAQIECBAYvcDdd90XfdE/+oL/aTl33i6paxQQIECAAIEiCghlFHFVjIkAAQIECBAgQIAAAQIEUglss83M2GzKlFQ1SeM7MmyjnPomCggQIECAAAECBAjUscAdt6d/bOCLXrRtdHQ01bGaqRMgQIBALQkIZdTSapoLAQIECBAgQIAAAQIE6lhg3l67pp59ln8gTn0TBQQIECBAgAABAgTqWOD2DKGM3XZ7WR2LmToBAgQI1JqAUEatraj5ECBAgAABAgQIECBAoE4FsjzC5I7b76pTLdMmQIAAAQIECBAgkL/AwMDA4O506XfK2OVlL81/cO5AgAABAgTGSUAoY5yg3YYAAQIECBAgQIAAAQIE8hXYffe5qW/w0KLHY9Gix1LXKSBAgAABAgQIECBAYGSBZGe6zu6ukRuu12KXPV6+3hk/EiBAgACB6hUQyqjetTNyAgQIECBAgAABAgQIEFhHYPvtt4vJrW3rnBndx2t+eMPoGmpFgAABAgQIECBAgEAqgR9d89NU7ZPG287aKqZN60hdp4AAAQIECBRVQCijqCtjXAQIECBAgAABAgQIECCQSqChoSH23DP9X9T96Ifp/6E41cA0JkCAAAECBAgQIFCHAr29fXHtT36ReuY77+rRJanRFBAgQIBAoQWEMgq9PAZHgAABAgQIECBAgAABAmkE9tnvlWmaD7V95LHH4/d335+6TgEBAgQIECBAgAABAsML/Orm22JFZ+fwDYa5su8r9hzmitMECBAgQKA6BYQyqnPdjJoAAQIECBAgQIAAAQIENiJw8MEHRKlU2siVTZ+65hqPMNm0kKsECBAgQIAAAQIE0glck+HRJR3NLbHXPrumu5HWBAgQIECg4AJCGQVfIMMjQIAAAQIECBAgQIAAgdELTJs2OfZ95bzRF/xPy2t//LPo7+9PXaeAAAECBAgQIECAAIENBdas6Ymf3XDzhhdGOLPfQXtHU1PTCK1cJkCAAAEC1SUglFFd62W0BAgQIECAAAECBAgQIDCCwD+94cARWmx4+enlf41bf3PHhhecIUCAAAECBAgQIEAgtcD1190Y3X29qesOPHDf1DWVKJg0aVIkLwcBAgQIEMhDQCgjD1V9EiBAgAABAgQIECBAgMCECST/kNvW2Jz6/t/65vdT1yggQIAAAQIECBAgQGBDga9/9TsbnhzhzNSOybHnnnNHaJXPZYGMfFz1SoAAAQJ/ExDK8E0gQIAAAQIECBAgQIAAgZoSaGtri1cduHfqOf38xlviwQcfSV2ngAABAgQIECBAgACBvwv85pY74r4/Pfj3E6P8dNAhr4pSqTTK1poRIECAAIHqERDKqJ61MlICBAgQIECAAAECBAgQGKXAGw5J/wiTpOuLLvjaKO+gGQECBAgQIECAAAECGxP40oVf39jpEc8ddNCrR2yjAQECBAgQqEYBoYxqXDVjJkCAAAECBAgQIECAAIFNCuz/2r1j+rTNNtlmYxd/cv2N8cjDizd2yTkCBAgQIECAAAECBEYQuHPhvXHb7XeP0GrDy3Nmbhlz5+6w4YVxOuPxJeME7TYECBCoUwGhjDpdeNMmQIAAAQIECBAgQIBArQu85dgjMk3x4osvy1SniAABAgQIECBAgEC9C1x4wdczEbzpzYdnqlNEgAABAgSqQUAooxpWyRgJECBAgAABAgQIECBAILXAUcccHm2NzanrfvTDG2LJ40+mrlNAgAABAgQIECBAoJ4F/vCHB+LmW36bmmBqx+Q49NADUtdVssBOGZXU1BcBAgQIrC8glLG+iJ8JECBAgAABAgQIECBAoCYEpk2bHEcec2jqufQNVpz76QtT1ykgQIAAAQIECBAgUM8CH/vo5zJN/5j5h0Vzc/owdaabDVPU0OD/LhuGxmkCBAgQqICA/8pUAFEXBAgQIECAAAECBAgQIFBMgePf/KbI8g+s191wU9x268JiTsqoCBAgQIAAAQIECBRM4AdXXRcL774v9aiam0vxxje9IXVdpQuy/G+GSo9BfwQIECBQuwJCGbW7tmZGgAABAgQIECBAgACBuhfYetsZceCB+2ZyOOesc6OnpydTrSICBAgQIECAAAEC9SKwcmVnnPupL2aa7mGHHRzPe157ptpKFiWPL/EIk0qK6osAAQIE1hUQylhXw2cCBAgQIECAAAECBAgQqDmBBQvmZ5rToseeiC9ddFmmWkUECBAgQIAAAQIE6kXgs+ddHMtXrEw93WR3irfOPzx1XR4FdsrIQ1WfBAgQIFAWEMooS3gnQIAAAQIECBAgQIAAgZoU2Gmnl8S8eTtlmtslX/mPePTRJZlqFREgQIAAAQIECBCodYH77v1TXP6dqzNNc/999ooXzH5BptpKFtklo5Ka+iJAgACBjQkIZWxMxTkCBAgQIECAAAECBAgQqCmB005bkGk+fX39cc6Hzs1Uq4gAAQIECBAgQIBALQv09/fHB8/8VKYplkqlOPHUYzPVVrpIKKPSovojQIAAgfUFhDLWF/EzAQIECBAgQIAAAQIECNScwB57zI2DD94/07x+89s741uXXZmpVhEBAgQIECBAgACBWhW48IKvx58eejjT9I488pCYPXvrTLWVLvLokkqL6o8AAQIE1heYtHbwWP+knwkQIECAAAECBAgQIECAQK0JLHtiWey3/1GxZqAv9dQaG0tx+X9eFDvtvGPqWgUECBAgQIAAAQIEak3gt7fdGW859rRM05rS3hFX/vBrMXVqW6b6She1tLRE8kp2zHAQIECA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a3IECBAgQIAAAQLPFSjviCGI8VwXPxEgQIAAgawCQhlZ5dQRIECAAAECBAgQIECAAIEaFijvlJG8J484WfdVvlbD0zc1AgQIECBAgEBdCJR3w0h2xCi/yjthlN/rAsIkCRAgQIBAjgJCGTni6poAAQIECBAgQIAAAQIECNSKQDmIkbwPDAwMhTSS9+RVPlduUytzNg8CBAgQIECAQK0IJAGL5NXQ0PDsKwlhJD+Xwxfl91qZs3kQIECAAIGiCAhlFGUljIMAAQIECBAgQIAAAQIECFSRwPoBjPLP5aBGOaxRPp+8r/+qoukaKgECBAgQIECgcALloEX5PRlg+XPyXg5grHtu3Ukk5x0ECBAgQIBA/gJCGfkbuwMBAgQIECBAgAABAgQIEKgrgXIQY7hJj3R9uDrnCRAgQIAAAQIEniswUrBipOvP7c1PBAgQIECAQB4CQhl5qOqTAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqHuBhroXAECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQyEFAKCMHVF0SIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBIQyfAcIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAjkICGXkgKpLAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgIBQhu8AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQCAHAaGMHFB1SYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQyvAdIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjkICCUkQOqLgkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECQhm+AwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBHASEMnJA1SUBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAQCjDd4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkIOAUEYOqLokQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECAhl+A4QIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBHIQEMrIAVWXBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAGhDN8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAOAkIZOaDqkgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAglOE7QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDIQUAoIwdUXRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEhDJ8BwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECOQgIZeSAqksCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAgFCG7wABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAIAcBoYwcUHVJgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBDK8B0gQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECOQgIJSRA6ouCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJCGb4DBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEcBIQyckDVJQECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBAKMN3gAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQg4BQRg6ouiRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQICGX4DhAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEchAQysgBVZcECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAaEM3wECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQA4C/w3JnqM7o6v5jgAAAABJRU5ErkJggg==)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"OcxSqxE7Vkx0\"\n   },\n   \"source\": [\n    \"### 4.1 Define custom events\\n\",\n    \"\\n\",\n    \"The first thing that we need to do is to define the custom event that we will us throughout our framework.\\n\",\n    \"\\n\",\n    \"We can do it by subclassing the general `Event` classes that the `llama-index-workflows` package provides us with\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"5wJ_xQ-dVacH\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Literal\\n\",\n    \"\\n\",\n    \"from workflows.events import (\\n\",\n    \"    Event,\\n\",\n    \"    HumanResponseEvent,\\n\",\n    \"    InputRequiredEvent,\\n\",\n    \"    StartEvent,\\n\",\n    \"    StopEvent,\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class EmailEvent(StartEvent):\\n\",\n    \"    \\\"\\\"\\\"Triggers the workflow when an e-mail is received.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    email_content: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InformationExtractionEvent(Event):\\n\",\n    \"    \\\"\\\"\\\"Event that contains the extracted information from the email.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    sender_details: str\\n\",\n    \"    support_category: Literal[\\\"technical\\\", \\\"sales\\\", \\\"general_information\\\"]\\n\",\n    \"    support_questions: list[str]\\n\",\n    \"    extra_information: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class QuestionEvent(Event):\\n\",\n    \"    \\\"\\\"\\\"Event containing one or more questions to retrieve information from the company database.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    questions: list[str]\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class RetrievalEvent(Event):\\n\",\n    \"    \\\"\\\"\\\"Event that contains the retrieved information\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    information: dict[str, str]\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class CandidateEmailEvent(InputRequiredEvent):\\n\",\n    \"    \\\"\\\"\\\"Event where we generate a candidate email to send back to the user in response to their support questions\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    candidate_email: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class HumanFeedbackEvent(HumanResponseEvent):\\n\",\n    \"    \\\"\\\"\\\"Ask for human feedback.\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    approved: bool\\n\",\n    \"    feedback: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class SendEmailEvent(StopEvent):\\n\",\n    \"    \\\"\\\"\\\"Event where the agent sends an email\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"    email_content: str\\n\",\n    \"    email_successfully_sent: bool\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ProgressEvent(Event):\\n\",\n    \"    msg: str\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"gzoPnxIpn_JJ\"\n   },\n   \"source\": [\n    \"### 4.2 Define custom resources\\n\",\n    \"\\n\",\n    \"[Resources](https://docs.llamaindex.ai/en/stable/understanding/workflows/resources/) are a way of performing dependency injection in workflow steps.\\n\",\n    \"\\n\",\n    \"We will need three main resources:\\n\",\n    \"\\n\",\n    \"1. An extraction agent to perform information extraction from the email\\n\",\n    \"2. An LLM to provide us with questions to ask to the company database to retrieve information\\n\",\n    \"3. The company database client to perform information retrieval\\n\",\n    \"\\n\",\n    \"Let's first build our extraction agent:\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"vWZxGUpVOFd7\",\n    \"outputId\": \"be124c7d-0f15-47b4-a36d-abe7c272d5be\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"··········\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from getpass import getpass\\n\",\n    \"\\n\",\n    \"os.environ[\\\"OPENAI_API_KEY\\\"] = getpass()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"ChuquqUqgw0M\",\n    \"outputId\": \"ae7dcb07-97cb-46d0-ccbb-85bd6e1e88d1\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"··········\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"os.environ[\\\"LLAMACLOUD_API_KEY\\\"] = getpass()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"x0di0zk4pUwY\"\n   },\n   \"source\": [\n    \"We need to specify a schema for the extraction first:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"FebjynuqiZMu\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from pydantic import BaseModel, Field\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class SenderDetails(BaseModel):\\n\",\n    \"    name: str = Field(description=\\\"Name of the sender seeking support\\\")\\n\",\n    \"    email_address: str = Field(description=\\\"Email address of the sender\\\")\\n\",\n    \"    company: str | None = Field(\\n\",\n    \"        descrption=\\\"Company for which the sender works\\\", default=None\\n\",\n    \"    )\\n\",\n    \"    role: str | None = Field(descrption=\\\"Job role of the sender\\\", default=None)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class SupportEmailInformation(BaseModel):\\n\",\n    \"    sender_details: SenderDetails\\n\",\n    \"    support_category: Literal[\\\"technical\\\", \\\"sales\\\", \\\"general_information\\\"]\\n\",\n    \"    support_questions: list[str]\\n\",\n    \"    extra_information: str\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"KnYLPpseqMrp\"\n   },\n   \"source\": [\n    \"Then we create the extraction agent using `llama-cloud-services`, specifically [LlamaExtract](https://www.llamaindex.ai/llamaextract), our best-in-class platform for document extraction:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"2Haakfd9qRaE\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_cloud_services import LlamaExtract\\n\",\n    \"\\n\",\n    \"client = LlamaExtract(api_key=os.getenv(\\\"LLAMACLOUD_API_KEY\\\"))\\n\",\n    \"agent = client.create_agent(\\n\",\n    \"    name=\\\"EmailSupportAgent\\\", data_schema=SupportEmailInformation\\n\",\n    \")\\n\",\n    \"os.environ[\\\"EXTRACT_AGENT_ID\\\"] = agent.id\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"xAD-Dcl5rN7d\"\n   },\n   \"source\": [\n    \"Now that we created the agent, let's create a company database that we can query. We will do it using [LlamaCloud Index](https://docs.llamaindex.ai/en/stable/module_guides/indexing/llama_cloud_index/), a fully automated ingestion pipeline for our company documents.\\n\",\n    \"\\n\",\n    \"Let's imagine that we are a company called YourBestSoftware, and we sell productivity tools for enterprise solutions.\\n\",\n    \"\\n\",\n    \"Let's get the documents related to techinical, sales and general support for our company:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"up4K3g7Tq6uP\",\n    \"outputId\": \"82793183-09f6-4c65-ed44-99363ea49432\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"mkdir: cannot create directory ‘data’: File exists\\n\",\n      \"  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\\n\",\n      \"                                 Dload  Upload   Total   Spent    Left  Speed\\n\",\n      \"100 2564k  100 2564k    0     0  5475k      0 --:--:-- --:--:-- --:--:-- 5467k\\n\",\n      \"  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\\n\",\n      \"                                 Dload  Upload   Total   Spent    Left  Speed\\n\",\n      \"100 4108k  100 4108k    0     0  7445k      0 --:--:-- --:--:-- --:--:-- 7455k\\n\",\n      \"  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\\n\",\n      \"                                 Dload  Upload   Total   Spent    Left  Speed\\n\",\n      \"100 4526k  100 4526k    0     0   9.9M      0 --:--:-- --:--:-- --:--:--  9.9M\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"! mkdir data\\n\",\n    \"! curl https://raw.githubusercontent.com/run-llama/workflows-observability-support-data/main/data/support/yourbestsoftware_technical.pdf > data/yourbestsoftware_technical.pdf\\n\",\n    \"! curl https://raw.githubusercontent.com/run-llama/workflows-observability-support-data/main/data/support/yourbestsoftware_sales.pdf > data/yourbestsoftware_sales.pdf\\n\",\n    \"! curl https://raw.githubusercontent.com/run-llama/workflows-observability-support-data/main/data/support/yourbestsoftware_general.pdf > data/yourbestsoftware_general.pdf\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"5UZLnqEbxfgk\"\n   },\n   \"source\": [\n    \"Now let's upload them to a LlamaCloud Index:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"c-DRtTC2xefR\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_cloud import (\\n\",\n    \"    AdvancedModeTransformConfigChunkingConfig_Sentence,\\n\",\n    \"    AdvancedModeTransformConfigSegmentationConfig_Page,\\n\",\n    \"    PipelineCreateEmbeddingConfig_OpenaiEmbedding,\\n\",\n    \"    PipelineTransformConfig_Advanced,\\n\",\n    \")\\n\",\n    \"from llama_index.embeddings.openai import OpenAIEmbedding\\n\",\n    \"from llama_index.indices.managed.llama_cloud import LlamaCloudIndex\\n\",\n    \"\\n\",\n    \"embed_model = OpenAIEmbedding(\\n\",\n    \"    model=\\\"text-embedding-3-small\\\", api_key=os.getenv(\\\"OPENAI_API_KEY\\\")\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"index = await LlamaCloudIndex.acreate_index(\\n\",\n    \"    api_key=os.getenv(\\\"LLAMACLOUD_API_KEY\\\"),\\n\",\n    \"    name=\\\"observability-demo-index\\\",\\n\",\n    \"    embedding_config=PipelineCreateEmbeddingConfig_OpenaiEmbedding(\\n\",\n    \"        type=\\\"OPENAI_EMBEDDING\\\",\\n\",\n    \"        component=embed_model,\\n\",\n    \"    ),\\n\",\n    \"    transform_config=PipelineTransformConfig_Advanced(\\n\",\n    \"        mode=\\\"advanced\\\",\\n\",\n    \"        segmentation_config=AdvancedModeTransformConfigSegmentationConfig_Page(\\n\",\n    \"            mode=\\\"page\\\"\\n\",\n    \"        ),\\n\",\n    \"        chunking_config=AdvancedModeTransformConfigChunkingConfig_Sentence(\\n\",\n    \"            mode=\\\"sentence\\\",\\n\",\n    \"            chunk_size=1024,\\n\",\n    \"            chunk_overlap=200,\\n\",\n    \"            separator=\\\"<whitespace>\\\",\\n\",\n    \"            paragraph_separator=\\\"\\\\n\\\\n\\\",\\n\",\n    \"        ),\\n\",\n    \"    ),\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"uikFxEoiyXUF\"\n   },\n   \"source\": [\n    \"Now let's upload our files to this pipeline we just created:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true,\n    \"id\": \"nY1STkM_ycqv\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"files: list[dict] = []\\n\",\n    \"fls = [os.path.join(\\\"data\\\", f) for f in os.listdir(\\\"data/\\\")]\\n\",\n    \"\\n\",\n    \"file_ids = []\\n\",\n    \"for fl in fls:\\n\",\n    \"    file_ids.append(await index.aupload_file(fl, wait_for_ingestion=False))\\n\",\n    \"\\n\",\n    \"await index.wait_for_completion(file_ids=file_ids)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"ilPjdAXlzhEZ\"\n   },\n   \"source\": [\n    \"It will take some minutes for our pipeline to load and process all the files, so in the meantime we define the functions that would work as resources:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"oRiKpN1QzrD3\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_cloud_services.extract.extract import ExtractionAgent\\n\",\n    \"from llama_index.core.llms import LLM\\n\",\n    \"from llama_index.core.llms.structured_llm import StructuredLLM\\n\",\n    \"from llama_index.core.query_engine import BaseQueryEngine\\n\",\n    \"from llama_index.indices.managed.llama_cloud import LlamaCloudIndex\\n\",\n    \"from llama_index.llms.openai import OpenAIResponses\\n\",\n    \"from pydantic import BaseModel, Field\\n\",\n    \"\\n\",\n    \"llm = OpenAIResponses(model=\\\"gpt-4.1\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class SupportQuestions(BaseModel):\\n\",\n    \"    questions: list[str] = Field(\\n\",\n    \"        description=\\\"Support questions to ask within the company database to retrieve the most information about the users' requests\\\"\\n\",\n    \"    )\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"# we define a structured LLM that would give us the support questions to ask to the company database\\n\",\n    \"llm_struct = llm.as_structured_llm(SupportQuestions)\\n\",\n    \"\\n\",\n    \"query_engine = index.as_query_engine(llm=llm)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_extract_agent(*args, **kwargs) -> ExtractionAgent:\\n\",\n    \"    return agent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_company_database(*args, **kwargs) -> BaseQueryEngine:\\n\",\n    \"    return query_engine\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_llm(*args, **kwargs) -> StructuredLLM:\\n\",\n    \"    return llm_struct\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_llm_for_email(*args, **kwargs) -> LLM:\\n\",\n    \"    return llm\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"KwxmTX030oHH\"\n   },\n   \"source\": [\n    \"### 4.3 Create the workflow\\n\",\n    \"\\n\",\n    \"Finally, after defining events and resources, we can create our workflow!\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"rkDfEt76NI1V\"\n   },\n   \"source\": [\n    \"First we define a state where we will be storing some values during the workflow execution for persistency across steps:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"FHAqmHu4_ZV4\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"class WorkflowState(BaseModel):\\n\",\n    \"    email_text: str = \\\"\\\"\\n\",\n    \"    candidate_email: str = \\\"\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"LvD5ahJj1r-L\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import uuid\\n\",\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from llama_cloud_services.extract import SourceText\\n\",\n    \"from llama_index.core.llms import ChatMessage\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class EmailSupportWorkflow(Workflow):\\n\",\n    \"    # remember to use the step decorator!\\n\",\n    \"    @step\\n\",\n    \"    async def extract_email_information(\\n\",\n    \"        self,\\n\",\n    \"        event: EmailEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        extraction_agent: Annotated[ExtractionAgent, Resource(get_extract_agent)],\\n\",\n    \"    ) -> InformationExtractionEvent | SendEmailEvent:\\n\",\n    \"        extracted_content = await extraction_agent.aextract(\\n\",\n    \"            files=SourceText(\\n\",\n    \"                text_content=text, filename=f\\\"support_email_{str(uuid.uuid4())}.txt\\\"\\n\",\n    \"            )\\n\",\n    \"        )\\n\",\n    \"        if extracted_content:\\n\",\n    \"            extracted_data = SupportEmailInformation.model_validate(\\n\",\n    \"                extracted_content.data\\n\",\n    \"            )\\n\",\n    \"            ctx.write_event_to_stream(\\n\",\n    \"                ProgressEvent(msg=\\\"Information extracted successfully\\\")\\n\",\n    \"            )\\n\",\n    \"            return InformationExtractionEvent(\\n\",\n    \"                sender_details=extracted_data.sender_details.model_dump_json(),\\n\",\n    \"                support_category=extracted_data.support_category,\\n\",\n    \"                support_questions=extracted_data.support_questions,\\n\",\n    \"                extra_information=extracted_data.extra_information,\\n\",\n    \"            )\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Information extraction failed\\\"))\\n\",\n    \"\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.email_text = event.email_content\\n\",\n    \"\\n\",\n    \"        return SendEmailEvent(\\n\",\n    \"            email_content=\\\"\\\",\\n\",\n    \"            email_successfully_sent=False,\\n\",\n    \"            result=\\\"Failed to extract information, please re-run the workflow\\\",\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def generate_questions_to_ask(\\n\",\n    \"        self,\\n\",\n    \"        event: InformationExtractionEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        llm: Annotated[StructuredLLM, Resource(get_llm)],\\n\",\n    \"    ) -> QuestionEvent | SendEmailEvent:\\n\",\n    \"        response = await llm.achat(\\n\",\n    \"            messages=[\\n\",\n    \"                ChatMessage(\\n\",\n    \"                    role=\\\"user\\\",\\n\",\n    \"                    content=f\\\"Starting from this JSON representation of a support email:\\\\n\\\\n```json\\\\n{event.model_dump_json()}\\\\n```\\\\n\\\\nCan you please generate one or more support questions to ask to the company database to solve the users' problem?\\\",\\n\",\n    \"                )\\n\",\n    \"            ]\\n\",\n    \"        )\\n\",\n    \"        if response.message.content:\\n\",\n    \"            response_json = json.loads(response.message.content)\\n\",\n    \"            ctx.write_event_to_stream(\\n\",\n    \"                ProgressEvent(msg=\\\"Questions generated successfully\\\")\\n\",\n    \"            )\\n\",\n    \"            return QuestionEvent(questions=response_json[\\\"questions\\\"])\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Questions generation failed\\\"))\\n\",\n    \"        return SendEmailEvent(\\n\",\n    \"            email_content=\\\"\\\",\\n\",\n    \"            email_successfully_sent=False,\\n\",\n    \"            result=\\\"Failed to generate questions, please re-run the workflow\\\",\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def information_retrieval(\\n\",\n    \"        self,\\n\",\n    \"        event: QuestionEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        company_database: Annotated[BaseQueryEngine, Resource(get_company_database)],\\n\",\n    \"    ) -> RetrievalEvent:\\n\",\n    \"        q_and_a: dict[str, str] = {}\\n\",\n    \"        for question in event.questions:\\n\",\n    \"            response = await company_database.aquery(question)\\n\",\n    \"            q_and_a.update({question: response.response or \\\"\\\"})\\n\",\n    \"        ctx.write_event_to_stream(\\n\",\n    \"            ProgressEvent(msg=\\\"Answers to questions generated successfully\\\")\\n\",\n    \"        )\\n\",\n    \"        return RetrievalEvent(information=q_and_a)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def create_email_event(\\n\",\n    \"        self,\\n\",\n    \"        event: RetrievalEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        llm_email: Annotated[LLM, Resource(get_llm_for_email)],\\n\",\n    \"    ) -> CandidateEmailEvent:\\n\",\n    \"        response = await llm_email.achat(\\n\",\n    \"            messages=[\\n\",\n    \"                ChatMessage(\\n\",\n    \"                    role=\\\"user\\\",\\n\",\n    \"                    content=f\\\"Starting from this JSON representation of question-answer pairs for the support answers of a user:\\\\n\\\\n```json\\\\n{json.dumps(event.information, indent=4)}\\\\n```\\\\n\\\\nCan you please generate the text of the email to send back to the customer and answer their questions? Generate only the email.\\\",\\n\",\n    \"                )\\n\",\n    \"            ]\\n\",\n    \"        )\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Generated candidate email\\\"))\\n\",\n    \"\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.candidate_email = response.message.content\\n\",\n    \"\\n\",\n    \"        return CandidateEmailEvent(candidate_email=response.message.content)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def send_email_or_restart(\\n\",\n    \"        self, event: HumanFeedbackEvent, ctx: Context[WorkflowState]\\n\",\n    \"    ) -> SendEmailEvent | EmailEvent:\\n\",\n    \"        if event.approved:\\n\",\n    \"            state = await ctx.store.get_state()\\n\",\n    \"            return SendEmailEvent(\\n\",\n    \"                email_content=state.candidate_email,\\n\",\n    \"                email_successfully_sent=True,\\n\",\n    \"                result=\\\"Email successfully sent\\\",\\n\",\n    \"            )\\n\",\n    \"        else:\\n\",\n    \"            state = await ctx.store.get_state()\\n\",\n    \"            return EmailEvent(\\n\",\n    \"                email_content=state.email_text,\\n\",\n    \"            )\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"BVtoh_cvBCg3\"\n   },\n   \"source\": [\n    \"Now we will run the workflow as-is, an you will already see that it produces OpenTelemtry traces.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"k1yUKH2YBcYE\",\n    \"outputId\": \"2e0eee7a-ed0a-4ad3-fdfa-7c4929f11989\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\\n\",\n      \"                                 Dload  Upload   Total   Spent    Left  Speed\\n\",\n      \"\\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\\r100  1086  100  1086    0     0   5438      0 --:--:-- --:--:-- --:--:--  5457\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# first let's get some data\\n\",\n    \"! curl https://raw.githubusercontent.com/run-llama/workflows-observability-support-data/main/data/emails/general.txt > general.txt\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\",\n     \"height\": 256\n    },\n    \"id\": \"ZzlU5fUdBqQ3\",\n    \"outputId\": \"9622e6a2-3cd0-459b-fd68-954973ce1912\"\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"application/vnd.google.colaboratory.intrinsic+json\": {\n       \"type\": \"string\"\n      },\n      \"text/plain\": [\n       \"\\\"From: j.marino84@gmail.com  \\\\nTo: support@yourbestsoftware.com  \\\\nSubject: Request for General Assistance with Your Best Software Account and Features\\\\n\\\\nDear Support Team,\\\\n\\\\nI’m a fairly new user of Your Best Software and I was hoping to get some help understanding a few things related to my account and general use of the platform.\\\\n\\\\nI’m not encountering a specific error, but I’ve run into some confusion around how certain features are meant to work. In particular, I’d appreciate some guidance on:\\\\n\\\\n- Navigating between different areas of the dashboard (some settings and tools seem to be nested in ways I can’t always find easily)\\\\n- Understanding which features are included in my current subscription level\\\\n- General best practices for setting up my workspace to get the most out of the platform\\\\n\\\\nAny documentation, tips, or pointers to the right support resources would be very helpful. If there's someone I can speak with directly or a walkthrough available, that would be even better.\\\\n\\\\nThanks in advance for your help!\\\\n\\\\nBest regards,  \\\\nJamie Marino  \\\\nj.marino84@gmail.com\\\"\"\n      ]\n     },\n     \"execution_count\": 19,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"# Let's read these data\\n\",\n    \"\\n\",\n    \"with open(\\\"general.txt\\\", \\\"r\\\") as f:\\n\",\n    \"    text = f.read()\\n\",\n    \"text\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"4BgHaqhGCKUX\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"se = FileSpanExporter(file_path=\\\"workflow_traces.json\\\")\\n\",\n    \"\\n\",\n    \"instrumentor = LlamaIndexOpenTelemetry(\\n\",\n    \"    span_exporter=se,\\n\",\n    \"    service_name_or_resource=\\\"tracing.a.workflow.1\\\",\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"instrumentor.start_registering()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"cOYzx6hSBJ_J\",\n    \"outputId\": \"39d4e20c-a9d1-4a28-d409-35c213cd37f3\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Uploading files: 100%|██████████| 1/1 [00:00<00:00,  1.43it/s]\\n\",\n      \"Creating extraction jobs: 100%|██████████| 1/1 [00:01<00:00,  1.48s/it]\\n\",\n      \"Extracting files:   0%|          | 0/1 [00:00<?, ?it/s]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Writing 4 spans to workflow_traces.json\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Extracting files: 100%|██████████| 1/1 [00:04<00:00,  4.21s/it]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Information extracted successfully\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Writing 4 spans to workflow_traces.json\\n\",\n      \"Questions generated successfully\\n\",\n      \"Writing 11 spans to workflow_traces.json\\n\",\n      \"Writing 10 spans to workflow_traces.json\\n\",\n      \"Writing 10 spans to workflow_traces.json\\n\",\n      \"Answers to questions generated successfully\\n\",\n      \"Writing 18 spans to workflow_traces.json\\n\",\n      \"Generated candidate email\\n\",\n      \"Candidate email: Subject: Answers to Your Support Questions\\n\",\n      \"\\n\",\n      \"Dear [Customer Name],\\n\",\n      \"\\n\",\n      \"Thank you for reaching out with your questions. Please find detailed answers below to help you get the most out of your platform experience:\\n\",\n      \"\\n\",\n      \"**1. Documentation and Tutorials for New Users**  \\n\",\n      \"New users have access to a wide range of resources to help navigate the dashboard, including step-by-step Getting Started Guides, comprehensive user manuals, and feature documentation. We also offer a video library with over 200 on-demand training videos, interactive in-app tutorials, and live or on-demand webinars for detailed walkthroughs. Our knowledge base provides troubleshooting guides and integration instructions, while the community forum is available for sharing best practices and asking questions. For hands-on practice, sandbox accounts are also available.\\n\",\n      \"\\n\",\n      \"**2. Features Included in Your Current Subscription**  \\n\",\n      \"Your current subscription includes:\\n\",\n      \"- 24/7/365 support with a dedicated account manager\\n\",\n      \"- Priority support, custom integrations, on-site assistance, and training\\n\",\n      \"- Fast response times (4 hours for standard, 1 hour for critical issues)\\n\",\n      \"- Access to all support channels: portal, email, phone, emergency hotline, and live chat\\n\",\n      \"- Advanced workflow automation, enterprise integrations, and custom reporting (if using WorkflowMax Enterprise)\\n\",\n      \"- Automated daily incremental and weekly full data backups (365-day retention, 1-hour recovery)\\n\",\n      \"- AES-256 encryption at rest, TLS 1.3 in transit, and multi-factor authentication\\n\",\n      \"- Compliance with SOC 2 Type II, GDPR, HIPAA, and ISO 27001\\n\",\n      \"- 99.99% uptime guarantee and SLA credits for downtime\\n\",\n      \"- Mobile apps (iOS/Android), push notifications, in-app help, mobile chat, and offline resources\\n\",\n      \"- Participation in customer advocacy programs, feedback channels, and early access to new features\\n\",\n      \"- Dedicated account management, custom implementation, on-site training, regular health checks, and strategic planning\\n\",\n      \"- Full accessibility support (WCAG 2.1 AA, screen reader compatibility, keyboard navigation, alternative documentation formats)\\n\",\n      \"\\n\",\n      \"**3. Best Practices for Workspace Setup**  \\n\",\n      \"We provide best practice guides and industry-specific recommendations to help you set up your workspace for maximum efficiency. These include comprehensive user guides, downloadable workflow templates, and step-by-step video tutorials. Our knowledge base offers getting started guides, feature documentation, and integration instructions. You can also benefit from customer-shared tips and workflows in our community platform.\\n\",\n      \"\\n\",\n      \"**4. Scheduling a Walkthrough or Onboarding Assistance**  \\n\",\n      \"Yes, onboarding assistance is available. You will receive a welcome call within 24 hours of contract signing and dedicated support during your launch week. You can also schedule a walkthrough or speak directly with a support representative at any time via our main support line, live chat, or by contacting your customer success team.\\n\",\n      \"\\n\",\n      \"If you have any further questions or would like to schedule a walkthrough, please let us know. We’re here to help!\\n\",\n      \"\\n\",\n      \"Best regards,  \\n\",\n      \"[Your Name]  \\n\",\n      \"[Your Position]  \\n\",\n      \"[Company Name]  \\n\",\n      \"[Contact Information]\\n\",\n      \"Writing 2 spans to workflow_traces.json\\n\",\n      \"Approve? (y/<reason>): y\\n\",\n      \"Approved? y\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"wf = EmailSupportWorkflow(timeout=800)\\n\",\n    \"\\n\",\n    \"handler = wf.run(start_event=EmailEvent(email_content=text))\\n\",\n    \"async for ev in handler.stream_events():\\n\",\n    \"    if isinstance(ev, ProgressEvent):\\n\",\n    \"        print(ev.msg, flush=True)\\n\",\n    \"    elif isinstance(ev, CandidateEmailEvent):\\n\",\n    \"        print(f\\\"Candidate email: {ev.candidate_email}\\\", flush=True)\\n\",\n    \"        approved = input(\\\"Approve? (y/<reason>): \\\").strip().lower()\\n\",\n    \"        print(f\\\"Approved? {approved}\\\", flush=True)\\n\",\n    \"        if approved == \\\"y\\\":\\n\",\n    \"            handler.ctx.send_event(\\n\",\n    \"                HumanFeedbackEvent(approved=True, feedback=\\\"Approved\\\")\\n\",\n    \"            )\\n\",\n    \"        else:\\n\",\n    \"            handler.ctx.send_event(\\n\",\n    \"                HumanFeedbackEvent(approved=False, feedback=approved)\\n\",\n    \"            )\\n\",\n    \"\\n\",\n    \"result = await handler\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"H_2kI8U7Dm9b\",\n    \"outputId\": \"789d9e4b-1aa0-49de-9126-cfa1124a8827\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Subject: Answers to Your Support Questions\\n\",\n      \"\\n\",\n      \"Dear [Customer Name],\\n\",\n      \"\\n\",\n      \"Thank you for reaching out with your questions. Please find detailed answers below to help you get the most out of your platform experience:\\n\",\n      \"\\n\",\n      \"**1. Documentation and Tutorials for New Users**  \\n\",\n      \"New users have access to a wide range of resources to help navigate the dashboard, including step-by-step Getting Started Guides, comprehensive user manuals, and feature documentation. We also offer a video library with over 200 on-demand training videos, interactive in-app tutorials, and live or on-demand webinars for detailed walkthroughs. Our knowledge base provides troubleshooting guides and integration instructions, while the community forum is available for sharing best practices and asking questions. For hands-on practice, sandbox accounts are also available.\\n\",\n      \"\\n\",\n      \"**2. Features Included in Your Current Subscription**  \\n\",\n      \"Your current subscription includes:\\n\",\n      \"- 24/7/365 support with a dedicated account manager\\n\",\n      \"- Priority support, custom integrations, on-site assistance, and training\\n\",\n      \"- Fast response times (4 hours for standard, 1 hour for critical issues)\\n\",\n      \"- Access to all support channels: portal, email, phone, emergency hotline, and live chat\\n\",\n      \"- Advanced workflow automation, enterprise integrations, and custom reporting (if using WorkflowMax Enterprise)\\n\",\n      \"- Automated daily incremental and weekly full data backups (365-day retention, 1-hour recovery)\\n\",\n      \"- AES-256 encryption at rest, TLS 1.3 in transit, and multi-factor authentication\\n\",\n      \"- Compliance with SOC 2 Type II, GDPR, HIPAA, and ISO 27001\\n\",\n      \"- 99.99% uptime guarantee and SLA credits for downtime\\n\",\n      \"- Mobile apps (iOS/Android), push notifications, in-app help, mobile chat, and offline resources\\n\",\n      \"- Participation in customer advocacy programs, feedback channels, and early access to new features\\n\",\n      \"- Dedicated account management, custom implementation, on-site training, regular health checks, and strategic planning\\n\",\n      \"- Full accessibility support (WCAG 2.1 AA, screen reader compatibility, keyboard navigation, alternative documentation formats)\\n\",\n      \"\\n\",\n      \"**3. Best Practices for Workspace Setup**  \\n\",\n      \"We provide best practice guides and industry-specific recommendations to help you set up your workspace for maximum efficiency. These include comprehensive user guides, downloadable workflow templates, and step-by-step video tutorials. Our knowledge base offers getting started guides, feature documentation, and integration instructions. You can also benefit from customer-shared tips and workflows in our community platform.\\n\",\n      \"\\n\",\n      \"**4. Scheduling a Walkthrough or Onboarding Assistance**  \\n\",\n      \"Yes, onboarding assistance is available. You will receive a welcome call within 24 hours of contract signing and dedicated support during your launch week. You can also schedule a walkthrough or speak directly with a support representative at any time via our main support line, live chat, or by contacting your customer success team.\\n\",\n      \"\\n\",\n      \"If you have any further questions or would like to schedule a walkthrough, please let us know. We’re here to help!\\n\",\n      \"\\n\",\n      \"Best regards,  \\n\",\n      \"[Your Name]  \\n\",\n      \"[Your Position]  \\n\",\n      \"[Company Name]  \\n\",\n      \"[Contact Information]\\n\",\n      \"True\\n\",\n      \"Email successfully sent\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(result.email_content)\\n\",\n    \"print(result.email_successfully_sent)\\n\",\n    \"print(result.result)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"cmiYCDstDzKz\"\n   },\n   \"source\": [\n    \"As you can see from the output of the cell where we executed the workflow, our OpenTelemtry instrumentation has wrote several spans to the traces file. We can confirm by printing the last ten records out:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"collapsed\": true,\n    \"id\": \"et8hRebkFDlH\",\n    \"outputId\": \"10bacb30-214d-4c76-c0c7-a4125a2ab9e8\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"CompactAndRefine.aget_response-480348a5-76cf-4cae-8e6f-8c2435e7f4c6\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x0163c16cbfacd645\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x34c58f5c763e9447\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:07.068699Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:08.579093Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"BaseSynthesizer.asynthesize-d5567626-3114-4b5e-9a24-bed0144a9d01\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x34c58f5c763e9447\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0xee177fb17b7abfd2\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:07.068144Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:08.579872Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"SynthesizeStartEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:27:08.579846Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"d29ffe22-94d8-4a20-b79d-7efbefde5cd1\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"BaseSynthesizer.asynthesize-d5567626-3114-4b5e-9a24-bed0144a9d01\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"SynthesizeStartEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        },\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"SynthesizeEndEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:27:08.579859Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"34d0862f-d484-4196-8d67-b345f7b56ed4\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"BaseSynthesizer.asynthesize-d5567626-3114-4b5e-9a24-bed0144a9d01\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"SynthesizeEndEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"RetrieverQueryEngine._aquery-a8750d21-ed4b-4b01-9670-9f342dddc9d2\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xee177fb17b7abfd2\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x7dc478b93e97a9e9\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:05.364018Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:08.579954Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"BaseQueryEngine.aquery-8f1dfce0-2aa7-4cdf-9b90-c30c232a3b18\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x7dc478b93e97a9e9\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0xaa78953496248fdc\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:05.363614Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:08.580464Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"QueryStartEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:27:08.580447Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"bce8be96-e5cf-4b16-85cf-b77dc7a9d770\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"BaseQueryEngine.aquery-8f1dfce0-2aa7-4cdf-9b90-c30c232a3b18\\\",\\n\",\n      \"                \\\"query\\\": \\\"Is there an option to schedule a walkthrough or speak directly with a support representative for onboarding assistance?\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"QueryStartEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        },\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"QueryEndEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:27:08.580457Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"214866a0-5bc2-4880-9369-50a64dcb8c5e\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"BaseQueryEngine.aquery-8f1dfce0-2aa7-4cdf-9b90-c30c232a3b18\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"QueryEndEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"EmailSupportWorkflow.information_retrieval-86146c42-7f90-428e-b7c5-7a973ea25f8b\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xaa78953496248fdc\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x84a8252993fbfce4\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:26:45.179353Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:08.580727Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"OpenAIResponses.achat-27ad2815-4058-4fcd-a073-fca81b1e4029\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xa7f867e9e6a4c546\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x3bbb036cb49feabb\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:08.582691Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:16.660637Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"LLMChatStartEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:27:16.660605Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"675a2923-6bf9-4265-b0f6-1892dac9af87\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"OpenAIResponses.achat-27ad2815-4058-4fcd-a073-fca81b1e4029\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"LLMChatStartEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        },\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"LLMChatEndEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:27:16.660618Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"event_data\\\": \\\"timestamp=datetime.datetime(2025, 7, 4, 16, 27, 16, 659922) id_='86341028-4bda-42e0-94a1-87696b32153c' span_id='OpenAIResponses.achat-27ad2815-4058-4fcd-a073-fca81b1e4029' tags={} messages=[ChatMessage(role=<MessageRole.USER: 'user'>, additional_kwargs={}, blocks=[TextBlock(block_type='text', text='Starting from this JSON representation of question-answer pairs for the support answers of a user:\\\\\\\\n\\\\\\\\n```json\\\\\\\\n{\\\\\\\\n    \\\\\\\"What documentation or tutorials are available for new users to help them navigate the dashboard, especially regarding nested settings and tools?\\\\\\\": \\\\\\\"New users have access to a variety of resources to help them navigate the dashboard, including step-by-step Getting Started Guides, comprehensive user manuals, and feature documentation. There are also video tutorials and a video library with over 200 on-demand training videos that visually demonstrate how to use different features, including navigating nested settings and tools. Interactive tutorials provide in-app guided learning experiences, and users can participate in live webinars or access on-demand training modules for more detailed walkthroughs. Additionally, the knowledge base offers troubleshooting guides and integration instructions, while the community forum allows users to ask questions and share best practices. For those who prefer hands-on practice, sandbox accounts are available as practice environments.\\\\\\\",\\\\\\\\n    \\\\\\\"What features are included in the current subscription level for this user?\\\\\\\": \\\\\\\"The current subscription level for this user includes the following features:\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\n- 24/7/365 support coverage with a dedicated account manager\\\\\\\\\\\\\\\\n- Priority support, including custom integrations, on-site assistance, and training\\\\\\\\\\\\\\\\n- Response times of 4 hours for standard issues and 1 hour for critical issues\\\\\\\\\\\\\\\\n- Access to all support channels (support portal, email, phone, emergency hotline, live chat)\\\\\\\\\\\\\\\\n- Advanced workflow automation, enterprise integrations, and custom reporting (if using WorkflowMax Enterprise)\\\\\\\\\\\\\\\\n- Automated daily incremental and weekly full data backups, with a 365-day retention policy and 1-hour recovery time\\\\\\\\\\\\\\\\n- AES-256 encryption at rest, TLS 1.3 encryption in transit, and multi-factor authentication\\\\\\\\\\\\\\\\n- Compliance with SOC 2 Type II, GDPR, HIPAA, and ISO 27001\\\\\\\\\\\\\\\\n- Uptime guarantee of 99.99%\\\\\\\\\\\\\\\\n- SLA credits for downtime below guaranteed levels\\\\\\\\\\\\\\\\n- Access to mobile apps (iOS and Android), push notifications, in-app help, mobile chat, and offline resources\\\\\\\\\\\\\\\\n- Participation in customer advocacy programs, customer feedback channels, and early access to new features through beta testing\\\\\\\\\\\\\\\\n- Dedicated account management, custom implementation, on-site training, regular health checks, and strategic planning sessions\\\\\\\\\\\\\\\\n- Full accessibility support, including WCAG 2.1 AA compliance, screen reader compatibility, keyboard navigation, and alternative documentation formats\\\\\\\",\\\\\\\\n    \\\\\\\"Are there any recommended best practices or guides for setting up a workspace to maximize platform usage?\\\\\\\": \\\\\\\"Yes, there are best practice guides and industry-specific recommendations available to help set up a workspace and maximize platform usage. These resources include comprehensive user guides, downloadable templates for workflows, and visual step-by-step video tutorials. Additionally, customers can access a knowledge base with getting started guides, feature documentation, and integration instructions. The community platform also offers customer-shared tips, workflows, and best practices to further enhance workspace setup and usage.\\\\\\\",\\\\\\\\n    \\\\\\\"Is there an option to schedule a walkthrough or speak directly with a support representative for onboarding assistance?\\\\\\\": \\\\\\\"Yes, onboarding assistance includes a welcome call within 24 hours of contract signing, as well as dedicated support during the launch week. You can also access support through the main support line, live chat, or by contacting the customer success team. These options allow you to schedule a walkthrough or speak directly with a support representative for onboarding help.\\\\\\\"\\\\\\\\n}\\\\\\\\n```\\\\\\\\n\\\\\\\\nCan you please generate the text of the email to send back to the customer and answer their questions? Generate only the email.')])] response=ChatResponse(message=ChatMessage(role=<MessageRole.ASSISTANT: 'assistant'>, additional_kwargs={}, blocks=[TextBlock(block_type='text', text='Subject: Answers to Your Support Questions\\\\\\\\n\\\\\\\\nDear [Customer Name],\\\\\\\\n\\\\\\\\nThank you for reaching out with your questions. Please find detailed answers below to help you get the most out of your platform experience:\\\\\\\\n\\\\\\\\n**1. Documentation and Tutorials for New Users**  \\\\\\\\nNew users have access to a wide range of resources to help navigate the dashboard, including step-by-step Getting Started Guides, comprehensive user manuals, and feature documentation. We also offer a video library with over 200 on-demand training videos, interactive in-app tutorials, and live or on-demand webinars for detailed walkthroughs. Our knowledge base provides troubleshooting guides and integration instructions, while the community forum is available for sharing best practices and asking questions. For hands-on practice, sandbox accounts are also available.\\\\\\\\n\\\\\\\\n**2. Features Included in Your Current Subscription**  \\\\\\\\nYour current subscription includes:\\\\\\\\n- 24/7/365 support with a dedicated account manager\\\\\\\\n- Priority support, custom integrations, on-site assistance, and training\\\\\\\\n- Fast response times (4 hours for standard, 1 hour for critical issues)\\\\\\\\n- Access to all support channels: portal, email, phone, emergency hotline, and live chat\\\\\\\\n- Advanced workflow automation, enterprise integrations, and custom reporting (if using WorkflowMax Enterprise)\\\\\\\\n- Automated daily incremental and weekly full data backups (365-day retention, 1-hour recovery)\\\\\\\\n- AES-256 encryption at rest, TLS 1.3 in transit, and multi-factor authentication\\\\\\\\n- Compliance with SOC 2 Type II, GDPR, HIPAA, and ISO 27001\\\\\\\\n- 99.99% uptime guarantee and SLA credits for downtime\\\\\\\\n- Mobile apps (iOS/Android), push notifications, in-app help, mobile chat, and offline resources\\\\\\\\n- Participation in customer advocacy programs, feedback channels, and early access to new features\\\\\\\\n- Dedicated account management, custom implementation, on-site training, regular health checks, and strategic planning\\\\\\\\n- Full accessibility support (WCAG 2.1 AA, screen reader compatibility, keyboard navigation, alternative documentation formats)\\\\\\\\n\\\\\\\\n**3. Best Practices for Workspace Setup**  \\\\\\\\nWe provide best practice guides and industry-specific recommendations to help you set up your workspace for maximum efficiency. These include comprehensive user guides, downloadable workflow templates, and step-by-step video tutorials. Our knowledge base offers getting started guides, feature documentation, and integration instructions. You can also benefit from customer-shared tips and workflows in our community platform.\\\\\\\\n\\\\\\\\n**4. Scheduling a Walkthrough or Onboarding Assistance**  \\\\\\\\nYes, onboarding assistance is available. You will receive a welcome call within 24 hours of contract signing and dedicated support during your launch week. You can also schedule a walkthrough or speak directly with a support representative at any time via our main support line, live chat, or by contacting your customer success team.\\\\\\\\n\\\\\\\\nIf you have any further questions or would like to schedule a walkthrough, please let us know. We\\\\u2019re here to help!\\\\\\\\n\\\\\\\\nBest regards,  \\\\\\\\n[Your Name]  \\\\\\\\n[Your Position]  \\\\\\\\n[Company Name]  \\\\\\\\n[Contact Information]')]), raw={'id': 'resp_686800dca3d8819b84d69bf5aa5bb5df041318cc04eeded5', 'created_at': 1751646428.0, 'error': None, 'incomplete_details': None, 'instructions': None, 'metadata': {}, 'model': 'gpt-4.1-2025-04-14', 'object': 'response', 'output': [{'id': 'msg_686800dd0448819b8573406b00683f12041318cc04eeded5', 'content': [{'annotations': [], 'text': 'Subject: Answers to Your Support Questions\\\\\\\\n\\\\\\\\nDear [Customer Name],\\\\\\\\n\\\\\\\\nThank you for reaching out with your questions. Please find detailed answers below to help you get the most out of your platform experience:\\\\\\\\n\\\\\\\\n**1. Documentation and Tutorials for New Users**  \\\\\\\\nNew users have access to a wide range of resources to help navigate the dashboard, including step-by-step Getting Started Guides, comprehensive user manuals, and feature documentation. We also offer a video library with over 200 on-demand training videos, interactive in-app tutorials, and live or on-demand webinars for detailed walkthroughs. Our knowledge base provides troubleshooting guides and integration instructions, while the community forum is available for sharing best practices and asking questions. For hands-on practice, sandbox accounts are also available.\\\\\\\\n\\\\\\\\n**2. Features Included in Your Current Subscription**  \\\\\\\\nYour current subscription includes:\\\\\\\\n- 24/7/365 support with a dedicated account manager\\\\\\\\n- Priority support, custom integrations, on-site assistance, and training\\\\\\\\n- Fast response times (4 hours for standard, 1 hour for critical issues)\\\\\\\\n- Access to all support channels: portal, email, phone, emergency hotline, and live chat\\\\\\\\n- Advanced workflow automation, enterprise integrations, and custom reporting (if using WorkflowMax Enterprise)\\\\\\\\n- Automated daily incremental and weekly full data backups (365-day retention, 1-hour recovery)\\\\\\\\n- AES-256 encryption at rest, TLS 1.3 in transit, and multi-factor authentication\\\\\\\\n- Compliance with SOC 2 Type II, GDPR, HIPAA, and ISO 27001\\\\\\\\n- 99.99% uptime guarantee and SLA credits for downtime\\\\\\\\n- Mobile apps (iOS/Android), push notifications, in-app help, mobile chat, and offline resources\\\\\\\\n- Participation in customer advocacy programs, feedback channels, and early access to new features\\\\\\\\n- Dedicated account management, custom implementation, on-site training, regular health checks, and strategic planning\\\\\\\\n- Full accessibility support (WCAG 2.1 AA, screen reader compatibility, keyboard navigation, alternative documentation formats)\\\\\\\\n\\\\\\\\n**3. Best Practices for Workspace Setup**  \\\\\\\\nWe provide best practice guides and industry-specific recommendations to help you set up your workspace for maximum efficiency. These include comprehensive user guides, downloadable workflow templates, and step-by-step video tutorials. Our knowledge base offers getting started guides, feature documentation, and integration instructions. You can also benefit from customer-shared tips and workflows in our community platform.\\\\\\\\n\\\\\\\\n**4. Scheduling a Walkthrough or Onboarding Assistance**  \\\\\\\\nYes, onboarding assistance is available. You will receive a welcome call within 24 hours of contract signing and dedicated support during your launch week. You can also schedule a walkthrough or speak directly with a support representative at any time via our main support line, live chat, or by contacting your customer success team.\\\\\\\\n\\\\\\\\nIf you have any further questions or would like to schedule a walkthrough, please let us know. We\\\\u2019re here to help!\\\\\\\\n\\\\\\\\nBest regards,  \\\\\\\\n[Your Name]  \\\\\\\\n[Your Position]  \\\\\\\\n[Company Name]  \\\\\\\\n[Contact Information]', 'type': 'output_text', 'logprobs': []}], 'role': 'assistant', 'status': 'completed', 'type': 'message'}], 'parallel_tool_calls': True, 'temperature': 0.1, 'tool_choice': 'auto', 'tools': [], 'top_p': 1.0, 'background': False, 'max_output_tokens': None, 'max_tool_calls': None, 'previous_response_id': None, 'prompt': None, 'reasoning': {'effort': None, 'generate_summary': None, 'summary': None}, 'service_tier': 'default', 'status': 'completed', 'text': {'format': {'type': 'text'}}, 'top_logprobs': 0, 'truncation': 'disabled', 'usage': {'input_tokens': 715, 'input_tokens_details': {'cached_tokens': 0}, 'output_tokens': 606, 'output_tokens_details': {'reasoning_tokens': 0}, 'total_tokens': 1321}, 'user': None, 'store': False}, delta=None, logprobs=None, additional_kwargs={'built_in_tool_calls': [], 'annotations': [], 'usage': ResponseUsage(input_tokens=715, input_tokens_details=InputTokensDetails(cached_tokens=0), output_tokens=606, output_tokens_details=OutputTokensDetails(reasoning_tokens=0), total_tokens=1321)})\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"EmailSupportWorkflow.create_email_event-57147920-3e10-47a9-b77a-38de1bf78c96\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x3bbb036cb49feabb\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x84a8252993fbfce4\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:08.582322Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:16.660864Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"EmailSupportWorkflow.send_email_or_restart-e5d56238-60fd-4688-8cc5-a1c1208db80e\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xf764db02e4e2a839\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x84a8252993fbfce4\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:19.191843Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:19.192015Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"Workflow._done-97fc1c3f-0dfb-4f8d-9653-9293fdfeece8\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x9f81b0c9fbfaeb84\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0x84a8252993fbfce4\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:27:19.192436Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:19.192695Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"ERROR\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"SpanDropEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:27:19.192676Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"0a1e5e34-65ba-4942-aae8-014a94f14403\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"Workflow._done-97fc1c3f-0dfb-4f8d-9653-9293fdfeece8\\\",\\n\",\n      \"                \\\"err_str\\\": \\\"\\\",\\n\",\n      \"                \\\"class_name\\\": \\\"SpanDropEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\",\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"Workflow.run-c69ed985-c027-4cec-a3c3-5874bffe3d52\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x39d8e6e5961a74a286c7015e272ee118\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0x84a8252993fbfce4\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": null,\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:26:35.330829Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:27:19.193650Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"with open(\\\"workflow_traces.json\\\", \\\"r\\\") as f:\\n\",\n    \"    lines = f.readlines()\\n\",\n    \"    for line in lines[-10:]:\\n\",\n    \"        print(json.dumps(json.loads(line), indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"zQVUbgEBFRwt\"\n   },\n   \"source\": [\n    \"As we saw in Part 1, we could also create a workflow that has customized events - in this case we'll create more, so that we can add them to every step of the workflow and see that they are placed within the specific span that the step emits. As for Part 1, we will not use the `@dispatcher.span` decorator since all workflow steps are already instrumented.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"aR3C78CNF-iI\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index_instrumentation import get_dispatcher\\n\",\n    \"from llama_index_instrumentation.base import BaseEvent\\n\",\n    \"\\n\",\n    \"dispatcher = get_dispatcher()\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowEmailEvent(BaseEvent):\\n\",\n    \"    email_len: int\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowExtractionEvent(BaseEvent):\\n\",\n    \"    extraction_latency: float\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowQuestionsEvent(BaseEvent):\\n\",\n    \"    questions_number: int\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowAnswersEvent(BaseEvent):\\n\",\n    \"    q_and_a_latency: float\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowDoneEvent(BaseEvent):\\n\",\n    \"    error: bool\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowCandidateEmailEvent(BaseEvent):\\n\",\n    \"    candidate_email_len: int\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"vUZdNOpyFq5M\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"import time\\n\",\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class EmailSupportWorkflow(Workflow):\\n\",\n    \"    # remember to use the step decorator!\\n\",\n    \"    @step\\n\",\n    \"    async def extract_email_information(\\n\",\n    \"        self,\\n\",\n    \"        event: EmailEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        extraction_agent: Annotated[ExtractionAgent, Resource(get_extract_agent)],\\n\",\n    \"    ) -> InformationExtractionEvent | SendEmailEvent:\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.email_text = event.email_content\\n\",\n    \"\\n\",\n    \"        st = time.time()\\n\",\n    \"        extracted_content = await extraction_agent.aextract(\\n\",\n    \"            files=SourceText(\\n\",\n    \"                text_content=text, filename=f\\\"support_email_{str(uuid.uuid4())}.txt\\\"\\n\",\n    \"            )\\n\",\n    \"        )\\n\",\n    \"        end = time.time()\\n\",\n    \"        latency = end - st\\n\",\n    \"        if extracted_content:\\n\",\n    \"            extracted_data = SupportEmailInformation.model_validate(\\n\",\n    \"                extracted_content.data\\n\",\n    \"            )\\n\",\n    \"            ctx.write_event_to_stream(\\n\",\n    \"                ProgressEvent(msg=\\\"Information extracted successfully\\\")\\n\",\n    \"            )\\n\",\n    \"            dispatcher.event(\\n\",\n    \"                event=WorkflowEmailEvent(email_len=len(event.email_content))\\n\",\n    \"            )\\n\",\n    \"            dispatcher.event(event=WorkflowExtractionEvent(extraction_latency=latency))\\n\",\n    \"            return InformationExtractionEvent(\\n\",\n    \"                sender_details=extracted_data.sender_details.model_dump_json(),\\n\",\n    \"                support_category=extracted_data.support_category,\\n\",\n    \"                support_questions=extracted_data.support_questions,\\n\",\n    \"                extra_information=extracted_data.extra_information,\\n\",\n    \"            )\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Information extraction failed\\\"))\\n\",\n    \"        dispatcher.event(event=WorkflowDoneEvent(error=True))\\n\",\n    \"        return SendEmailEvent(\\n\",\n    \"            email_content=\\\"\\\",\\n\",\n    \"            email_successfully_sent=False,\\n\",\n    \"            result=\\\"Failed to extract information, please re-run the workflow\\\",\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def generate_questions_to_ask(\\n\",\n    \"        self,\\n\",\n    \"        event: InformationExtractionEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        llm: Annotated[StructuredLLM, Resource(get_llm)],\\n\",\n    \"    ) -> QuestionEvent | SendEmailEvent:\\n\",\n    \"        response = await llm.achat(\\n\",\n    \"            messages=[\\n\",\n    \"                ChatMessage(\\n\",\n    \"                    role=\\\"user\\\",\\n\",\n    \"                    content=f\\\"Starting from this JSON representation of a support email:\\\\n\\\\n```json\\\\n{event.model_dump_json()}\\\\n```\\\\n\\\\nCan you please generate one or more support questions to ask to the company database to solve the users' problem?\\\",\\n\",\n    \"                )\\n\",\n    \"            ]\\n\",\n    \"        )\\n\",\n    \"        if response.message.content:\\n\",\n    \"            response_json = json.loads(response.message.content)\\n\",\n    \"            ctx.write_event_to_stream(\\n\",\n    \"                ProgressEvent(msg=\\\"Questions generated successfully\\\")\\n\",\n    \"            )\\n\",\n    \"            dispatcher.event(\\n\",\n    \"                event=WorkflowQuestionsEvent(\\n\",\n    \"                    questions_number=len(response_json[\\\"questions\\\"])\\n\",\n    \"                )\\n\",\n    \"            )\\n\",\n    \"            return QuestionEvent(questions=response_json[\\\"questions\\\"])\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Questions generation failed\\\"))\\n\",\n    \"        dispatcher.event(event=WorkflowDoneEvent(error=True))\\n\",\n    \"        return SendEmailEvent(\\n\",\n    \"            email_content=\\\"\\\",\\n\",\n    \"            email_successfully_sent=False,\\n\",\n    \"            result=\\\"Failed to generate questions, please re-run the workflow\\\",\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def information_retrieval(\\n\",\n    \"        self,\\n\",\n    \"        event: QuestionEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        company_database: Annotated[BaseQueryEngine, Resource(get_company_database)],\\n\",\n    \"    ) -> RetrievalEvent:\\n\",\n    \"        q_and_a: dict[str, str] = {}\\n\",\n    \"        st = time.time()\\n\",\n    \"        for question in event.questions:\\n\",\n    \"            response = await company_database.aquery(question)\\n\",\n    \"            q_and_a.update({question: response.response or \\\"\\\"})\\n\",\n    \"        end = time.time()\\n\",\n    \"        ctx.write_event_to_stream(\\n\",\n    \"            ProgressEvent(msg=\\\"Answers to questions generated successfully\\\")\\n\",\n    \"        )\\n\",\n    \"        dispatcher.event(event=WorkflowAnswersEvent(q_and_a_latency=end - st))\\n\",\n    \"        return RetrievalEvent(information=q_and_a)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def create_email_event(\\n\",\n    \"        self,\\n\",\n    \"        event: RetrievalEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        llm_email: Annotated[LLM, Resource(get_llm_for_email)],\\n\",\n    \"    ) -> CandidateEmailEvent:\\n\",\n    \"        response = await llm_email.achat(\\n\",\n    \"            messages=[\\n\",\n    \"                ChatMessage(\\n\",\n    \"                    role=\\\"user\\\",\\n\",\n    \"                    content=f\\\"Starting from this JSON representation of question-answer pairs for the support answers of a user:\\\\n\\\\n```json\\\\n{json.dumps(event.information, indent=4)}\\\\n```\\\\n\\\\nCan you please generate the text of the email to send back to the customer and answer their questions? Generate only the email.\\\",\\n\",\n    \"                )\\n\",\n    \"            ]\\n\",\n    \"        )\\n\",\n    \"        ctx.write_event_to_stream(ProgressEvent(msg=\\\"Generated candidate email\\\"))\\n\",\n    \"\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.candidate_email = response.message.content\\n\",\n    \"\\n\",\n    \"        dispatcher.event(\\n\",\n    \"            event=WorkflowCandidateEmailEvent(\\n\",\n    \"                candidate_email_len=len(response.message.content)\\n\",\n    \"            )\\n\",\n    \"        )\\n\",\n    \"        return CandidateEmailEvent(candidate_email=response.message.content)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def send_email_or_restart(\\n\",\n    \"        self, event: HumanFeedbackEvent, ctx: Context[WorkflowState]\\n\",\n    \"    ) -> SendEmailEvent | EmailEvent:\\n\",\n    \"        if event.approved:\\n\",\n    \"            state = await ctx.store.get_state()\\n\",\n    \"            dispatcher.event(event=WorkflowDoneEvent(error=False))\\n\",\n    \"            return SendEmailEvent(\\n\",\n    \"                email_content=state.candidate_email,\\n\",\n    \"                email_successfully_sent=True,\\n\",\n    \"                result=\\\"Email successfully sent\\\",\\n\",\n    \"            )\\n\",\n    \"        else:\\n\",\n    \"            state = await ctx.store.get_state()\\n\",\n    \"            dispatcher.event(event=WorkflowEmailEvent(email_len=len(state.email_text)))\\n\",\n    \"            return EmailEvent(\\n\",\n    \"                email_content=state.email_text,\\n\",\n    \"            )\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"mazDgFljHY5J\",\n    \"outputId\": \"03ff1718-01e3-4e0e-cb8b-fa7ab058dce1\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Uploading files: 100%|██████████| 1/1 [00:01<00:00,  1.18s/it]\\n\",\n      \"Creating extraction jobs: 100%|██████████| 1/1 [00:01<00:00,  1.90s/it]\\n\",\n      \"Extracting files:   0%|          | 0/1 [00:00<?, ?it/s]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Writing 4 spans to workflow_traces.json\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Extracting files: 100%|██████████| 1/1 [00:03<00:00,  3.88s/it]\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Information extracted successfully\\n\"\n     ]\n    },\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Questions generated successfully\\n\",\n      \"Writing 15 spans to workflow_traces.json\\n\",\n      \"Writing 10 spans to workflow_traces.json\\n\",\n      \"Answers to questions generated successfully\\n\",\n      \"Writing 18 spans to workflow_traces.json\\n\",\n      \"Generated candidate email\\n\",\n      \"Candidate email: Subject: Welcome! Resources and Guides for New Users\\n\",\n      \"\\n\",\n      \"Hi [Customer Name],\\n\",\n      \"\\n\",\n      \"Thank you for reaching out with your questions about getting started and making the most of our platform. I’m happy to provide the information you need:\\n\",\n      \"\\n\",\n      \"**1. Documentation and Onboarding Guides:**  \\n\",\n      \"New users have access to a variety of resources to help you navigate the dashboard and locate settings and tools. These include:\\n\",\n      \"- A Getting Started Guide with step-by-step setup instructions\\n\",\n      \"- A comprehensive User Manual\\n\",\n      \"- Video tutorials with visual walkthroughs\\n\",\n      \"- Interactive tutorials for in-app guided learning\\n\",\n      \"- A knowledge base with feature documentation and troubleshooting guides\\n\",\n      \"- Downloadable templates\\n\",\n      \"- Live webinars and on-demand training modules\\n\",\n      \"- A community forum for peer support and best practices\\n\",\n      \"- Self-paced courses and a resource library with best practice guides and migration assistance\\n\",\n      \"\\n\",\n      \"**2. Subscription Level Feature Breakdown:**  \\n\",\n      \"You can find a detailed breakdown of features included in each subscription level in our User Manual, available at: [https://docs.yourbestsoftware.com/manual](https://docs.yourbestsoftware.com/manual). This resource provides comprehensive information about the features and capabilities of each subscription tier.\\n\",\n      \"\\n\",\n      \"**3. Best Practices and Setup Guides for Beginners:**  \\n\",\n      \"Yes, we offer recommended best practices and setup guides to help you configure your workspace and maximize the use of the platform. These include:\\n\",\n      \"- A comprehensive Getting Started Guide with step-by-step instructions\\n\",\n      \"- Best practice guides with industry-specific recommendations\\n\",\n      \"- Downloadable templates for pre-built workflows\\n\",\n      \"- Interactive and video tutorials\\n\",\n      \"- A knowledge base with feature documentation and troubleshooting guides\\n\",\n      \"\\n\",\n      \"If you have any further questions or need personalized assistance, please don’t hesitate to reply to this email. We’re here to help you succeed!\\n\",\n      \"\\n\",\n      \"Best regards,  \\n\",\n      \"[Your Name]  \\n\",\n      \"[Your Position]  \\n\",\n      \"[Your Company]  \\n\",\n      \"[Contact Information]\\n\",\n      \"Writing 2 spans to workflow_traces.json\\n\",\n      \"Approve? (y/<reason>): y\\n\",\n      \"Approved? y\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Let's re-run the custom instrumented workflow\\n\",\n    \"\\n\",\n    \"wf = EmailSupportWorkflow(timeout=800)\\n\",\n    \"\\n\",\n    \"handler = wf.run(start_event=EmailEvent(email_content=text))\\n\",\n    \"async for ev in handler.stream_events():\\n\",\n    \"    if isinstance(ev, ProgressEvent):\\n\",\n    \"        print(ev.msg, flush=True)\\n\",\n    \"    elif isinstance(ev, CandidateEmailEvent):\\n\",\n    \"        print(f\\\"Candidate email: {ev.candidate_email}\\\", flush=True)\\n\",\n    \"        approved = input(\\\"Approve? (y/<reason>): \\\").strip().lower()\\n\",\n    \"        print(f\\\"Approved? {approved}\\\", flush=True)\\n\",\n    \"        if approved == \\\"y\\\":\\n\",\n    \"            handler.ctx.send_event(\\n\",\n    \"                HumanFeedbackEvent(approved=True, feedback=\\\"Approved\\\")\\n\",\n    \"            )\\n\",\n    \"        else:\\n\",\n    \"            handler.ctx.send_event(\\n\",\n    \"                HumanFeedbackEvent(approved=False, feedback=approved)\\n\",\n    \"            )\\n\",\n    \"\\n\",\n    \"result = await handler\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"iPx3T4j5IiaO\"\n   },\n   \"source\": [\n    \"If we now print records from `workflow_traces.json`, we will see that the tracer has registered also our custom spans. For example, we can see the `WorkflowExtractionEvent` spans:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"sMf5cjYVIsif\",\n    \"outputId\": \"bb545028-4e99-4a7c-95f1-9e1a7c921408\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"{\\n\",\n      \"    \\\"name\\\": \\\"EmailSupportWorkflowInst.extract_email_information-a705e6ce-ae61-42ca-852e-c0b89c4cab8c\\\",\\n\",\n      \"    \\\"context\\\": {\\n\",\n      \"        \\\"trace_id\\\": \\\"0x1624a32351233b4d8c1ae6f0217fa7a7\\\",\\n\",\n      \"        \\\"span_id\\\": \\\"0xdd9813577a2fa2a4\\\",\\n\",\n      \"        \\\"trace_state\\\": \\\"[]\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"kind\\\": \\\"SpanKind.INTERNAL\\\",\\n\",\n      \"    \\\"parent_id\\\": \\\"0xd2996fdeef100e39\\\",\\n\",\n      \"    \\\"start_time\\\": \\\"2025-07-04T16:52:19.057441Z\\\",\\n\",\n      \"    \\\"end_time\\\": \\\"2025-07-04T16:52:26.024419Z\\\",\\n\",\n      \"    \\\"status\\\": {\\n\",\n      \"        \\\"status_code\\\": \\\"OK\\\"\\n\",\n      \"    },\\n\",\n      \"    \\\"attributes\\\": {},\\n\",\n      \"    \\\"events\\\": [\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"BaseEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:52:26.024378Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"c8ac716e-5be5-4cf2-bc16-8d195216022a\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"EmailSupportWorkflowInst.extract_email_information-a705e6ce-ae61-42ca-852e-c0b89c4cab8c\\\",\\n\",\n      \"                \\\"email_len\\\": 1076,\\n\",\n      \"                \\\"class_name\\\": \\\"BaseEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        },\\n\",\n      \"        {\\n\",\n      \"            \\\"name\\\": \\\"BaseEvent\\\",\\n\",\n      \"            \\\"timestamp\\\": \\\"2025-07-04T16:52:26.024402Z\\\",\\n\",\n      \"            \\\"attributes\\\": {\\n\",\n      \"                \\\"id_\\\": \\\"5fb97d99-7011-4e4d-847e-c7fa42f641f0\\\",\\n\",\n      \"                \\\"span_id\\\": \\\"EmailSupportWorkflowInst.extract_email_information-a705e6ce-ae61-42ca-852e-c0b89c4cab8c\\\",\\n\",\n      \"                \\\"extraction_latency\\\": 6.965550899505615,\\n\",\n      \"                \\\"class_name\\\": \\\"BaseEvent\\\"\\n\",\n      \"            }\\n\",\n      \"        }\\n\",\n      \"    ],\\n\",\n      \"    \\\"links\\\": [],\\n\",\n      \"    \\\"resource\\\": {\\n\",\n      \"        \\\"attributes\\\": {\\n\",\n      \"            \\\"service.name\\\": \\\"tracing.a.workflow.1\\\"\\n\",\n      \"        },\\n\",\n      \"        \\\"schema_url\\\": \\\"\\\"\\n\",\n      \"    }\\n\",\n      \"}\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"def has_extraction_latency_attribute(data: dict) -> bool:\\n\",\n    \"    for event in data.get(\\\"events\\\", []):\\n\",\n    \"        if \\\"extraction_latency\\\" in event.get(\\\"attributes\\\", {}):\\n\",\n    \"            return True\\n\",\n    \"    return False\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"with open(\\\"workflow_traces.json\\\", \\\"r\\\") as f:\\n\",\n    \"    lines = f.readlines()\\n\",\n    \"    for line in lines:\\n\",\n    \"        data = json.loads(line)\\n\",\n    \"        if has_extraction_latency_attribute(data):\\n\",\n    \"            print(json.dumps(data, indent=4))\\n\",\n    \"            break\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"HqQtZcjiYQKp\"\n   },\n   \"source\": [\n    \"As you can see, in this case we have a span with two events, corresponding exactly to the workflow step where we emit both the `WorkflowEmailEvent` and the `WorkflowExtractionEvent`\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"-3T7L45DYmzy\"\n   },\n   \"source\": [\n    \"And this is all for our advanced observability demo: let us know if you have any more questions and don't forget to star the [LlamaIndex workflows repo](https://github.com/run-llama/llama-agents)🦙🚀\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"name\": \"python\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/observability/workflows_observablitiy_arize_phoenix.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Tracing Workflows with Arize Phoenix\\n\",\n    \"\\n\",\n    \"This notebook demonstrates how to gain real-time observability for your [LlamaIndex Workflows](https://docs.llamaindex.ai/en/stable/module_guides/workflow/) by ingesting traces to [Arize Phoenix](https://arize.com/docs/phoenix/integrations/frameworks/llamaindex/llamaindex-workflows-tracing).\\n\",\n    \"\\n\",\n    \"Arize Phoenix offers several versions of their tracing platform (cloud, self-hosted docker, notebook). In this example, we will use the notebook-friendly version, but you can visit [their docs](https://arize.com/docs/phoenix/integrations/frameworks/llamaindex/llamaindex-workflows-tracing) to explore all options!\\n\",\n    \"\\n\",\n    \"## Step 1: Install Dependencies, set keys\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install arize-phoenix llama-index-workflows llama-index-instrumentation openinference-instrumentation-llama_index\\n\",\n    \"# Optional if using openai or other llama-index packages\\n\",\n    \"%pip install llama-index-llms-openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import os\\n\",\n    \"\\n\",\n    \"# Your openai key\\n\",\n    \"os.environ[\\\"OPENAI_API_KEY\\\"] = \\\"sk-proj-...\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Step 2: Launch Arize Phoenix\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import phoenix as px\\n\",\n    \"\\n\",\n    \"px.launch_app()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Step 3: Initialize LlamaIndex Instrumentation\\n\",\n    \"\\n\",\n    \"Now, we initialize the [OpenInference LlamaIndex instrumentation](https://docs.arize.com/phoenix/tracing/integrations-tracing/llamaindex). This third-party instrumentation automatically captures LlamaIndex operations and exports OpenTelemetry (OTel) spans to Arize Phoenix.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from openinference.instrumentation.llama_index import LlamaIndexInstrumentor\\n\",\n    \"from phoenix.otel import register\\n\",\n    \"\\n\",\n    \"tracer_provider = register()\\n\",\n    \"LlamaIndexInstrumentor().instrument(tracer_provider=tracer_provider)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Step 4: Create a Simple LlamaIndex Workflows Application\\n\",\n    \"\\n\",\n    \"In LlamaIndex Workflows, you build event-driven AI agents by defining steps with the `@step` decorator. Each step processes an event and, if appropriate, emits new events.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from llama_index.core.llms import ChatMessage\\n\",\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_llm():\\n\",\n    \"    return OpenAI(model=\\\"gpt-4.1-mini\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(\\n\",\n    \"        self, ev: StartEvent, llm: Annotated[OpenAI, Resource(get_llm)]\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        message = ChatMessage(role=\\\"user\\\", content=ev.get(\\\"input\\\"))\\n\",\n    \"        response = await llm.achat([message])\\n\",\n    \"        return StopEvent(result=response.message.content)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"wf = MyWorkflow()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Hello! How can I assist you today?\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"response = await wf.run(input=\\\"Hi there!\\\")\\n\",\n    \"print(response)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"bat\"\n    }\n   },\n   \"source\": [\n    \"## Step 5: View Traces in Arize Phoenix\\n\",\n    \"\\n\",\n    \"After running your workflow, open Arize Phoenix (which in this case is running at [http://localhost:6006/](http://localhost:6006/)) to explore the generated traces. You will see logs for each workflow step along with metrics such as latencies, and execution paths. \\n\",\n    \"\\n\",\n    \"![image.png](data:image/png;base64,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mXS/asG+N998sylMFW133HGHP/3gk08+6QVmgn169eplZ599thdqcdu1TU3BlPvvv982bNjgdvGaAoGcPks9evSwCy+8MNOZNW2kftR++9vfemFH10mhu9tuu800hV+0rVu3zh544IFMgb3g74I33njDm5pSYalomz9/vldtzQX3ovtVjU3XrM+ta2eddZb3jCog+cwzz3ibFYRyz402xPt8avsFF1xg3bt316L3GddnnYYAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAAChVNgf6qocF6fd1WqluSaQgnRsI72qY/CUCeccEIoLOWO06sq2GjKuipVqgQ3e8vHHHOMN71dsDpOsJMqICnY06ZNm+Bmb7lnz552zz33hMJSwU6qfPPEE094Yabg9kTLCxcu9Kss6b6uv/76RF29ijjXXHON6efGG29MeO+JBtB9K7TiKnhF+6nSlc7vwl/R/alYD76/wWWNfe6559r5558fCksFz6kKVgpnqaoWLTUCuXmWggHFZK5CAcO///3v1ixOWErHZ2Rk2MMPP+xVjQqOF/x8KNAXLyyl/k2bNrXf//73wUO9ZRfsO+mkk0JhKddR+48++mi78847vd8pqmC1bNkyt9tOOeUUfzm44AKd2jZmzJjgLpYRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAoJAJFIkKU/HMNDWWflxQQ5VnNKWcmqoyffHFF6ZqVFru2rWrnXrqqd4+BaJU8ebDDz/01vWPQkFXX321H6DYtGmTN83f5MmTrVWrVnbGGWd4YSiFKVQ5RuEkN22cglSqrOOCHJombPDgwbZ+/XovvKVqWDpOladU1ebFF1/0z5vVgqpSHXrooV4XTfH32GOP2auvvmq6pqza5s2bveCYrkeVp1yQQ5WY3LlVfUdNVYR+85vf+MOtWbPGVH1KU5rpvi+66CJvKj916NOnj33yySde3/vuu880NZq85aYmj0ceecRbTtV0ZAq2uepgGnj27Nn20Ucfma5T0zFqn2xVBUjvf7ASkHch/JMrgdw8S1999ZVffW3AgAF+ePCbb76xb7/91qvcFJxmT2Emff7UFEoaOHCgzZo1yxTgUyhS1af03urz9Yc//CHhfej5/u6772zs2LFeEFLPqpuKUgHJhg0b2uLFi/3j+/XrF5pi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WKhp+rBRo0b57+w//vEPe/TRR70Qliri/PWvf/XCE7Nnz7Y6dep4U8bp/GqHHnqotWrVyqZNm+YfH29B4QudU9euplDUSSedZApS6Z50jarWpIpWwWnudB/BNmXKFH+1fv36dsEFF3gBkJ9++slUbee7777zp7TTFGW6D03Np2pZCmo5TzdIdH3Tpk3eNGS6BpneeOON3r1t3LjRRo8e7R02YcIEvzpU69at7bbbbjNt073pp0aNGm74TK8KprnKUnrV9SncJgOZqhqYey/1vgTDPpkGK8Eb9N5ef/31WQpomrznnnvOq9yVl2dJJ9FnS591ta5du9ratWu950fT7+lZfe+997yglKsgdeutt9qPP/5oU6dO9QJwmmZRz4mqS+kz17t3b38qRm/QXP6je/z444/9kKGeKVXU0rlVme7www8PhbcUsIo23YumFaxVq5a/S8+6pgKlIYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEDRECgWgSlRv/baazZgwAAvZKGgRVZTbmn6Nk01t2TJEu9dUtjh8ccf96pQqWKRAkldunTxfoJvo6rT3HvvvcFNXnji9ddft/79+3vnVnioW7du3k+oY2xF1WmyC0u5Yx588EG75557vGCQtqmaVHAKMNfPvX766ac2ZswYt+q96v5UBUvXpHtWKEzto48+8kItCoUpjOICXbVr187yHI0bN/aOD/6zbNkyUyBHTeb6mT9/vh+YUkhGFbbc1GYKTeknmaZgms6p49V0ff369ct0qN6/p556KtN2NuwXOPLII/evJFjSM6QQXF6eJQ09btw473OlZT1L559/vha9z4SrSqbnTRXXFETUZ7NDhw7ej9cx8I/CSHq+UtX0DCokpfOptWjRwvuJjq8qWIMGDYpu9ta//vprP3SlDUOHDo3bj40IIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggUDgFDtiUfAryuKbKL1k1TXnlWqK+48ePt4cffthU3SjaVDFHAaTg9FpHHXVUqNvMmTPt5ptv9sI+wfOpk6517ty5pko4LmQVPPirr77ywh+qcBTv+tatW2ePPPKIDR48OHhYlssKAd19993etGTLly+P21d9FixYYH/5y1+8qj3xOqkqU/R+FFBR07XqHLILvh9unypQvfTSS15f/RNv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AAIIIIAAAggggAACCCCAAAIIIIAAAggggED+ChCYyl9fRkcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFCJEBgqhC9GVwKAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5K8Agan89WV0BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKEQCBKYK0ZvBpSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggED+ChCYyl9fRkcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFCJEBgqhC9GVwKAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5K8Agan89WV0BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKEQCBKYK0ZvBpSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggED+ChCYyl9fRkcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFCJEBgqhC9GVwKAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5K8Agan89WV0BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKEQCBKYK0ZvBpSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggED+ChCYyl9fRkcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFCJEBgqhC9GVwKAggggBJikKQAAEAASURBVAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5K8Agan89WV0BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKEQCBKYK0ZvBpSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggED+ChCYyl9fRkcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFCJEBgqhC9GVwKAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5K8Agan89WV0BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKEQCBKYK0ZvBpSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggED+ChCYyl9fRkcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFCJEBgqhC9GVwKAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII5K8Agan89WV0BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKEQCBKYK0ZvBpSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggED+CpRt3759/p6B0RFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBQiJAhalC8kZwGQgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJD/AqUaN268N/9PwxkQQAABBBBAAAEEEEAAAQQQMKtZs2aWDKtXr85yPzsRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEMirABWm8irI8QgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIFBkBAhMFZm3igtFAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBvAoQmMqrIMcjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAkREgMFVk3iouFAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBPIqQGAqr4IcjwACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAkVGgMBUkXmruFAEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDIqwCBqbwKcjwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUGQECU0XmreJCEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIK8CBKbyKsjxCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggUGQECEwVmbeKC0UAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIG8ChCYyqsgxyOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggECRESAwVWTeKi4UAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEE8ipAYCqvghyPAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACRUaAwFSReau4UAQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEMirAIGpvApyPAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCBQZAQJTReat4kIRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgrwJl8zoAxyOAAAIIIIAAAggggEDJEUhLS7NmzZpZrVq1rEqVKkXqxjdv3myrVq2yefPm2YYNG/L12ouyk2AK0ipf3wgGRwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgjkCpxo0b742znU0IIIAAAggggAACCCCAQEigTZs21qJFi9C2oroye/ZsmzZtWr5cfnFyElCqrWrWrJml++rVq7Pcz04EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBPIqQIWpvApyPAIIIIAAAggggAACJUDgqKOOsvr163t3unjxYq9Sk6oQFaWmiliqjNWwYUMv+FW5cmUbO3ZsSm+hODgJpCCsUgrPYAgggAACCCCAAAIIIIAAAggggAACCCCAAAII5ECgTHp6+j056E9XBBBAAAEEEEAAAQQQKGECqpjUpEkT27Ztm02dOtVWrlxpO3fuLHIKuub169fbmjVrLCMjw6pXr25ly5b17icVN1NcnGSRn1YKqmXVtm7dmtVu9iGAAAIIIIAAAggggAACCCCAAAIIIIAAAgggkGeB0nke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9mSYa9aebeY2GovGAABAAElEQVSaLP9AHKUGb1IwDb2H9IGsDElfP0d2bF6GLI0ESIAESIAESIAESIAESIAESIAESIAEYkIA4c9sCyZ4stsh7RY/hSOassdI5LB89j5wZug+N1TPUnpFe3cbd94eM1nT4YildO9eoimto2cpJZF4VwqmEu+eccUkQAIkQAIkQAIkQAIkQAIkQAIFQACCIHyq1WohNeq3leq1W5lZVTil1wJYSlxMsTNjhexI/0l2bVsVF+vhIkiABEiABEiABEiABEiABEiABEiABJKXAMQskdjmzZsFYfauuOIKp1so0ZQ7LJ/TMckSKobSa5JtL0/biUQspRPgeYKozxbyJZtYqkSJEnLTTTfJZZddZrb96aefyssvvywIz+hlV111lVx99dVSrlw5mT9/vrzwwgsBQ1tefPHFpm3NmjVl9erV8vrrrws8oxWmUTBVmPQ5NwmQAAmQAAmQAAmQAAmQAAmQQNwTgEAIn2LFS0qV6k2kYpX6xutU2XJVpGTpClK8RKm430MkCzxx/KgcO7JfDh3MNN6k9vlCFGbuXC8nTxyLZBi2JQESIAESIAESIAESIAESIAESIAESIIECJQAvUwiVBtGUhtqDyAWh+z766KMCXUtBTlayZMl8nS6/x8/XxXsMnhexFIYZMWJEUoulsEcIwAYOHIiksWHDhkmFChXkscce0yLnCrHU2LFjnXznzp3Nz9ro0aPl1KlTTjkSffr0kTvuuMMpa968ufzrX/8StE1PT3fKCzpBwVRBE+d8JEACJEACJEACJEACJEACJEACCUkAgiEVTyXkBrhoEiABEiABEiABEiABEiABEiABEiABEohjAhA2xcIgjrJFUxBP4QNBlW3uvFcbu328peENqGrVqmZZ+SFqyu/xC4vn8OHD/aYOx0tUURBL1a5d208spZAgoHrrrbckIyNDi8wVnqXchp/hHj16yIwZM/yq3OE2tRJt33vvPc0W+LVYgc/ICUmABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEggnwggRJ9t9evXt7NJl4YnrVGjRsVsX+3atZMOHTo440E8lQwGD2S2USyVQ6N8+fI5GVfKqw5h+LzMq7xixYpeTY33Ks+KAipMjqe6gGBxGhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggfglAG82EBDZNmfOHDvrmXZ7nPJsFEeFx48fF3xUzDRy5Ehp27at/PTTTzJ+/Pg8rRRCKYyDa9myZfM0Rjx3sgVTFEv536l169bJ7t27Ha9lWosy1Llt/vz5gjB8bluxYoW7SJYuXSodO3bMVb582bJcZQVZQMFUQdLmXCRAAiRAAiRAAiRAAiSQYAQOHDggeHsEH6STyfStmFjsK5k54Z7HklUyPUPcCwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQOwIwCsUwuLB8hqez0sshRB9XqZzedUlStmhQ4fE9t4DoZOKnrAHCFUgoApmEFnB0M/LMAc+yWYQTAWzohCGz71/CMoefPBBKV68uKk6ceKE2CIzu/0LL7xgfk7tn9Xnn39e0tLS7GYmjbB7TZs2ld69ezt1H3zwgcydN8/JF0aCgqnCoM45SYAESIAESIAESIAESCBBCOzcudOIZapXr550ginsCYY9RmvJzAlsYskqWtbsTwIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkkJwE0tPTo9pYILFUIM9Rdpi+QG2iWlABdD527Jjs27cv4EwqoArYIERFsoqlQmxbiqJYCkzmzp0rw4cPl06dOhlECxYskKysLE9c+JkZPXq09OjRQxCG75dffpFgP0f//Oc/ZfLkyVKzZk1Zu3atrF692nPcgiykYKogaXMuEiABEiABEiABEiABEkgwAhs3bpRGjRqZN0UgCoqFN6Z4QACPSfrmC/YYrSUrJ3CJNatoWbM/CZAACZAACZAACZAACZAACZAACZAACZBA8hOA9yd8ggkwbAruEHzoF8izlPbT80Hk4d0qUQ2iKXiSeuutt/y8S+V1P7EcK69ryI9+EAOFY0VVLKVsIJD67rvvNBv0eurUKZkxY0bQNnblwoUL7WyhpymYKvRbwAWQAAmQAAmQAAmQAAmQQPwSwJcjxCdPTU2VFi1ayKpVqxJeNAUBEPYCw94CvSETyV1JRk7Yf36wioQr25IACZAACZAACZAACZAACZAACZAACZAACRQdAhA54aOh8rp06RK2YMqmFI5YCt6odB70jda7lT1/YaUhdMJn/PjxZgm2dykNu+e1NoTsQz+YXpEOFKIPdYloc+bMcZaN++9lRV0s5cUkmcsomErmu8u9kQAJkAAJkAAJkAAJkEAMCMCVLlzq1qlTx3xJxttWiehtCuIfhJbTN8e2bt1q3ATHAJEZIlk4YTP5zSpWzDkOCZAACZAACZAACZAACZAACZAACZAACZBAchHA2aMKmXDFBwKocC0csZTXWJHM4dU/HstUQBWPa4uHNUEcpWaLqbTszjvvFHxoyUuAgqnkvbfcGQmQAAmQAAmQAAmQAAnEjABc5Z511lnG0xQERyo6itkEBTwQPEtB4BRrSzZO4JNfrGLNnuORAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAkkPgEIV+zwesG8TCHkHjwFaXuEXPMSvrip2H1QF26oNvc4zCcugVDPCcVSiXtvI1k5BVOR0GJbEiABEiABEiABEiABEijCBCAwgmvqxo0bG09N8EKUSHbgwAHjGWvjxo0xCcMXaO+Jzgn7KihWgRiynARIgARIgARIgARIgARIgARIgARIgARIoOgSgBDqiiuuMADgYQoCp0ACF5QHqvMiiPFUYIV6eJaKpL/XmCxLDAJ4jsaNGxd0sWgDsZT9jATtwMqEJkDBVELfPi6eBEiABEiABEiABEiABAqWQFZWliCmPS04AXIKzoe1JEACJEACJEACJEACJEACJEACJEACJEACJBCIAERM+EDcBFPxSrTCJoynQiyde968eZpMuCtC7o0cOTLh1l1YC8ZzBDGUehTT5woiKTUt0zyvyU2Agqnkvr/cHQmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAkkFAH1MhUr0RREMW4xDIQzEGYlqkEwpdauXTuZMmWKvPXWWzJ+/HgtztMVY0GIhatasrxEC8EUjQSUAAVTSoJXEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBuCAA708qmMKCIHjCB0KncL1NoX+XLl38xsFYkYyB9vFqEEjZXqaQxgdiKhU52cIqTdtiKOwN+bZt2/qJpOw9RyvCssdimgTihQAFU/FyJ7gOEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABQwDen8aNG2fC6AUSTqFhenq64ylK29WvX1/q1auXSyiF9skilsJeVMhki6ZQDgGUiqLcdagP1yCwgiiLRgLJSICCqWS8q9wTCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACSQBAYTn8wqp5w6xF2qrEGDBa1Uih+Hz2iNEU/iMGjXKz9uUV9twy1QopR6pwu0X7+1GjBgRtneyvOwFIf8Y9i8v5AqnDwVThcOds5IACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACYRBACH48PESToXqnqxCKfe+VThle5cKFmZP+6soCiH8kNa81ifLFd7Kwg3lmNc9Yw4KpvJKr+D7UTBV8Mw5IwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQIQEbOEUugbyMqVepJLRo1QoZMksegq192D1ENtB0JSfRrFUftKN/dgUTMWeKUckARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARLIJwLqKUivmKZBgwZJF24vn/AVyWEhrktLS5O5c+fm2/4DCfjybUIOHBUBCqaiwsfOJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEAChU1AvUoV9jo4f3wToKgpvu9PQa6uWEFOxrlIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIoDAJ0MNUYdLn3CRAAiRAAiRAAiRAAiRAAiRAAnFPoFH7BmaNuG5a9KtJ6zXuF88FkgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJ5CJAwVQuJCwgARIgARIgARIgARIgARIgARIo6gQgjuo5tpuoWEp59ByrKZEZr842memvZF9zapgiARIgARIgARIgARIgARIgARIgARIgARIgARIgARKIZwIUTMXz3eHaSIAESIAESIAESIAESIAESIAECpRAIKGU1yIgqILhCvFUrIVTpcuXkvpn15O6rWtLSq0U2bN5j2Ss2i7r5m7wWk7EZTWb1pAmXRpL1QaVZfPyrbJ+/ibZt31fWONUrFEhe21t6kipsiVl18ZdkrY4XTJWbw+rf1FoVKFCBWnQINs7WST73bp1q+zZsydol1KlSklqaqpp8+uvv8r+/ftztS9btqxp06hRIzly5Ihs2LBBNm7cKCdOnMjVNlRBs2bNpESJErJ7927Ztm1b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JEACJEACJEACJBA3BCiYiptbwYWQAAmQAAmQAAmQAAmQAAmQQPITiHfBlN6BUMIp9TyFK4VSSo1XEiABEiABEiABEiABEiABEiABEiABEiABEiABEkgMAnEnmKpcO1VSuw6VWk07SIVqdeUM3//y007JKdm/a4sv/MCPsm7u/8mejHX5OR3HJgESIAESIAESIAESIAESIIGICCC8EMJoBQqlFdFgETRGyDJ8NMxRBF2DNk0UwZR7E3b4PQqk3HSYJwESIAESIAESIAESIAESIAESIAESIAESIAESIIHEIhBXgql2F90sLXtdXagEV06fIEu/eqFQ18DJSYAESIAESIAESIAESIAESAAETpw4UeBCKTd5iKaKFy/uLs5zPlEFU3neMDuSAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAnEHYFi8bKibtc+VOhiKbCAYAtroZEACZAACZAACZAACZAACZBAYRKAsKigvUp57RdrCCVy8urHMhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARKIVwJxIZiCZ6kGbc6PG0ZYC9ZEIwESIAESIAESIAESIAESIIHCIADPUgjFFy+GtWBNNBIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARJIBgKFLpiqXDs1LjxLuW8mPE1hbTQSIAESIAESIAESIAESIAESKEgCECfFg2cp956xpngScbnXxzwJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJhEug0AVTqV2HhrvWAm8Xz2srcBickARIgARIgARIgARIgARIoEAIxKNYSjcez2vTNfJKAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAqEIFLpgqlbTDqHWWGj18by2QoPCiUmABEiABEiABEiABEiABPKVQDyLkuJ5bfl6Uzg4CZAACZAACZAACZAACZAACZAACZAACZAACZAACZBAUhEoUdi7qVCtbmEvIeD88by2gIvOx4oWLVpIp06dnBkmTpwoWVlZTj7cRLFixeTss8+WDh06SKtWraRs2bJy8OBByczMlIyMDElLS5OVK1fK5s2bwx2S7UiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEggLAKFLpg6Q84Ia6FejdYtnWGKp7z1sLlqPrVdT0lt18uUDRh5r1fXsMqiWZtO0KBBA2nevLlmZeHChbJ3714nr4mUlBTp16+fnHXWWbJo0SKZMmWKHD9+XKuda+PGjSU1NdXJL1iwIE+iJWeACBLDhg2TXr2yuaLbihUr5Mcff4xgBJFKlSrJSy+9JNhvKJs2bZr8+9//DtWM9SRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQNoFCF0yFvVKrIYRREEmpQMqqMkmUa9034x+WgaPuk2iEU+7xI8kPHz5c+vfv73T5+OOP5ZVXXnHymrjmmmvk0ksvNdkePXrIgQMHZPr06VrtXO+66y5p2rSpk3/44Ydl5syZTj6eE1WqVJHXXn1Vyvg8SoVja9asCacZ25AACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZBA2AQSTjAFIdQLd1/obFC9STVp29OUIT/lrUdMet3S6UY4BdFUYQmnZs+e7SeYatOmjbN2O9G+fXs7K7169vQUTNWrV8+v3bx58/zy8ZwZNGhQLrHU2rVrZdOmTSYsX5cuXaR48eLxvAWuzYNA7da1pO7ZtaRq4ypy9OAx2b0hU1Z+s0aOH8ntIc2ju19RseJnSI3m1aXeOXWlaqPKsntjpmxeslV2rNkpJ0+c8mtrZ1LqVpSGHetLjWbV5IwzzpA96Xtl9XfrZP+OA3YzpkmABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEhAEkowBSEUhE8wCKMGjLzPXN33Ub1J4QqBlXqj0r5a7+6XH3mEzDt16pQRcWB8hOjzsrp16/oVn9W6tV8emdKlSxthkVZkZWXJsWPHNBv31w4dOvit8YknnpCvv/7aKbvuuuvk2muvdfJMxDeBEqWKS78/95YGHfxFfFh15+vby9THZ8iGOWlhb6JspTIydNxgKV+1nNOncdeG0v6qdnJg5wH59K4v5fDew06dJs4dfrZ0uOYczTpX9Fs+cYXMfW2hU8YECZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACRRLFAS2WAoh9m7+72RPsZR7PxBWoS36wCCa0nB97rb5kT9+/Ljs3bvXGbpcuXJSqlQpJ4/EWWedlcuzUuXKlXO169y5s1+/devW+eXjPdOwYUNniRCR2WIpp4KJhCFw/h97OGKpowePStrCdNm+aodZf7ESxaTvPb2kcv1KYe2nZJkSMvyFoY5Y6sDug5K+ZIsczsoWSJWvXl6GP3+pQKRlW/N+qY5Y6uTJk7JlWYakL94iSMPaXNJKWg5sZndhmgRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIoIgTSBgPU+odCsKnvHiI0j4YcXSkWQAAQABJREFUByH9whVcxeL5gLDJ9q4E4dPMmTOdofv16+ekNYGwYj169JDvv/9ei6Rjx45OGon58+f75e0MPFlhnpSUFFmyZIksW7ZMIN7ysgoVKkitWrWcKqy3RIkS0q1bNyPmWrRoUdC5nI6+RJMmTRxvWijfsmWL1KlTx5RBLGZbamqqyR46dMi0s+vCSYe7x2rVqgkEaDB45EpL8/d6BNadOnUyrBAecM2aNX7TlyxZUmyx19atW+XgwYN+bexMuDxr164t5cuXN10DMbDbHDhwQDIyMpypsCasDbZz504jzCtWrJi0a9dOEOJxz549snz5clm1apXTJ1aJspXLSKMu2d7SMn/dI//n8/504ugJMzw8Tl1wf19zzzted458++9pIadt4AunV7Js9l5Wf79Opj812+nTzye8OrNbIylVrpQRaNleqzqNPNe0O370uHx8+yTZt22/yZerUlauenmYQLiFNSBEII0ESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEQCAhBFPwLgXLDsN3r0nn5Z/sEH3TnTB9qe0m52WYiPtA2GQLpiB8sgVT55yTO5wYJnELplq2bOk39/Tp0/3yyNx0001y6aWX+nmsGj58uGkHodA999zj5/EKFbfffrv06tXLtME/Tz31lCmDkAgG4dSoUaNMOtg/Dz30kBFpaRt4kfrnP/8p999/vxY5V4z9zDPPmDyEXEOGDHHqQiUi3ePf/vY3adYsx8vQ5Zdf7id46tmzp/z1r391psVabHHZ4MGD5be//a1T/9prr8mHH37o5N2JcHk+++yzoiKyo0ePmvvmHuv555+XMmXKmOLDhw/LZZdd5jR58cUXnfQsnwBv+44dMnToUD/BGhpAtHbffff5ia2cjnlM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SjlDEiABEiABEiABEiABEiABEiABEiABEiABEiABEsjdBHJNSj4nZrhJ4QfhFAIp+rSblHaRgkgKocuqwH+QAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAnkWQI5Lpi6KBcln+d/gQZEUwi9DHQct36YG4MESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCB8CETk9KGcPLwvp6fgdf+hPDevk+YGEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABrwRyXDB1YMtyr5PL6Q2hPLecZsP9kwAJkAAJkAAJkAAJkAAJZA2BiIgc/79pXg8slOfmddLcQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIOAjmekm/rku+lRvObHNMKjSLmxiABEiABEiABEiABEiABEiCB7CQAUVJycnJ27tLvfVEw5TcqNiQBEiABvwgULFhQihUrJoULF5ZChQpJgQIFJDIy0q++Fy5ckPPnz8vZs2fl9OnTkpCQIImJiX71ZSMSIAESIIHQI8B3QuidE86IBEiABEiABEiABEggvAnkuGDqWNxW2TD/K6nT5q6QIo05YW4MEiABEiABEiABEiABEiABEshOAvny5ZNQFE1hTpgbgwRIgARIIPMEoqOjJSYmRooUKRLwYBBW4QehVcmSJdU4p06dkiNHjsjRo0cDHpcdSYAESIAEspcA3wnZy5t7IwESIAESIAESIAESIAFNIMcFU5jIyukfSJGY8lLp8rZ6Xjm63L1mrppTjk6COycBEiABEiABEiABEiABEsizBPAB/OLFi+oXChAglPLX8SQU5ss5kAAJkECoEihatKhUqFBBiZwwRzgKwiHq3Llz6gfXKH9dBiFkxbM5KipK/SCcggALvzJlysi+ffvk5MmToYqC8yIBEiCBPE+A74Q8fwkQAAmQAAmQAAmQAAmQQA4TyNe0adOLOTwHa/cNuzyY405TcJaCgItBAiRAAiRAAiRAAiRAAiRAAjlNICMfzrNqrvqDfLDGT0pKSneow4cPp7udG0mABEggtxIoV66cxMbGqukjlR7coPALZmjBFFL7IQ4ePChxcXHB3AXHIgESIAES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AAIIIIAAAggggAACCCCAAAIIIIAAAggUuEB8fLy5Z506dcRWZCrwSeTDDfVZ9Jm02WfMh9sUypAEpgqFnZsigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIRIJAcnKy7NixwzxKixYtIiI0pWEpfRZt+mz6jJHUoiLpYXgWBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQKWmDLli1SpkwZqVKlinTo0MGEjPbu3SuHDx8u6Knk6X4alNJl+GxlqX379ok+W6Q1AlOR9o7yPAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIFLrBmzRpp0KCBCRtp4MiGjgp8IiG6oVaWisSwlPIQmArRJwnDIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCJzZAhow2rNnj9SsWVNiYmJM1amiJJKSkiIJCQkSHx8fccvwud8HAlNuDbYRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEMiDQHJysmzcuDEPI3BpfgsUz+8bMD4CCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEC4CBKbC5Z1gHggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJDvAgSm8p2YGyCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEC4CBCYCpd3gnkggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAvgsQmMp3Ym6AAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAAC4SJAYCpc3gnmgQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAvkuQGAq34m5AQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCISLAIGpcHknmAcCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjkuwCBqXwn5gYIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCA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sCjvkdYtESBAgAABAgSaokA6ovymm26KF198Mf72t79VCqJqivM1JwIECBAgQIAAAQIECBAgQIAAAQIECLREgbL+/ftX85+3W+JSm8aaTvnlmys1keKdotKNKZBq8Ja75ftIgVCFwKj3XnsyH1RVKKcGaZeq1H5l0rVHD1mZ5k2mbQq42W677aJz5875QKlp06Y1mbnV1UQae43du3ePdMRcIYgtBYrdcccdK7W8tHPVPvvsU+WewYMHR3qlYLq0e9fKpsa2STsGpONVkk/awatgtLLr0J4AAQIECBAgUAoCPXr0WO4yG3vnpb322is/v/vuu2+581yZi+nfUy655JK49dZbo/iXGIoDptIve7z99tsr022dt23Xrl2d9Dlk06Fx8c+uzvc1e9bMOPLQA+ukX50QIECAAAECBAgQIECAAAECBAgQIFDaAoXvb2s62WxldOwwtTJajdA2BUWlVyFwKgVEFYKiCkfuLTutFCRVm92olr2vOZfTjj7PPfdcc17CCufe2GuszyC0UaNGRXqtampsm5kzZ8Zjjz22qtN3HwECBAgQIECAQAsXmDNnTv5Y5xa+zGx5Hdp3yPLFxy9nlTIECBAgQIAAAQIECBAgQIAAAQIECBBoZAEBU438AGo7fHHgVLonHdFXCJwq7CKVgqRSKpTzBX8QqCeBKVOmxIgRI1aq91dffXWl2mtMgAABAgQIECBAgEDzE+g3YGA26Xkfz8vyMgQIECBAgAABAgQIECBAgAABAgQIEGgqAgKmGvhJVERFlOX+t6opBU6lVHhf1X6quy/NTSJQW4F0dN43v/nN2jbXjgABAgQIECBAgACBEhH44h57Ziv9aMrKH7md3SxDgAABAgQIECBAgAABAgQIECBAgACBehIQMFVPsDV1O/PDsdGt19Lftq2pXWPUp7lJBAgQIECAAAECBAgQIECgtgJlZWXRuXOXaNe+fQzeYKPY/6Cvxbr9B2S33/XbX2d5GQIECBAgQIAAAQIECBAgQIAAAQIECDQVAQFTDfwkxo18tskGTKW5SQQIECBAgAABAgQIECBAoLYCu39hjzjhlNOrNJ8xfXr87te3xRsjX69yTQUBAgQIECBAgAABAgQIECBAgAABAgQaW6BVY0+g1MYf/uidTXbJTXluTRbNxAgQIECAAAECBAgQIFDCAq1aVf1aYcH8+THqnbfipf8+X8Iylk6AAAECBAgQIECAAAECBAgQIECAQFMWsMNUAz+dj8a/HS/987bYdo9jG3jk5Q+X5pTmJhEgQIAAAQIECBAgQIAAgdoKPP/MU7GwvDw6dOgYG2y0UXz6M7vnj+fb9lM7xu2/uTuOOeKQmDVzZm27044AAQIECBAgQIAAAQIECBAgQIAAAQINIlD1V0EbZNjSHuTpe66Id/77zyaDkOaS5iQRIECAAAECBAgQIECAAIGVEZg1a2b855GH4sG//yWu+/nlcfzR34iFCxfmu2jVunV8+4RTV6Y7bQkQIECAAAECBAgQIECAAAECBAgQINAgAgKmGoS56iAP3nhafqepqlcatibtLJXmIhEgQIAAAQIECBAgQIAAgdUVmD5tWjz68L+ybvoPGJjlZQgQIECAAAECBAgQIECAAAECBAgQINBUBBzJ14hPIu3q9NYzD8Tmux8a/TfdKdbsNSDKcv+rz1QRFTHzw7ExbuSzMfzROx3DV5/Y+iZAgAABAgQIECBAgEAJCrw67KX44pf3zK+8c5cuJShgyQQIECBAgAABAgQIECBAgAABAgQINHUBAVON/IQ+Gv92/Oc3P2nkWRieAAECBAgQIECAAAECBAjUjcCM6dOyjlq1srF1hiFDgAABAgQIECBAgAABAgQIECBAgECTEfDNZZN5FCZCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEB9C9hhqr6F9U+AAAECBAgQIECAAAECJS+w4YYbxplnnlnJoaxs6ZHs5513XsybNy+7fuedd8bjjz+elZtTZtGiRdl0W7VqneVlCBAgQIAAAQIECBAgQIAAAQIECBAg0FQEBEw1lSdhHgQIECBAgAABAgQIECDQYgX69u0b/fr1q3F9PXr0qHRto402arYBU5MmTszW0r5DhywvQ4AAAQIECBAgQIAAAQIECBAgQIAAgaYi4Ei+pvIkzIMAAQIECBAgQIAAAQIEWqxA8a5LtVnkyravTZ8N1Wb27FkRFRX54dq0aRO7fmb3hhraOAQIECBAgAABAgQIECBAgAABAgQIEKiVgB2masWkEQECBAgQIECAAAECBAgQWHWBp556Kvbcc89V76CZ3fnBxAmxzrpLdtQ67azvx7HHnxRzZs+OMWNGx88u/kkzW43pEiBAgAABAgQIECBAgAABAgQIECDQ0gTsMNXSnqj1ECBAgAABAgQIECBAgACBRha48rKLYtHChdksOnfpEn1yxxJuvvmWWZ0MAQIECBAgQIAAAQIECBAgQIAAAQIEGkvADlONJW9cAgQIECBAgAABAgQIECDQQgVGvzcqvn7gXrHv/gfHeusPjh5rrx1duq4Zkz6Y2EJXbFkECBAgQIAAAQIECBAgQIAAAQIECDQnAQFTzelpmSsBAgQIECBAgAABAgQIEGgmAmmHqfvuubOZzNY0CRAgQIAAAQIECBAgQIAAAQIECBAoJQFH8pXS07ZWAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUuIGCqxD8Alk+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAQETJXS07ZWAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUuIGCqxD8Alk+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAQETJXS07ZWAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUuIGCqxD8Alk+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAQETJXS07ZWAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUuIGCqxD8Alk+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAQETJXS07ZWAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUuIGCqxD8Alk+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECglAQETJXS07ZWAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUu0KbE12/5BAgQIECAAAECBAgQIECAAAECBBpVoFOnTrFO33Wjd591om3bdvH4f/7dqPMxOAECBAgQIECAAAECBAgQIECgpQsImGrpT9j6CBAgQIAAAQIECBAgQIAAAQIEmqTAAQcfGvvuf1B06ty50vyeefqJKF+woFKdAgECBAgQIECAAAECBAgQIECAQN0JCJiqO0s9ESBAgAABAgQIECBAgACBWgmsvfbasSAX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vzni9Vj26MriG4pd0w5tK5PS8/zeGSfHjddeFQty/9ypKaWf5+eefjJeeP6ZmposrV/m2VdnuLSxHAECBAgQIECAAAECBAgQaH4CZf3796/527Pmtx4zJkCAAAECBAgQIECAAIEmLJCOoVtempo7wqsx0157LTlmrD52eerevXv+eL7evXtHr1698sdTjRo1Kt5+++1o7HUn83R8oEQgCXTvsXbuWLO78hgpwOIbB+2dz288ZNPYYaddYnLuCLDnn3kqZsyoOUAkf8Mnf3Ttumb+OLDBud2zRo54LV547pmoKfik+D55As1NoG3u79GhQzePIUM3i3Zt2+WOYB0f748eFaPeebtBl5J+5jbbYsvcjlObRPoZHj9ubLz7zlv5YwobdCIGI0CAAAECBAgQIECAAAECdSxQ+P72r3/962r37Ei+1SbUAQECBAgQIECAAAECBAgQWLHAtGnT4tlnn11xQy0INFGBt94cGem1smnWrJnx+H/+nX+t7L3aE2hOAumIymGvvJR/Nea808/cM7md2NJLIkCAAAECBAgQIECAAAECBKoXcCRf9S5qCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBogQICplrgQ7UkAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgSqFxAwVb2LWgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEWqBAmxa4JksiQIAAAQIECBAgQIAAAQIECBBYRYHyBQtiwfz5+bunTZ26ir24jQABAgQIECBAgAABAgQIECBAgEDTFRAw1XSfjZkRIECAAAECBAgQIECAAAECBBpcYPbsWfH1A77a4OMakAABAgQIECBAgAABAgQIECBAgEBDCTiSr6GkjUOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQKMLCJhq9EdgAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQINJSAgKmGkjYOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKNLiBgqtEfgQkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINBQAgKmGkraOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQINLqAgKlGfwQmQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAQwkImGooaeMQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQINDoAgKmGv0RmAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAg0l0KahBjIOAQIECBAgQIAAAQIECBAgQKC+BHbcZddo3ap1TJw4PsaPGxvlCxbU11D6JUCAAAECBAgQIECAAAECBAgQIECgmQsImGrmD9D0CRAgQIAAAQIECBAgQIAAgYgzz/lhlLUqyygWlpfHQ//8e9x+8w1ZnQwBAgQIECBAgAABAgQIECBAgAABAgSSgCP5fA4IECBAgAABAgQIECBAgACBFifQpm3b+Mpe+8aXvrJXi1ubBREgQIAAAQIECBAgQIAAAQIECBAgsHoCAqZWz8/dBAgQIECAAAECBAgQIECAQBMQOOJr+8YJxx4RV156YUyfOjWb0cFfPzzLyxAgQIAAAQIECBAgQIAAAQIECBAgQCAJOJLP54AAAQIECBAgQIAAAQIECDSSwMknnxw77rhjNvrFF18cI0eOzMoytReYO3dupNfkSR/EuLFj4uobb8vf3Llz59p3oiUBAgQIECBAgAABAgQIECBAgAABAiUhIGCqJB6zRRIgQIAAAQIECBAgQIBAUxMYMmRI7LHHHpWm1b1790plhVUTSAFTUVERUVYW6Wg+iQABAgQIECBAgAABAgQIECBAgAABAsUCjuQr1pAnQIAAAQIECBAgQIAAAQINJPDDH/6wgUYqzWEWL15cmgu3agIECBAgQIAAAQIECBAgQIAAAQIEViggYGqFRBoQIECAAAECBAgQIECAAIG6FfjWt74V3bp1q9tO9UaAAAECBAgQIECAAAECBAgQIECAAAECtRIQMFUrJo0IECBAgAABAgQIECBAgEDdCPTu3Tv22WeffGfjxo2LhQsX1k3HeiFAgAABAgQIECBAgAABAgQIECBAgACBWgkImKoVk0YECBAgQIAAAQIECBAgQKBuBC644IIoKyuLioqKuPDCC+umU70sV6B9+/bLve4iAQIECBAgQIAAAQIECBAgQIAAAQKlJSBgqrSet9USIECAAAECBAgQIECAQCMK7L333tGvX7/8DB577LGYMGFCI86mZQ9dXl6eLXDQeoOzvAwBAgQIECBAgAABAgQIECBAgAABAgQETPkMECBAgAABAgQIECBAgACBBhDo3LlzHHPMMfmR5s+fH1dffXUDjFq6Q8yePStb/H4HfS3LyxAgQIAAAQIECBAgQIAAAQIECBAgQEDAlM8AAQIECBAgQIAAAQIECBBoAIHzzjsv2rRpkx/ppptuioULFzbAqKU7xP333ZMtfvsddorv/fCC2Gqb7aJbt7UiBa9JBAgQIECAAAECBAgQIECAAAECBAiUrsCSb2pLd/1WToAAAQIECBAgQIAAAQIE6l1ghx12iM033zw/TjqG76GHHqr3MUt9gAf/9pfo0KFD7HvAIdG5S5dIQVPpVUinnXBsjBs7plD0ToAAAQIECBAgQIAAAQIECBAgQIBACQnYYaqEHralEiBAgAABAgQIECBAgEDDC6Rdpc4666z8wBUVFXHRRRc1/CRKdMTXhr0cEyeMr3Y3r1atWpeoimUTIECAAAECBAgQIECAAAECBAgQIGCHKZ8BAgQIECBAgAABAgQIECBQjwKnnXZadOzYMT/Ck08+GWPHjq3H0XRdENhw4yFx6ZXXFYrx8bx58eTjj8aod9+JheXlMX6855DhyBAgQIAAAQIECBAgQIAAAQIECBAoMQEBUyX2wC2XAAECBAgQIECAAAECBBpOYIMNNojPfe5z+QEXLFgQV111VcMNXuIjnXDy6ZnAnNmz47gjvx7z58/P6mQIECBAgAABAgQIECBAgAABAgQIEChdAQFTpfvsrZwAAQIECBAgQIAAAQIE6lngm9/8ZjbCzJkz49RTT83KhUzr1kuPhttvv/1ixx13jLlz58aNN95YaOJ9FQTWXrtndtfv7rhdsFSmIUOAAAECBAgQIECAAAECBAgQIECAgIApnwECBAgQIECAAAECBAgQIFBPAq1atcp67tmzZ7bbVFa5TGbIkCGRXhUVFQKmlrFZ2WL7Dh2yW1587pksL0OAAAECBAgQIECAAAECBAgQIECAAIGl39yyIECAAAECBAgQIECAAAECBAi0EIGysrJsJTNmTM/yMgQIECBAgAABAgQIECBAgAABAgQIELDDlM8AAQIECBAgQIAAAQIECBCoJ4HLL788evfuvdzeU5vCsXy///3v46WXXooFCxYs9x4XCRAgQIAAAQIECBAgQIAAAQIECBAgQGDVBQRMrbqdOwkQIECAAAECBAgQIECAwHIFZsyYEem1vJSO3yuksWPHxltvvVUoel8NgbIym2qvBp9bCRAgQIAAAQIECBAgQIAAAQIECLRoAd8etujHa3EECBAgQIAAAQIECBAgQKD0BFq3aRNlrT45kq8oIK30JKyYAAECBAgQIECAAAECBAgQIECAAIHqBARMVaeijgABAgQIECBAgAABAgQIEGi2AkcceWw293nz5mV5GQIECBAgQIAAAQIECBAgQIAAAQIECCQBR/L5HBAgQIAAAQIECBAgQIAAgUYUKD6Sb9GiRY04k+Y99E+vuDa6dVsruq65ZnTo2DFbzKh33s7yMgQIECBAgAABAgQIECBAgAABAgQIEEgCAqZ8DggQIECAAAECBAgQIECAQCMK7Lvvvo04essZesONhiw9hu+TZX2c213qyssuajmLtBICBAgQIECAAAECBAgQIECAAAECBOpEQMBUnTDqhAABAgQIECBAgAABAgQIEGhMgTHvvxft27ePWbNmxfRpU+Pl/74Qjzz8z8ackrEJECBAgAABAgQIECBAgAABAgQIEGiiAgKmmuiDMS0CBAgQIECAAAECBAgQIECg9gJnnHJ87RtrSYAAAQIECBAgQIAAAQIECBAgQIBASQu0KunVWzwBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUlIGCqpB63xRIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAobQEBU6X9/K2eAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQEkJCJgqqcdtsQQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRKW0DAVGk/f6snQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUFICAqZK6nFbLAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHSFhAwVdrP3+oJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlJSAgKmSetwWS4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKC0BQRMlfbzt3oCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECJSUgYKqkHrfFEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEChtAQFTpf38rZ4AAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBASQm0KanVWiwBAgQIECBAgAABAgQIECBQpwKbbLpZ9OrdJyZP/iAmjBsXs2fPqtP+dUaAAAECBAgQIECAAAECBAgQIECAAIG6FhAwVdei+iNAgAABAgQIECBAgAABAiUkcMrp50SvPn2yFVcsrojhr74SV152YcyZMyerlyFAgAABAgQIECBAgAABAgQIECBAgEBTEXAkX1N5EuZBgAABAgQIECBAgAABAgSaoUBFVFSadVmrsthi623ijHN+WKlegQABAgQIECBAgAABAgQIECBAgAABAk1FwA5TTeVJmAcBAgQIECBAgAABAgQIEGiGAqeecGx06dI1+q7bLw762jdisy22yq8iBU2VlZVFRUXlgKpmuERTJkCAAAECBAgQIECAAAECBAgQIECghQkImGphD9RyCBAgQIAAAQIECBAgQKBpCay55ppx44031mpSL7zwQlxzzTW1attUGpUvWBDTpn6Uf73+2rC4+8//iDZt2+an16//gBg3dkxTmap5ECBAgAABAgQIECBAgAABAgQIECBAIC8gYMoHgQABAgQIECBAgAABAgQI1KNAx44do1u3brUaYdCgQbVq15QbzZo1M7r3WDs/xf4DBgqYasoPy9wIECBAgAABAgQIECBAgAABAgQIlKiAgKkSffCWTYAAAQIECBAgQIAAAQKNI7C8I+r+97//Nc6k6nDUj+d9nPXWsdMaWV6GAAECBAgQIECAAAECBAgQIECAAAECTUVAwFRTeRLmQYAAAQIECBAgQIAAAQItXmD27Nnxta99rUWvsyIqWvT6LI4AAQIECBAgQIAAAQIECBAgQIAAgeYv0Kr5L8EKCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUDsBAVO1c9KKAAECBAgQIECAAAECBAgQWEmBdu3areQdmhMgQIAAAQIECBAgQIAAAQIECBAgQKD+BQRM1b+xEQgQIECAAAECBAgQIECAQMkIzMkdO1hI/foPKGS9EyBAgAABAgQIECBAgAABAgQIECBAoMkItGkyMzERAgQIECBAgAABAgQIECDQwgU6d+4cv/zlL6Nr166xcOHC+PDDD2PMmDFx1113xcSJE1vE6idP+iA23mTT/Fp23mW3uPUX17WIdVkEAQIECBAgQIAAAQIECBAgQIAAAQItR8AOUy3nWVoJAQIECBAgQIAAAQIECDRxgbKysujdu3d07NgxunTpEoMHD47dd989brnlljjuuOOa+OxrN70//P6OiIqKfOOu3brFtTf9MnbZ9bPRK7fuLl26RqtWvoqonaRWBAgQIECAAAECBAgQIECAAAECBAjUl4BvKetLVr8ECBAgQIAAAQIECBAgQKAagYpcMFF6FacUSLXPPvvEOeecU1zdLPNph6mLfnJuTJk8OR84tW6//nH6OefGL27/Xfz6rvviG0cc3SzXZdIECBAgQIAAAQIECBAgQIAAAQIECLQcAUfytZxnaSUECBAgQIAAAQIECBAg0AQFPv7445g+fXr85z//ifvvvz+mTp2an2XaYerggw+O/fbbL1LAVEq77bZb/OlPf4p33nknX26uf7z79lsx+r13o3NujR07daq0DDtMVeJQIECAAAECBAgQIECAAAECBAgQIECgEQQETDUCuiEJECBAgAABAgQIECBAoHQEZsyYEYcddliVBc+ePTtuv/32ePTRR+O6667LgqZOPvmkOOWUU6u0by4V7du3j1vvuCvatmuXn3LF4op49ukn4q03RsT//ve/GP7asOayFPMkQIAAAQIECBAgQIAAAQIECBAgQKCFCgiYaqEP1rIIECBAgAABAgQIECBAoHkIjB49Op566qnYdddd8xPu3btP85h4DbP82mFHZsFSubMH47snfSvGjR1TQ2vVBAgQIECAAAECBAgQIECAAAECBAgQaHiBVg0/pBEJECBAgAABAgQIECBAgACBYoHnn38+K66xxhpZvjlmNtpoSDbtYa+8JFgq05AhQIAAAQIECBAgQIAAAQIECBAgQKCpCAiYaipPwjwIECBAgAABAgQIECBAoGQFPvzww2ztZWVl0aZN890Qusuaa2ZrGTH81SwvQ4AAAQIECBAgQIAAAQIECBAgQIAAgaYiIGCqqTwJ8yBAgAABAgQIECBAgACBkhXYeOONs7WXl5fHwoULs3JzzsycObM5T9/cCRAgQIAAAQIECBAgQIAAAQIECBBooQICplrog7UsAgQIECBAgAABAgQIEGg+AnvssUc22enTp2d5GQIECBAgQIAAAQIECBAgQIAAAQIECBCoewEBU3VvqkcCBAgQIECAAAECBAgQIJAJnHfeebH55ptn5WUzJ5xwQqy77rpZ9csvv5zlm2Om0nGCFRXNcQnmTIAAAQIECBAgQIAAAQIECBAgQIBACxdo08LXZ3kECBAgQIAAAQIECBAgQKBRBXbaaadIr9mzZ8c777wTEyZMiBkzZsTAgQNjq622iq5du2bzS22uv/76rNwcM2t0WiOb9uzZs7K8DAECBAgQIECAAAECBAgQIECAAAECBJqKgICppvIkzIMAAQIECBAgQIAAAQIEWrRAly5dYptttsm/qlvowoUL4yc/+UlUNONdmTbZdLPoXBQANvL116pbqjoCBAgQIECAAAECBAgQIECAAAECBAg0qoCAqUblNzgBAgQIECBAgAABAgQItHSBsWPHRv/+/aOsrKzGpb744otx2WWXxbx582ps01QvfOfk02Po5ltE586do0vXNbNpzp0zJ+bkXhIBAgQIECBAgAABAgQIECBAgAABAgSamoCAqab2RMyHAAECBAgQIECAAAECBFqUwHe+8518sNQWW2yRP4avZ8+e0b59+5iYO5pvxMiRMWrUqFi8eHGzXfMWW24dvfr0qTT/xYsWxXVXX16pToEAAQIECBAgQIAAAQIECBAgQIAAAQJNRUDAVFN5EuZBgAABAgQIECBAgAABAi1WIB2z9+qrr+ZfLW2Rb4x8PR/wNWfO7Jg5Y3q89967cd89d0X5ggUtbanWQ4AAAQIECBAgQIAAAQIECBAgQIBACxEQMNVCHqRlECBAgAABAgQIECBAgACBxhC49qrLGmNYYxIgQIAAAQIECBAgQIAAAQIECBAgQGCVBVqt8p1uJECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQDMTEDDVzB6Y6RIgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgsOoCAqZW3c6dBAgQIECAAAECBAgQINDCBMrLy/MratPGCfYt7NFaDgECBAgQIECAAAECBAgQIECAAAECzVig8J1t4Tvc1V2KgKnVFXQ/AQIECBAgQIAAAQIECLQYgblz5+bX0rFjxxazJgshQIAAAQIECBAgQIAAAQIECBAgQIBAcxcofGdb+A53ddcjYGp1Bd1PgAABAgQIECBAgAABAi1GYObMmfm1dOvWrcWsyUIIECBAgAABAgQIECBAgAABAgQIECDQ3AUK39kWvsNd3fUImFpdQfcTIECAAAECBAgQIECAQIsRmDp1an4tffr0aTFrshACBAgQIECAAAECBAgQIECAAAECBAg0d4FevXrll1D4Dnd11yNganUF3U+AAAECBAgQIECAAAECLUZg8uTJ+bUMGDAgCls8t5jFWQgBAgQIECBAgAABAgQIECBAgAABAgSaoUD6rrZv3775mRe+w13dZQiYWl1B9xMgQIAAAQIECBAgQIBAixEoLy+PsWPH5tczcODAFrMuCyFAgAABAgQIECBAgAABAgQIECBAgEBzFSh8V5u+u03f4dZFEjBVF4r6IECAAAECBAgQIECAAIEWIzB69Oj8WoYOHRpdunRpMeuyEAIECBAgQIAAAQIECBAgQIAAAQIECDQ3gfQd7YYbbpifduG727pYg4CpulDUBwECBAgQIECAAAECBAi0GIFZs2bFqFGj8uvZcsstW8y6LIQAAQIECBAgQIAAAQIECBAgQIAAAQLNTWCzzTbLTzl9Z5u+u62rJGCqriT1Q4AAAQIECBAgQIAAAQItRmDkyJExc+bM6N27d2yxxRYtZl0WQoAAAQIECBAgQIAAAQIECBAgQIAAgeYisPnmm8faa6+d/642fWdbl0nAVF1q6osAAQIECBAgQIAAAQIEWozAK6+8EuXl5fntngVNtZjHaiEECBAgQIAAAQIECBAgQIAAAQIECDQDgRQsNWjQoPx3tOm72rpOrddcc82f1HWn+iNAgAABAgQIECBAgAABAtUJdOrUqbrqrG7evHlZvrEzCxYsiGnTpsU666wTPXv2jB49esT06dMj1bfE1Lp165a4LGsiQIAAAQIECBAgQIAAAQIECBAgQKAZCXTp0iW23Xbb6Nu3bz5Y6oUXXsjvMFXXSyjr379/RV13qj8CBAgQIECAAAECBAgQIFCdQAo6Wl6aOnXq8i43yrX0L+hbb7115H7hKD/+iBEjYsyYMdGUgrvqAqZdu3Z10Y0+CBAgQIAAAQIECBAgQIAAAQIECBAgsNICHTt2jIEDB+Z3/E83z5z5/+zdB5wVxf0A8BFpSlUQFAREVKygsWvsvcfY9W9v0aixG5OoUWNirFETe9dgSTHGaOy9d8ESGyqCCIIU6c3/m8Xde+/u3XHl3XHlO5/P8WZnZ2dnvrtv9d773cykEGeW+u6772rcVnUOEDBVHSV1CBAgQIAAAQIECBAgQKAkAk0xYCod+KqrrhoGDBiQboYRI0aEr7/+OkycODEJnpozZ062rylmBEw1xaumzwQIECBAgAABAgQIECBAgAABAgSapkDr1q1DDJLq2rVr6NGjRzKjVDqSTz/9NLz//vvpZr28CpiqF1aNEiBAgAABAgQIECBAgEAxgaYcMBXH07lz59C/f//Qt2/fYsNTRoAAAQIECBAgQIAAAQIECBAgQIAAAQK1FIh/pPrZZ5+FyZMn17KF6h8mYKr6VmoSIECAAAECBAgQIECAQB0FmnrAVDr8Nm3ahJ49e4Y4nrhU3+KLLx5imUSAAAECBAgQIECAAAECBAgQIECAAAECCxaYPXt2mDZtWrL03vjx48OYMWNCLGuo1LqhTuQ8BAgQIECAAAECBAgQIECguQjEX9xHjhyZ/DSXMRkHAQIECBAgQIAAAQIECBAgQIAAAQIEWopAq5YyUOMkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQICAgCn3AAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECLUZAwFSLudQGSoAAAQIECBAgQIAAAQIECBAgQIAAAQIE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gRhYGX9ikOXAgQOTZ8nU3DOlVDNNNZfnU307NZabqFVj6Yh+ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBygS6duicLcMnWKoyJeUECCxIIAYExWdITHFpz/hsqWtqjs+n+nCqq3MpjxcwVUpNbREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAvQgM6LVc0m5chi9+kS8RIECgtgLxGRKfJTGlz5batpXfRnN7PpXaqS7GpT5WwFSpRbVHgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUX6LlE96TNuJyWRIAAgboKpM+S9NlSl/bSNtI269JWYzs2HVM6xsbWv9r2R8BUbeUcR4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQINJtBxsQ7Jucwu1WDkTkSgWQukz5L02VKXwaZtpG3Wpa3Gdmw6pnSMja1/te2PgKnayjmOAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEmJyBgqsldMh0mQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKC2AgKmaivnOAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEmpyAgKkmd8l0mAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB2goImKqtnOMIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGhyAq2bXI91mAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAzFhg8eHA48MADazXCoUOHhttvv71Wx7aUgwRMtZQrbZwECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKNXuCggw6qdbBUHFwabHXqqaeGd955p9GPd2F00JJ8C0PdOQkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUE0iDncoV12qztjNU1epkTewgM0w1sQumuwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAs1TIAZM5ac77rgjf3OB+UGDBiUzTC2wYguvIGCqhd8Ahk+AAAECBAgQIECAAAECBBqrwKorrRI2WX+j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VlVB60V9pFOVj30ECBAgQIAAAQIECBCoT4Fhw4ZlzU+ZMiUMGTIk265upmPuD4ZOOOGECtVXXnnlEAOpHn744Qr7FBBoCQKXXHJJMsw77rhjgcGIsW4aMHXqqae2BB5jLKGAJflKiKkpAgQIECBAgAABAgQIECDQGARiIFQMiKosNYZgqdi3GBQVl9Irn0495sTyRdl269atw8brbphtp5kvRo5Is0VfKwteKlq5RIV9l624REPHXEBY2zZtC84Ql+mbMnVKQVn+xqKtFs3fTPJ9ei9boUwBAQIECBAgQIAAAQIECDSMwHvvvZedqEuXLmHttdfOtstnllxyyfJFyfbvf//70OaHP6gZN25cuPPOO7N6MZCqc+fO2bYMgZYikM4uFcd74IEHhoMOOqjSoafBUrHC0KFDK61nB4HKBARMVSajnAABAgQIECBAgAABAgQINGGBYkFT6RJ8C3tmqXzW5199MX8zyW++0SZh7UFrVSiPBZf99o9h0UUrBhBdfv2fs/rFZpvqvmS3bH+aKRZ4le6r6rV1kfMXq7/qiitXKD5gj30rlE2fXrac4KTJkyrs71skOGrg8itWqFedgjatK85uVZ3j1CFAgAABAgQIECBAgACBMoHZs2eH4cOHZwW//vWvQ7HAqL333jvcc8894dJLLw3xD4DStO2224ZVVlkl3Qxnn312uO2228K3336blMVAqt/97nfZ/jQzderUNBvat2+f5WUINBeBuLRenFkqTZUFTeUHS8W6luRLxbzWREDAVE201CVAgAABAgQIECBAgAABAk1IIAZGxSX64oxSabBUfG1M6YwLzgpz584t6FKrRVqFW/50XThwz/1DnJEppgH9lg+35sq23Hizgrpx44XXXgofDf84K3/nvYp/VbhMj6XDWqsPzurE9vbZbc9su7LMd0VmfurTa9kQ+7igFD8M33qTLbNq8ZzHHHRktp1m3vvwgzSbG8cnWT7NbLXJFqFjh47pZthtu52LBo1lFX7ITJ8+o3xR+NEaa1YoU0CAAAECBAgQIECAAAECNReIQU7prMmdOnVKZog6+aSTkuX0DjvssHDttdeGI4+c/zvgoEGDQvyJqUOHDuHEE8tmVn7uuefCxx/P/5323HPPzToSA6ri0nz5afLkySEGa8UUf+c8//zzw+677x623LLsd8/8+vIEmqLA7bffXmXQVPlgqbgUn4CppnilF36fy8JYF35f9IAAAQIECBAgQIAAAQIECBAosUAMkGpsQVL5Q5z83eRw6713hsP3Ozi/OAlIOuPnJ4f4EwOqis0qFQ+I+8743W8Kjv08tzxf/NC6/DJ8t195YxjzzdjQNbdcQvt21ftL3Gdfej6Ess+xk/N06tgpvPLQM2FS7oPqT78YHo4+/fiC8+dv/Om8i8KMmTNyS+5NDd2WWLJCn2LdC664KDvkvQ/fz/JpZrH2i4UX7n8ifDN+XFiqW/dKLdL66evrQ98MG66zfrqZvK631jrh+Vxb8QP2u+//W7jujpsK9tsgQIAAAQIECBAgQIAAgeoJjBkzJlx11VXhuOOOC61atUqW19thxx1D/CmfhgwZEt58882k+IILLsiW4ps1a1a48MILs+rvv/9+eOmll8KGG85fij4uzff888+HKVPKlnEfOXJk6N+/f3LMBhtsEOLPJ598Ep588smsHRkCTV0gBk3FFGeYyn+NgYf5y/YJlkp4/FNLgQX/OWQtG3YYAQIECBAgQIAAAQIECBAgQKA6Alfc8JfwwccfVlq1smCpGBR14Z8vDROLLGP3+jvzP4jObzQGUC3do2e1g6XisaPHfh1mz5n/17v5bcUgpthWnG1qQSkGZ8UlAcsHcMXj4sxYMegqTU+98GyYMq1siYW0PBrE81VmkdbLf41tFUtdO3dJAq969Vym2G5lBAgQIECAAAECBAgQaLECc+bMycY+b968LF9Z5oEHHggHHXRQGDFiRChWf/z48eG0004Lt9xyS9LE2muvHVZbbbWsuSuuuCLEoKn8FAOq0lmk4tJ8cbm//HTyySeHiRMn5hcV/X2zoIINAk1QoNhMU4KlmuCFbMRdNsNUI744ukaAAAECBAgQIECAAAECBFqCwLzv54W9jjogHHvwUeGYg4+s1ge9MUjq0BOPCh9/9mlRopN/e0Z4+h+PVBlgNGv2rNC2Tduix+cX/vuRB8MeO/0kvyjLx78irm36aszocNAJhUv0RYuz/nhuuPzcslmnirVfbAat8vViMNYXudm2+i3bt/yuZLtYAFfRigoJECBAgAABAgQIECDQQgRGjx4dttlmmxqNNs40dfjhhyfH9OvXLwwYMCB888034YMPPgj5AVixwhtvvLHA9mfOnBl2LDJLVdqpONvUXnvtFfr06RMGDhwYYv2hQysuTZ/W90qgKQuUn2kqHYuZpVIJr3URqP2nenU5q2MJECBAgAABAgQIECBAgAABAuUErr7t+rDHEfsns01Nmz6t3N4QYjDR+Anjw38eeyhsuvs2lQZLxQMnTJoYdvy/3Qtmb0objMFGn434PGy1147J0n1peXyN5yifzrnkd+HOf9xVdN+cuWV/fVz+uEeefjwZS/k24wfmr7z1Wth+/91yS/WVLauQHv/Ys08my/zF5QrLp3jsUy8+E2JAWPlU/oP4uH+Xg/cMw/73XvmqyXb6F8tFdyokQIAAAQIECBAgQIAAgRoLfPHFF8nSeMOGDasQLFXjxhZwwJdffhkef/zx8Nxzz4VJkyYtoLbdBJquQP5MU++8804QLNV0r2Vj67kZphrbFdEfAgQIECBAgAABAgQIECDQggXirEhxtqmY2rVtGzZed8OwbK/e4dW3Xg//++SjGsmM+vqrsNshe4eOHTqGH62xZli+X//wzntDw1vvvpO1s8aW62b5qjJx6b/4s2L/AWGDtdcPc3OBUjHo6pVcvypLs2bNzMay9qC1wmoDVwkvvPpy0SCu8m288NpLYaNdtww9l+oR1hn0o7BE1yXC86+8ED7PzRiVptW3WCfNVvoal4TY75iDQ5wJa53BP8r1YdXw3ZTvwnsfvl/lMoiVNmgHAQIECBAgQIAAAQIECBAgQKCBBWLQVDrbVAOfut5PF4PA8lNc5rPUY41LGR544IH5p5HPCQiYchsQIECAAAECBAgQIECAAAECjVJg5qxZ4ckXnqlz3+IsTs++/HzyU9fG4hKAlS0DWFXbbwx9K8SfmqYx34wNDz7xcE0Pq1A/Bk7FoLP4IxEgQIAAAQIECBAgQIAAAQIECDQegRg0FYOaYoqBTfUd3FTqgKzGI1mznliSr2ZeahMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoiUBcZrCh0h133NFQp2r05xEw1egvkQ4SIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAg0V4Ftttkm1GcwU5zFKgZmmV2q7A6yJF+ZhRwBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBBheIwUzxJ12er1QdiMFSUkUBAVMVTZQQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQaHABAU4NQy5gqmGcnYUAAQIECBAgQIAAAQIECBBoxgKPP/tk6NyxU8EIn3j+mYJtGwQIECBAgAABAgQIECBAgAABAgQINA4BAVON4zroBQECBAgQIECAAAECBAgQINCEBR7LBUzFH4kAAQIECBAgQIAAAQIECBAgQIAAgcYv0Krxd1EPCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUBoBAVOlcdQKAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJNQEDAVBO4SLpIgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBpBARMlcZRKwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAi1QYMr0qcmoO3To0OxGn44pHWNzGaCAqeZyJY2DAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECzVgg/bI+/fK+GQ/V0AgQaACB9FmSPlvqcsoxE8Ylh3fv3r0uzTTKY9MxpWNslJ2sRacETNUCzSEECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0LAC6Zf16Zf3DXt2ZyNAoLkJpM+S9NlSl/F9+tXnyeG9e/cOaSBWXdprLMfGscQxxZSOsbH0ra79EDBVV0HHEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEC9C6Rf1je3gIR6h3MCAgQqCJQ6EGji1Mnhf19+mpxn4MCBzSJoKhrFscQUxxbH2JySgKnmdDWNhQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAs1UoDkGJDTTS2VYBBq1QH0FAg0d/n748pvRoX379mHw4MGhX79+TTJwKvrEvscxxLHEMcWxNbfUurkNyHgIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoHkKxC/tO7RfPPRZapnky/xRo0aFcePGhalTpzbPARsVAQIlE4iBQHEZvnSJufoIBHrp/dfD1OVXDSv3GZCcJz1XyQbRwA3FmaWaY7BUZBQw1cA3k9MRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQO0FmltAQu0lHEmAQG0F6jMQKAYYjRgzMgzotVzouUT30HGxDrXt5kI5bsr0qWHMhHEhLoPa3JbhywcVMJWvIU+AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECjV6gqQckNHpgHSTQDAUaMhAoBhq98fHQZqjYfIYkYKr5XEsjIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQIsREJDQYi61gRIgQKDkAq1K3qIGCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0EgFBEw10gujWwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlF5AwFTpTbVIgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAjFRAw1UgvjG4RIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFB6AQFTpTfVIgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECjVRAwFQjvTC6RYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBA6QUETJXeVIsECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECDRSAQFTjfTC6BYBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAqUXEDBVelMtEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQSAUETDXSC6NbBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUXkDAVOlNtUiAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQCMVEDDVSC+MbhEgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUHoBAVOlN9UiAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKNVEDAVCO9MLpFgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDpBQRMld5UiwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQINFKB1o20X7pFgAABAgQIECBAgAABAgQWmsAaP9thoZ3biQkQIECAAAECBAgQIECAAAECBAgQIECgfgXMMFW/vlonQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKARCZhhqhFdDF0hQIAAAQIECBAgQIAAgcYhMOza/zaOjugFAQIECBAgQIAAAQIECBAgQIAAAQIECCQCpVwZwAxTbioCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQFT7gECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBFqMgICpFnOpDZQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdYICBAgQIAAAQIECBAgQIAAAQJVCZx/+llh/R+tV7TKmb8/O7wx9K2i+/ILS9FGfnvyBAgQqI1A76V7hfNPPzs59O33hoYrb7q6Ns04hkCDCPTvu1y4+KwLknO9MfTt8IerLm6Q85biJK0WaRWW7dU7jPlmTJg5a1YpmtQGAQIECBAgQIAAAQIESiogYKqknBojQIAAAQIECBAgQIAAAQKlERhy9a1h2WV6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GKT4yefDw7u5oKyaXJtStFHjjpc7oNsSSybvsb69l80tLdg9fJWb9e7DTz/KLWE6tMpg1HLNNJrNOJ64FGac0S2mnQ/8aY2uSTymrs/huFxmXNpwmR49w2PPPZUsFRvbbYqpFM/hNVZeLWyeW3500neTwgOP/bdGgaqler9G+712/mk455RfJZchBlqvs33ZDJJN8droMwECBAgQIECAAAECzUtgjZ/N//z2k4vq/vmtgKnmdW8YDQECBAgQIECAAAECBBq1gICpRn15dI4AAQIEWojAH848N+yy7U7JaONMRtvuu0sLGblhLkjg0bsfyJZrjbOnnZlbAlYiQIAAAQIECBAgQIBAYxEoZcCUJfkay1XVDwIECBAgQIAAAQIECBAgQIAAAQIECNSzwNqD1go7b7Njdpa4DJ5EIAoce8hRWbBUXC7yTzf+BQwBAgQIECBAgAABAgSarUDrZjsyAyNAgAABAgQIECBAgAABAgQIECBAgACBJEDqsH0PDD2X6hG6dOqSiUydNjUMue/ebFum5QmcefxpYb3cEqj9+vQNbdu0zQBef+fNMOabsdm2DAECBAgQIECAAAECBJqbgICp5nZFjYcAAQIECBAgQIAAAQIECBAgQIAAAQJ5Aj9afXBYafkV80pCmDlrZtjn6AMLymy0PIGN19sgLLdsv4KBjxo9Khx52s8LymwQIECAAAECBAgQIECguQkImGpuV9R4CBAgQIDA/7d3Pz121nUUwH+2QNtp6RQ6bdXQglETDYEYTVy5cOHC+DpcufUN+AJcmLjQuGDl2qVrlgaDmvonbAikFmlLi9OWMjO0VHluvKUhc1g4t4F7ns8khOl8mcucz7kbnpwMBAgQIECAAAECBAgQIECAwEMCV965Onb3dhdf2b55c/z5b38Zv3zp1+PSW/986J/y6RwFrly7Or545tyY/hd8b1+9Mv7wpz+On//qF+PevXtz5JCZAAECBAgQIECAAIEZCRhMzahsUQkQIECAAAECBAgQIECAAAECBAgQmJ/Ab3770pj+8kHgkwI//ulPPvklfyZAgAABAgQIECBAgMAsBA7NIqWQBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQ+EjAYMrbgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB2QgYTM2makEJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEDCY8h4gQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGA2AgZTs6laUAIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEDKa8BwgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQmI2AwdRsqhaUAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAGDKe8BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRmI2AwNZuqBSVAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAwGDKe4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgdkIGEzNpmpBCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAwmPIeIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgNgIGU7OpWlACBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAymvAcIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEJiNgMHUbKoWlAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABgynvAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEZiNgMDWbqgUlQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQMBgynuAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHZCBhMzaZqQQkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQMJjyHiBAgAABAgQIECBAgAABAv8TuL97d/HZ4aNPMCFAgAABAgQIECBAgAABAgQIECBAgACBz4nA8pnt8hnuQX8sg6mDCvp+AgQIECBAgAABAgQIEKgRuLu9s8hy5NTxmkyCECBAgAABAgQIECBAgAABAgQIECBAYN0Fls9sl89wD5rHYOqggr6fAAECBAgQIECAAAECBGoE9q7dWmTZOHuqJpMgBAgQIECAAAECBAgQIECAAAECBAgQWHeB5TPb5TPcg+YxmDqooO8nQIAAAQIECBAgQIAAgRqBnUs3FlmevLBVk0kQAgQIECBAgAABAgQIECBAgAABAgQIrLvA8pnt8hnuQfMYTB1U0PcTIECAAAECBAgQIECAQI3AndevLbKceGZrLH/Fc004QQgQIECAAAECBAgQIECAAAECBAgQILCGAtOz2umZ7fSxfIZ70BgGUwcV9P0ECBAgQIAAAQIECBAgUCNwf+/euHXx8iLP1gvP1eQShAABAgQIECBAgAABAgQIECBAgAABAusqsHxWOz27nZ7hruLDYGoVil6DAAECBAgQIECAAAECBGoEbr765iLL089fGBvnnqrJJQgBAgQIECBAgAABAgQIECBAgAABAgTWTWB6Rjs9q50+ls9uV5HBYGoVil6DAAECBAgQIECAAAECBGoE9t65PbZfeWOR58vf+2ZNLkEIECBAgAABAgQIECBAgAABAgQIECCwbgLLZ7TTM9vp2e2qPgymViXpdQgQIECAAAECBAgQIECgRuD6y6+Nvau3xrEzm+PCD75Vk0sQAgQIECBAgAABAgQIECBAgAABAgQIrIvA9Gx2ekY7Paudntmu8sNgapWaXosAAQIECBAgQIAAAQIEagSu/v7iuL97d2x+7UtGUzWtCkKAAAECBAgQIECAAAECBAgQIECAwDoITGOp6dns9Ix2ela76o/Dm5ubP1v1i3o9AgQIECBAgAABAgQIECCwn8DGxsZ+X37wtZ2dnQeff9affPj+B2P3rX+PE18/N46d3Rwnnz07dt99b9y9s/tZ/2j+/QQIECBAgAABAgQIECBAgAABAgQIEKgU2Dj31Hj2h98eJ85vLcZSb//u1bF35ebKs37h/Pnz/1n5q3pBAgQIECBAgAABAgQIECCwj8Dp06f3+erHX7px48bHf/icfPbEmSfHuR+9OI6cO7n4id79+6Vx/a9vjr3tO5+Tn9CPQYAAAQIECBAgQIAAAQIECBAgQIAAgfUWOHLq+Nh64bnx9PMXFkGm/w3f9JulPnjn9iMJZjD1SFi9KAECBAgQIECAAAECBAjsJ7COg6lljq3vf2Oc+u5Xln8c712+Pm5fuj7ev7a9GE99uPvBg5tPCBAgQIAAAQIECBAgQIAAAQIECBAgQCALHD76xJhGUhtnT40nL2yNE89sPfiHt195Y1x/+bUHf34UnxhMPQpVr0mAAAECBAgQIECAAAEC+wqs82BqCnTko982tfmd58bJF5/ZN58vEiBAgAABAgQIECBAgAABAgQIECBAgMD/J3Dr4uVx89WPfrv/I/qtUg//VAZTD2v4nAABAgQIECBAgAABAgQeqcC6D6aWOIeOPDaOf/XsOHbh9Dhy9uR4/NSxcejo48uzvxMgQIAAAQIECBAgQIAAAQIECBAgQIDApwjc37077m7vjL1rt8bOpRvjzuvXxv29e5/yHas9Pbbal/NqBAgQIECAAAECBAgQIECgX2D6D/fb//jX4q/+tBISIECAAAECBAgQIECAAAECBAgQIECgS+BQVxxpCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAmYDBVVqg4BAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAhkAYOpbONCgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECZgMFUWaHiECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECCQBQymso0LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlAgZTZYWKQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFjCYyjYuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAiUCRhMlRUqDgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECWcBgKtu4ECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQJmAwVVaoOAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIZAGDqWzjQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAmYDBVFmh4hAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgkAUMprKNCwECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQIGU2WFikOAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBYwmMo2LgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgFcS40MAAAHQSURBVAABAgQIlAkYTJUVKg4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAlnAYCrbuBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCZgMFVWqDgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECGQBg6ls40KAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQJmAwVRZoeIQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIJAFDKayjQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUCBlNlhYpDgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAWMJjKNi4ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECJQJGEyVFSoOAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJZwGAq27gQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAm8F9u5JlEYWWppgAAAABJRU5ErkJggg==)\\n\",\n    \"\\n\",\n    \"## References\\n\",\n    \"\\n\",\n    \"- [LlamaIndex Workflows documentation](https://docs.llamaindex.ai/en/stable/module_guides/workflow/)  \\n\",\n    \"- [Arize + LlamaIndex Docs Hub](https://arize.com/docs/phoenix/integrations/frameworks/llamaindex)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# (Optional) Step 6: Adding Custom Spans and Events\\n\",\n    \"\\n\",\n    \"Using the `llama-index-instrumentation` package, you can add custom spans and events to your workflow!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index_instrumentation import get_dispatcher\\n\",\n    \"from llama_index_instrumentation.base import BaseEvent\\n\",\n    \"\\n\",\n    \"dispatcher = get_dispatcher()\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyEvent(BaseEvent):\\n\",\n    \"    data: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"@dispatcher.span\\n\",\n    \"def my_span(data: str) -> None:\\n\",\n    \"    dispatcher.event(MyEvent(data=data))\\n\",\n    \"    print(data)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Since workflows are automatically instrumented, any custom events and spans will be automatically captured and ingested into Langfuse as a workflow runs!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"This is a custom span\\n\",\n      \"Hello! How can I assist you today?\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_llm():\\n\",\n    \"    return OpenAI(model=\\\"gpt-4.1-mini\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(\\n\",\n    \"        self, ev: StartEvent, llm: Annotated[OpenAI, Resource(get_llm)]\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        message = ChatMessage(role=\\\"user\\\", content=ev.get(\\\"input\\\"))\\n\",\n    \"        response = await llm.achat([message])\\n\",\n    \"\\n\",\n    \"        # This will create a custom span and event in Langfuse\\n\",\n    \"        my_span(\\\"This is a custom span\\\")\\n\",\n    \"\\n\",\n    \"        return StopEvent(result=response.message.content)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"wf = MyWorkflow()\\n\",\n    \"response = await wf.run(input=\\\"Hi there!\\\")\\n\",\n    \"print(response)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"![image.png](data:image/png;base64,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AAEEEEAAAQQQQAABBBBAAAEEEEAAgV0CBKr4KCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACuwQIVPFRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQR2CRCo4qOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCOwSIFDFRwEBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQ2CVAoIqPAgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCwS4BAFR8FBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGCXAIEqPgoIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAwC4BAlV8FBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBXQLld7dEkyZNrFWrVjlOW65cOevUqZO3ff369fbLL7/k6KMNkyZNsnXr1sXdV1o2VqpUyTp27Og5Nmva1DZt3mxLliyxb775xubMmVNaGLjPPAjUrl3b2rdv7x0xf/58mzVrVh6OpisCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIFB6BMo0b958x+683T9fdpl13W+/fJ/yqaeesjFjxuT7+Ew+UKGzM88803r16mVajtdWrlxpd9xxhxewirefbaVT4LTTTrO+fft6Nz937ly78cYbSycEd40AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCOQisNsrVOVyPexOIFCmTBm76667rF69egl67Nxcq1YtGzp0qN122205qlUdfPDB1r9/f6+jQmmvv/560rHyurN69ereeXXcwoUL7e67787rEPQvgQJ/+MMfrEuXLt6dPfPMMzZ58uQSeJfcEgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgiUFIHdHqgaPmKETYoTqKhfv74dd9xxvuuzzz7rLwcXvv/+++BqqVm+5JJLQmGqn3/+2b766iv74YcfrHHjxta5c2c74ogjrEKFCt7PTTfdZDfccIMXbHJI6qfAlVrLli3d5rS9VqlSxR+/YsWKaRuXgTJbQFN8us+dnnMaAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBQnAV2e6Bq3rx5pp9oU9jHBaq2bNlin332WbRLqV7fd999/fv/6KOP7KWXXvLXV6xYYT/++KNpuypTVa5c2ZsS8KyzzrL777/f78cCAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIJBfY7YGq5JeT2l5NLVenTh2v85IlS2zDhg3WunVr69atm61Zs8ZGjhwZGkhVm7R/zz339MJGqu70008/2bZt20L9Eq2o2lKnTp2sbdu2NmvWLPv2229Noa9UWu3atW2//fbzrlfnzMt53fiq6lO+fPZblWiqvqVLl9ro0aPthBNO8A7V9ao1b97cNGVgo0aNvHX9o+tq0aKFt75gwYK496PpBffaay+vn5wnTZpkOke06fpUnSpYfahSpUr++Ap86X1Rc3137NiRY0pCN66urUaNGt7q3Llzbfv27W6X/1q2bFlr06aN95OVleUFyvJj6w/IQkoCqT5L6qeQpFrNmjX9sZs0aeJ9LpK9//rsKECoZ3bmzJneFIF6xuO14O+CRYsW2aZNm7xnRRXb9NnV53Xq1KkJP2vRMXV9mp5Q1//NN9/Y/Pnzo138z7V2zJ49O8d+bdA9NGzY0Nuna9fzQ0MAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBzBDITulkxvV6V3nuueda165dveVPP/3UDj74YC8o5W7BBaoUihg0aJAdeuihpgCOa64S1owZM+yOO+5IGKxSIOSaa64JBULcGAoIPfTQQ6Yx4jWFla688kpT2Me1vn37eou//PKL3XPPPQnP6/q7V1Wcck1BlK1bt7rVHK9jx4613/3ud972zZs3e0GnW2+9NUc/3dstt9zibX/llVds1KhRfp/27dvb+eef74fW3I6BAwd6537iiSdswoQJbrNp/OA1ake5cuX88RV00v2qBfteccUVprBVtF133XX+9IYPPvigF6gJ9unTp4+dfPLJXujFbdc2NQVXbr/9dlu9erXbxWsaBPL6LPXu3dvOOOOMHGfWtJT6UfvTn/7khSFdJ4Xyrr76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fzm6oGeuSZMm3maFttQ++OADu/TSS73l4LR/qlil3zFqCiTq9wQNAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBDJLIOMDVYsWLUoortCNpuvSVF3R0FSigzSVngtFbd68OUeVJnecqlapmk6waSoy1xR0uvfee91qwleFo/LaFixY4AWrdJyqUx177LG25557+vdYq1Ytr2LV5ZdfnqO6Virn6tGjh5144olxgy2pHF9YfTR9mip89ezZ0zuFgjXnnnuuN4Wiqn4pUKOgi5surrCuozSOm59nKRUnVR0LVhLTlH25NRdGjPZzlaSi2+NVM1OfYDgwWHktevzkyZNNP8E2YcIEL8SnAJWuX8+hQln6bLrfH3pOo9XtgmOwjAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALFUyDjp/xLxKqKSnfeeaftv//+ftBIfTVdl6seFe9YhZFcSxTQcPujr66KTXR7svXgNHbJ+iXapxCHQigXXXSRV93H9VOg65JLLnGrKb+efvrpXkgpWCVIgRRVx8qrR8onzUPHZ555xp5//nlbt26df5QCLHXq1LG+ffuaKlQNHjzY38dCwQXy+yylcub8PDPBAFYq50jUp2rVqv6ulStX+supLkz5/nu/69FHH+0td+/e3d/22Wef+cssIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEDmCGR8hapE1Nddd50/HZ6m6XrzzTftk08+8abx0zGqWDVs2LAch2vaO9fyOqXd6tWr3aFecOvtt9/21xMt/Pjjj4l2+dtVkcddy+LFi72x/Z27FrZu3WpPP/20qarW4Ycf7m3dY489ot2Sru+zzz7Wp08fv4/CWm+88YYFq/dcddVV1rFjR79PYSwoHJWs6X3Uj6Zw1L1q6sNgAKx3797etHD3339/smHYl6JAfp+lVIZftWpVqNuIESNC6/FWklWli9c/0bZgJbPcPnPxxnjn3Xet6377ebvctH9uGkGFEP/73//GO4xtCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIFDMBUpkoEoVixo2bOjR79ixw/72t79ZqhVoFIpSGKJs2bJWsWJFL3ilqlaptF9++cXvtn79ens3FrhIR7vpppusZs2a3lAv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7bffHrp8d43aGO/zEN0e7O8GUlW5l19+2auW5ba5V32Wdd7g597tc68ffvihW/ReFSCjIYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEDmCpSJVfwp8XNTqSJThw4dvOngNL2dq4qTl7dN1ZBat27tBZbyOpWcql+p6tPy5ctNFXbihTryci2ur8Z11aAUQtLUd6k0BVn23HNPb5oyBc5++umnHAEqjaMpCnXP8+bNszlz5qQytN9HUwqq8o+mTpO5Kh1FW5UqVbwp/FS1SAE2TZ+W19YqNtWhzrV48WIvpBUNguV1PPonFyjIs6Rqb3oONbWfKpnpWUjUatWq5U0Bqfdz6tSp+XpmE42dbLu7Pz0jmnIwlc+Tqlv9+c9/9obVFJkPPPBAslOwDwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSKuUCpCFQV8/eAy0MAgQwWGDp0qDdlqG5BlecUIKQhgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQOYKFJsp/zKXkCtHAIHSKtC7d28/TKXKd4SpSusngftGAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEChJAuVL0s1wLwgggEBhC8SmSbULL7zQsrKyvB93vnfeecct8ooAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACGSxAoCqD3zwuHQEEdr9A27ZtrVmzZqETz5w500aOHBnaxgoCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIZKYAgarMfN+4agQQKCKBVatW2bZt26xs2bK2fv16++abb+y5554roqvhtAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCQboEysemrdqR7UMZDAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBDJRoGwmXjTXjAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUhgCBqsJQZUwEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDISAECVRn5tnHRCCCAAAIIIIAAAggggAACCCCAAAIIIIAA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g/LNN9+82CPzacaMGU5hiRIlnDQTJEACJEACJEACJEACJJDfCEC8Ywt4kPYKpJD3tgMnlPt5kUFbrycntA9WjrpENBWURbp27B+iND38XEJ7eaJPfrYzP8fmrciDF18aLBCI8BZL4Yj32hQtVlL+55l0MVXgnST5x7ML5MrL+gWO9DCAixd4e8RfPlbM42FnM2fOdC1Df7d169Yt4DUs4x22lJQUV7tYZvD8on///ubFsBO2WzNrEjyXgDcteMm67777rJqsJ71zwUO3155//nnp27dvps/EhYDyEA+bIYyCKC0Rzc8rlXcf8DwFUZUahFSRiqlqt6ul3QJirfSX/JwCJkiABEiABEiABEiABEiABEiABEiABEiABHKcQMbTvRyfKvwE69evD98o0ALqMrjOr1+/vtSoUcO8hakPrIMpz8qVKyf2w71Zs2aFnAviLa917NjRVdS0aVOxvV7Zld43M+G2TuOB2u2YJgESIAESIAESIAESIIG8TgACHhUGIY0D3qhssY+WazswQZmeveVa5zcG+qBcxzSDJMiPrIibsE8VcWCb9hhIezkouwRBkuPLPHkiTYqXcnv2ycqkm35aK737NTVd21w2SJYt/izTMENueVoKFCwkiwMhAbds+j5TfSQF3XqPEoi2YK8+/51M/vCP6ZmLP5tkOEVylcdDJimpmlkGmOcVe/vttwUvgKl16dLFJO0yFLz33numPCd/YA4czZo1k8GDb5AuXbpKtWrVMomZRo8eLfCo/dvf/jZby7FfWsNA9gtuyN91113mpTik1VIWLZI3A16yli1bpkXmPGTIEHnsscdcZYmQgdAJofs0fJ/fmjWcny2kQjt4pgollNIxQ7Xxm49lJEACJEACJEACJEACJEACJEACJEACJEACsSEQV4KqcFvCwzp4p2rfvn3gAfLFJ8jhOl2sb9mypavlpk2bXPlIMvYfJtD+hhtuiKSbaYOHlRRURYyLDUmABEiABEiABEiABPIYAQiovBZpGfr5tc1KuXcN+SGvoqr8sNes7PH4kd1SvnIDqV23jaxa7vY2FM14333zvtx5/19Nl9EP/k4evN0tqOp7zYPy4CPp3o+fTtudZUFVrcA61U6eOKJJcy5evLSULecqissMmOcV279/v/muj5eoYPAEBe95l112mbNFeHJavny5k8/pxJo1awQHDC+fwXvV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HMKXL4eF+C41NVUWLlyYY/sPJvDLsQk5cEwJUFAVU5wcjARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARI4FITUK9Ul3odnD++CVD0FN/X51KuruClnJxzkwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEA8EaCHqni6GlwLCZAACZAACZAACZAACZAACZBA3BOo076WWSPOW5duM2k9x/3iuUASIAESIAESIAESIAESIAESIAESIAESIAESIAESIIGwBCioCouIDUiABEiABEiABEiABEiABEiABPI7AYineo7uJiqmUh49R2tKZO7b803m27fSzxk1TJEACZAACZAACZAACZAACZAACZAACZAACZAACZAACSQSAQqqEulqca0kQAIkQAIkQAIkQAIkQAIkQAK5SiCYkMpvERBcwXCGuCrWwqpipYpKcquaUqNFNSlbtawc2nFIdq/bKxsXbvZbTrbKChUpJL3u7iZFA3NuX7lTVs9cE3K8MpVLp6+tZXUpWqKIHNhyQFKXbZfd6/eG7MfK2BKoV6+eFC9ePKpBz5w5Ixs2bAjbp2bNmlK2bFk5deqUbN7sf8+VL19eGjZsKNWrV5eDBw/Kxo0bZdeuXWHH9jZISkoyY6B87dq1cuHCBW8Tadq0qWmDNR09etSsKTU1Vc6dO5eprbegUKFCUqtWLUlOTpaKFSvKiRMnZP/+/bJq1SoBj3BWpkwZadmypWCdhQsXlh07dpi9Hj58OGRXtG3UqFHINlq5e/duSUtL02yWzthbnTp1ZOnSpWH747oVKVIkbLsjR46Y/YZtGKIB7tFWrVrJihUr5PTp0yFaZlRVqVJFGjduLBUqVDD31KZNm+TAgQMZDbKYwp6x9/r165u1bNmyRXCEuw/KlSsnNWrUiGhWfA4i2WfBggWlSZMmQcc8f/684B4/efJk0DaoKF26tLQK3J8VK1WSnTt3mv3g8xiN4fNVu3ZtKVCggPkMbt26NZrubEsCJEACJEACJEACJEACJEACJEACeYIABVV54jJyEyRAAiRAAiRAAiRAAiRAAiRAArEmAEGRiqQwtob1U09UOp96rbLbajpWoqoW/ZvJdf97jRQsVFCndc4nDp2Qz574QjYt2uKUZTcx4LH+0npA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IibsYbvBbWT2S99o82yd4XFJbevWrfLpp59q1jlD0JMbBkFOo0aNnKm++eYbJx0sgVBlKipavXp1sGamHN6v1BDuz88QMs4WCcE71JEj6R7B0F9D20GAEsojlD32ddddJ1WrVnWK4BlLw5c5hZ4ExGEIdQaDQMYv9F65cuWcXsFC2mG9N954o9MO40ybNs3JB0v079/fiH7A4pNPPnGJyrQPBC441GzhmZbF+tyuXTtp0qSJGRZh//y4oFLFVEjb4kDkY20QIKmHKAgAvZ6adD5b7KfiJ60Ldu7apYvUb9DAVM+fP98IHv3aQpynnqXgoUoN12fYsGHmOkGUN336dK1ynbUvCvEZiAfDdQNPeKXCZxzX3v79AMEVjQRIgARIgARIgARIgARIgARIgARIIIMABVUZLJgiARIgARIgARIgARIgARIgARLIxwTmvj0/IKYabghACPXtW/NjSiMr4+1au8cReLXs30wWfJCSaU1tBrVyyvZvOeikQyW+fPFrKVI8tKjq8l/2dIb45rX0P7RraDwIu878HPDgEhAkwdtThYCoyuvhCSKwupdleGHavznDQ5MzcA4lml7eSG54cqAZPXXZdvno15NcM23/YadM/9uXMuh/0r0MNehSNyCocjXJcsYW+sDTjZ/F0puO3/haBu8zKtA5duyY7NixQ6uCnm3vWosWLQraDhV2GL369evL0qVLM7WHgErXgErbi1TXrl2d9ksC4e4isSuuuEIaXBTEoP2UKVNCetHSMeF9p0qVKiaL/hDTeK1z585OEXh5DcKakSNHOoIziMAmTXLfW94+msd9UalSJZNt27at+O0XYQTVckuEA3EfQvjBunXrJhMnTtQlOGcI32zx3K5du5y6nEjAm5heK5wh5oIgzmu2l7dwnte0b4WAIE7Hbt++fVBBFURHarbIDIK4atWqmXsA3CBC87tXbKFSMG9oOn5unuFJ7vLLLzdT9u7d25kae4AHOxoJkAAJkAAJkAAJkAAJkAAJkAAJkEAGgYIZSaZIgARIgARIgARIgARIgARIgARIIP8SgIcqHDCE6YO3quwYRFka7g9irazY0k9XON163t1Vino8SyW3rilVG6WLRPCH/qVTljvtNVGxTgUnxJ2WpUxYKt+9vyjkAcGUmrZdNWONFsn6uRnhqK5/YoBTroked3UViKpgadvTMgmutF1O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yl5//XWBMMrPsC6EfHzrrbeCCo1wP//73/8WXDM/gwASTIPda3597PvF5uTXNlTZOet3qt84+KziUJs3b54mnXOs1uIMyAQJkAAJkAAJkAAJkAAJkAAJkAAJJCCBAgFX1NE/kUrAjXLJJEACJEACJEACJEACJEACJEAC8UcAoa5C2YEDB0JV52id19MUJoMHq9Rl28y8SENEVbtdengopG374IEJpr1dlt10sVJFpXL9SlKsdDE5sPVgIKzd4bBDlqtW1oitzv58Nmzb7DQoXamUVK5XyQgQ9vy0T04ezrogKDvrCNa3aCDUX/Vm1Yy3qr0b9wkEV7lhEEwhLBhEHDiisQoVKkjVqlUFghYIM7Ijsgo378033yzVq1c3zbITuq1cuXJmHISR27Nnj++0nTp1ku7du5s6hE+DeCW3DR6i4B0M6z1y5IgJvecnPsmpdSHcHK4t7g+IW+DJKjfnD7Uv/F6GhzQIpiACw/0XyuCNDfd2Tq4fYj/cn/CIBSHV3r17w86HawsRXTBBlu6pfPnyxqMcREQ7duyI+nOGEI5YG0SMWFckAkGdm2cSIAESIAESIAESIAESIAESIAESIIH4JUBBVfxeG66MBEiABEiABEiABEiABEiABPI8gXgWVAG+eqayvU6FuygaNhBnGgkkCoFbbrnFEat4PRHFeg/9+vUzYQwhYIFHqHCeimI9P8cjARIgARIgARIgARIgARIgARIgARIgARIggXAEKKgKR4j1JEACJEACJEACJEACJEACJEACOUYg3gVVuvFwwir1XIUzhVRKjWcSIAESIAESIAESIAESIAESIAESIAESIAESIAESSEwCcS+oqpTcWFr1GSm1mneVclVqS4HAfzlpF+SCHN6bKtt+XCA/fPWR7N++Pien49gkQAIkQAIkQAIkQAIkQAIkEBUBhFTSI6qO2WxcoEDg29jFI5tDuboniqDKtehAxg7vRwGVlw7zJEACJEACJEACJEACJEACJEACJEACJEACJEACJJDYBOJaUNV92MNy2dV3X1LC309/S76b+NwlXQMnJwESIAESIAESIAESIAESIAEQQHgsiKkupUFUVbBgwZgtIVEFVTEDwIFIgARIgARIgARIgARIgARIgARIgARIgARIgARIgATijkDsnoLHeGvX3P/CJRdTYUsQdGEtNBIgARIgARIgARIgARIgARK4lATiQUyF/UPQhbXQSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCCvEohLQRU8UzXqcHXcMMdasCYaCZAACZAACZAACZAACZAACVwKAvEiptK9U1SlJHgmARIgARIgARIgARIgARIgARIgARIgARIgARIgARLIiwTiTlBVKblxXHim8l5seKrC2mgkQAIkQAIkQAIkQAIkQAIkkJsEIF661GH+/PYbr+vyWyvLSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCAaAnEnqGrVZ2Q068/VtvG8tlwFwclIgARIgARIgARIgARIgARyjUA8iql08/G8Nl0jzyRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQLYG4E1TVat412j3kWvt4XluuQeBEJEACJEACJEACJEACJEACuUognkVL8by2XL1InIwESIAESIAESIAESIAESIAESIAESIAESIAESIAESCBPESgcb7spV6V2vC3JWU88r81ZZC4mWrZsKd26dXNmnDBhghw+fNjJR5ooWLCgtG/fXrp27SqtW7eWkiVLyrFjx+TgwYOyY8cO2bRpk6xatUpSU1MjHZLtSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCBLBOJOUFVACmRpI+i0ccVc03fWuKfMWfMN2vSUBm16mbJ+o/5ozln5kZ216Xx169aV5s2ba1YWLFggaWlpTl4TSUlJMmDAAGnTpo0sWrRIpk6dKmfPntVq59ygQQNp0qSJk//uu++yJGpyBogiMXLkSOnfv7/T44cffjD7cQoiSJQvX14mTZok2G84mzljhvzx8cfDNWM9CZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACWSZQNwJqrKyEwinIKJSAZV3DJRr3cyxT0n/2x6X7AirvONHk7/jjjtk4MCBTpdx48bJiy++6OQ1cffdd8vNN99ssn379jUem2bOnKnVzvnPf/6zNG3a1Mk/8sgj8tVXXzn5eE5UqFBBPv30U+ORKpJ1/rhmTSTN2IYESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEskwg4QVVEEq99vDVDgD1RlW/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QjytKsRaNq74NkRt1qrqt859scTixYtdi73iiitM3n4LfO3atYKQf7YHJIiiYF4xjB1GEH8QKVAgEIftoiEk3fTp0zUrhw4dkttuu83JIzFgwABX3s5s3LjRvOX+1VdfCUQ5yPtZy5Yt5S9/+YurCuH1/vrXv7rKYpHJ7h5Xr1rlLKNy5cqOV7ChQ4c45ZqA4K158+YmC+422xkzZmiziM+R8ox4wCANH3n0UacGXrDeeustJ49EnTp1XPmcyMAT1HVPpN9bp0+elpSPvw87TeFihZ02506fc9J24tzZjPKChdN/jRUpnt4PIQGDmYarLFoyI/SgeqZK254mSyYvMyEGT588I9+8Pk92rd1thkpuXSPYkCwnARIgARIgARIggUtOAOKndu3aCc5q+F4RSdg+/LsaAizbINC655577CKmSYAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCBfEUhAQdVcc4H6jfpjzC/UxhXpY8d8YJ8BZ82a5Srt0rmzEfVUq5YRWgwCJpgdAg+iJVizZs3MGT/OnTsnq1evdvINGjRw0ki89NJLrjwyO3bsMIdWwEsVvC/5mfeNd782KMMfYmyxEQRhDz30ULDm2SrP7h5nzMwITYc1q5CtR48McZ0dJm/o0KFmvSp8QwbinJnWOJFuKFKekY7n184vjODy5ctdTdUrmqswhhkIne548xYpUixdvPTJH6b6eo6K4ZRZGurIxbCC8HDVvF9T1xjv3/uxPNfvZXlhwKuucmZIgARIgARIgARIIB4JQATl/bcmPE9BNOVnqPMTU9EzlR8tlpEACZAACZAACZAACZAACZAACZAACZAACZAACZAACeQnAgknqNKLE0vxU4M2GSIaHT+nz+vWrTNCKJ2naUAg1bNnT5cgCZ6lYF988YU2k6SkJClZsqRUr17dKUMIO9tsUdbZs2fl4MGDdrWT3rpli5NGQr0wuQoDmWPH/MOuedt582+88Ya3KGb57O7R61kKnr9KlSolVapUMWuEWOqVV15x1tutWzeTbtu2rVO2b98+40HMKYgwkVWeEQ5vmh05ciRTc3g7y0279R/DpFy1smbKue8skE2LtkQ2fYC9YxmO1pwiJAoWtH51XWxud3M19stYU8x54zvTAsK6wf83UB6d81sZ+dJQadG/mRHN/XzsZzl9InfZ+S2ZZSRAAiRA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Tz9ZpwMPTBhGnpOSmbwi7bTTTglV8nIVN3lYeu211xJC7fXp08fOP/98580qDC2ovkuXLjWJoDJpF110kQ0cONAkppIp7OBf//pXu+OOOzIyTab2qHCJ8dCHr76aGKbu888/tx49eiSsW8Ipb/Jc1a9fP5NXKdl1111ngwYNShC1+bYFHYcPH2777LNP1KRz586muStVqhSNHVVmWeLYa7u7dWpZ1WtUtz6vnpfvChflhOZ78P8ec/VHXN7Vqlavaof9+RAb+fLXZr9Hi5w5bpaNfe8H2+OotlZ769p2yZvnO29XEkd5+2HIj7bktyU+646v/W1Azty9Te26XtzZDu1zcLQuNdiwfoN9cN8w19b/MfCGQXb8v491XqiOv+FY+2O/Y2zjho1uDN9m6AMf+yRHCEAAAhCAAAQgUGYJ4JGqzJ46Fg4BCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIlTCBXnVDCExdnOnmouvDOQW6Iwc/elOOp6uYiD6c+6isrDe9UfsGfJhFESYiUzD799NM8xYMHD85TpoI777zTRo4cmVBXtWpVi4upFOrvwgsvTGiXiYxC5t11110JQ5188sm25557JpQVJ5OJPb711lsJS1i9erXJG1hoce9dK1eutBkzZkRNxFU/b0rXqbMpJJ0vS+X44osvmjxihaawf16oFZaX6XROSEtvc36Z65ILZyyKxFS+bsCN79nIl76OwgZ6MZVEUUMf+tjeui5vGMzFOWKtp//0os2fusnzm8Ro3uZPW2APnvC4LZi+yZOaL/9x6M/27IUv26KZi1wITvXxc61cstIG/nuQff1arscs348jBCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAALlk0CuCqSM7U+iKgmhJIrSb+Loj3M8VV0bhQPMbzvySKUwf94zlcYoLe9UWuOQIUPsqquuiparUH+jRo2K8mFiwIABds4550RFq1atyiPA8ZUbNmywiy++2I4//njr27ev1a5d21e549q1a+3bb7+1v/zlL85rVVi5fv36MJs01KAaFNauf//+dswxx1jbtm3deBKqyEPVkUce6fJao7eNgcjGl8XHj+eLs0c/x/Tp000CqRo1ariisWPH+qroqLCKYZtx48ZFdUqsWbPGFCawU6dOrlwexuRBy1t83fFwjr6djqed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eXIez1+777677bjjjs6b2Oeff+6bRsdWOZ6iqlbd9BibP3++zZs3L6orKCG2u+66q+25556ma0PnQuMvW7YsoZu8Um2zzTYJ50ENGueUiZFM15I8nsVNcygU5P777+88n2n8SZMmxZu5vAR53rvUunXrbOLEia68bdu27hpdunSpDR061GbOnJm0f1goMd+XI0faxJy5zj333LBqs6Sr16hmXS7Y5Elq9fLV9tCJT9iKRSvcXKq78JVeVnvr2rZN68a2VaNatmxe8us9XFyXizaFMF27eq09evrTkVcp9b/kzfOtStUq1uXCg+y7/42JurXptqtJNCX75o1RNuiOD6K6I688zDqc0N5q1a9pbY/YzYUNjCpJQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABGIEyr2gSvuV6ERh/ySE+eWXX8pc6D95p5JtCvN3jUun88emEIAfR2EAW7XLFQWlM14m+hx++OH5CqrOOuusBKFSsvkkkpP3MW8//vij9ezZ02ej42uvveZC9fmC4447zj744AO75ppcnnHx1lFHHeWbJxwl8gnt4IM3iT982YgRI3wyOsqrVb9+/ZyIKSr8PaEwg5dffrmNHj06oUqiqdtvvz0q03WsdXtPaxs3bnTXdtQgn8TJJ59sV111VULto48+auIr0Vrc7r777qjo9NNPzyMyiipjiaLu8cwzzzSdP2+33HKLvf766z7rztfTTz8d5a+44gobPnx4lJfY6ZVXXony4te7d+8on1/i1FNPtYsvvjjpuVi5cqX9+9//tvfff991v/mmm2y/HEFU3HbaaSd74YUXXHGvXr1szJhcYY8EXmIogV4otLvsssts/fr19uCDD9qzzz6bMKSuQ90L3pS/4YYbIrGYytVf4rs//elPeUR7CxcudGxefvll++qrr9wwcU9qfuxMH1se0CIa8s1/vB2JqVS4ZuVae+eWwXbq3Se4Nrsc0tq+eX1U1D5ZYtvdmthWDWq5qm9eGxWJqVQgMdYXz39l8ogl8dTWLRvZ3EmbRHQdz9zX9ZEXq8H3DHVp/8fgu4c6j1YSYrU/bo98BVUas/52dW3OxHm2dtVa350jBCAAAQhAIOsJSODvvY4+8sgjxV6vxvr666+LPQ4DQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAATKKoHK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MHny5PFFN/SBAAIIIIAAAggggAACCCCAAAIIIIAAAggkWyAoKEjKly9vrhigJx8/flw0M1V0dHSy+0rJCQRUpUSNcxBAAAEEEEAAAQQQQAABBHwikBkDqqyJ16xZU8LDw61N2blzp+zfv1+OHTtmBledP3/e3pcZKwRUZca7xpgRQAABBBBAAAEEEEAAAQQQQAABBBDInAIBAQGiQVQhISESGhpqZqSyZrJlyxZZt26dtZkuzwRUpQszF0EAAQQQQAABBBBAAAEEEPAmkJkDqnQ+BQsWlIoVK0q5cuW8TY82BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgRQK6C+xbtu2TaKiolLYQ8pPI6Aq5XaciQACCCCAAAIIIIAAAgggkEqBzB5QZU0/d+7cUqJECdH56FKA+fLlE22jIIAAAggggAACCCCAAAIIIIAAAggggAACCFxZICYmRk6dOmUu7RcZGSkHDhwQbcuoEpBRF+a6CCCAAAIIIIAAAggggAACCGQVAf2H/e7du81HVpkT80AAAQQQQAABBBBAAAEEEEAAAQQQQAABBLKrQM7sOnHmjQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgh4ChBQ5SnCNgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCGRbAQKqsu2tZ+IIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCDgKUBAlacI2wgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJBtBQioyra3nokjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIICApwABVZ4ibCOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEC2FSCgKtveeiaOAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACngIEVHmKsI0AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALZVoCAqmx765k4AggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIeAoQUOUpwjYCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghkWwECqrLtrWfiCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg4ClAQJWnCNsIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCQbQUIqMq2t56JI4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAgKcAAVWeImwjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAthUgoCrb3nomjgACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAp4CBFR5irCNAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAAC2VaAgKpse+uZOAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCHgKEFDlKcI2AggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIZFsBAqqy7a1n4ggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIOApQECVpwjbCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggkG0FCKjKtreeiSOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggICnAAFVniJsI4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQLYVIKAq2956Jo4AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKeAgRUeYqwjQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAtlWgICqbHvrmTgCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgh4ChBQ5SnCNgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCGRbAQKqsu2tZ+IIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCDgKUBAlacI2wgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIJBtBQioyra3nokjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIICApwABVZ4ibCOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEC2FSCgKtveeiaOAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACngIEVHmKsI0AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAALZVoCAqv9v1w4JAAAAEIb1b41/BSaRDMvt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAPQb9BAAAwr0lEQVQAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFXCoqohMgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgMCtgEPV7fSKEyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAYeqisgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNwKOFTdTq84AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVcKiqiEyAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwK2AQ9Xt9IoTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABh6qKyAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQI3Ao4VN1OrzgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAhVwqKqITIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDArYBD1e30ihMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUAGHqorIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjcCjhU3U6vOAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECFRixMzwNpPgDzQAAAABJRU5ErkJggg==)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.3\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/observability/workflows_observablitiy_langfuse.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Tracing Workflows with Langfuse\\n\",\n    \"\\n\",\n    \"This notebook demonstrates how to gain real-time observability for your [LlamaIndex Workflows](https://docs.llamaindex.ai/en/stable/module_guides/workflow/) by ingesting traces to [Langfuse](https://github.com/langfuse/langfuse).\\n\",\n    \"\\n\",\n    \"## Step 1: Install Dependencies\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install langfuse llama-index-workflows openinference-instrumentation-llama_index llama-index-instrumentation\\n\",\n    \"\\n\",\n    \"# Optional if using openai or other llama-index packages\\n\",\n    \"%pip install llama-index-llms-openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Step 2: Set Up Environment Variables\\n\",\n    \"\\n\",\n    \"Configure your Langfuse API keys. You can get them by signing up for [Langfuse Cloud](https://cloud.langfuse.com) or [self-hosting Langfuse](https://langfuse.com/self-hosting).\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import os\\n\",\n    \"\\n\",\n    \"# Your openai key\\n\",\n    \"os.environ[\\\"OPENAI_API_KEY\\\"] = \\\"sk-proj-...\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"With the environment variables set, we can now initialize the Langfuse client.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Langfuse client is authenticated and ready!\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from langfuse import Langfuse\\n\",\n    \"\\n\",\n    \"langfuse = Langfuse(\\n\",\n    \"    secret_key=\\\"sk-lf-...\\\", public_key=\\\"pk-lf-...\\\", host=\\\"https://us.cloud.langfuse.com\\\"\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"# Verify connection\\n\",\n    \"if langfuse.auth_check():\\n\",\n    \"    print(\\\"Langfuse client is authenticated and ready!\\\")\\n\",\n    \"else:\\n\",\n    \"    print(\\\"Authentication failed. Please check your credentials and host.\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Step 3: Initialize LlamaIndex Instrumentation\\n\",\n    \"\\n\",\n    \"Now, we initialize the [OpenInference LlamaIndex instrumentation](https://docs.arize.com/phoenix/tracing/integrations-tracing/llamaindex). This third-party instrumentation automatically captures LlamaIndex operations and exports OpenTelemetry (OTel) spans to Langfuse.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from openinference.instrumentation.llama_index import LlamaIndexInstrumentor\\n\",\n    \"\\n\",\n    \"# Initialize LlamaIndex instrumentation\\n\",\n    \"LlamaIndexInstrumentor().instrument()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Step 4: Create a Simple LlamaIndex Workflows Application\\n\",\n    \"\\n\",\n    \"In LlamaIndex Workflows, you build event-driven AI agents by defining steps with the `@step` decorator. Each step processes an event and, if appropriate, emits new events.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 12,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from llama_index.core.llms import ChatMessage\\n\",\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_llm():\\n\",\n    \"    return OpenAI(model=\\\"gpt-4.1-mini\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(\\n\",\n    \"        self, ev: StartEvent, llm: Annotated[OpenAI, Resource(get_llm)]\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        message = ChatMessage(role=\\\"user\\\", content=ev.get(\\\"input\\\"))\\n\",\n    \"        response = await llm.achat([message])\\n\",\n    \"        return StopEvent(result=response.message.content)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"wf = MyWorkflow()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Hello! How can I assist you today?\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"response = await wf.run(input=\\\"Hi there!\\\")\\n\",\n    \"print(response)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"bat\"\n    }\n   },\n   \"source\": [\n    \"## Step 5: View Traces in Langfuse\\n\",\n    \"\\n\",\n    \"After running your workflow, open [Langfuse](https://cloud.langfuse.com) to explore the generated traces. You will see logs for each workflow step along with metrics such as token counts, latencies, and execution paths. \\n\",\n    \"\\n\",\n    \"![Langfuse Trace Example](https://langfuse.com/images/cookbook/integration-llamaindex-workflows/llamaindex-workflows-example-trace.png)\\n\",\n    \"\\n\",\n    \"_[Public example trace in Langfuse](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/e0987dce85c8c49602030599b84e77e8?timestamp=2025-07-01T09%3A42%3A54.701Z&display=details)_\\n\",\n    \"\\n\",\n    \"## References\\n\",\n    \"\\n\",\n    \"- [LlamaIndex Workflows documentation](https://docs.llamaindex.ai/en/stable/module_guides/workflow/)  \\n\",\n    \"- [Adding additional trace attributes](https://langfuse.com/docs/integrations/llama-index/workflows#interoperability-with-the-python-sdk)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# (Optional) Step 6: Adding Custom Spans and Events\\n\",\n    \"\\n\",\n    \"Using the `llama-index-instrumentation` package, you can add custom spans and events to your workflow!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 15,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_index_instrumentation import get_dispatcher\\n\",\n    \"from llama_index_instrumentation.base import BaseEvent\\n\",\n    \"\\n\",\n    \"dispatcher = get_dispatcher()\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyEvent(BaseEvent):\\n\",\n    \"    data: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"@dispatcher.span\\n\",\n    \"def my_span(data: str) -> None:\\n\",\n    \"    dispatcher.event(MyEvent(data=data))\\n\",\n    \"    print(data)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Since workflows are automatically instrumented, any custom events and spans will be automatically captured and ingested into Langfuse as a workflow runs!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 16,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"This is a custom span\\n\",\n      \"Hello! How can I assist you today?\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from llama_index.llms.openai import OpenAI\\n\",\n    \"from workflows import Workflow, step\\n\",\n    \"from workflows.events import StartEvent, StopEvent\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_llm():\\n\",\n    \"    return OpenAI(model=\\\"gpt-4.1-mini\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def step1(\\n\",\n    \"        self, ev: StartEvent, llm: Annotated[OpenAI, Resource(get_llm)]\\n\",\n    \"    ) -> StopEvent:\\n\",\n    \"        message = ChatMessage(role=\\\"user\\\", content=ev.get(\\\"input\\\"))\\n\",\n    \"        response = await llm.achat([message])\\n\",\n    \"\\n\",\n    \"        # This will create a custom span and event in Langfuse\\n\",\n    \"        my_span(\\\"This is a custom span\\\")\\n\",\n    \"\\n\",\n    \"        return StopEvent(result=response.message.content)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"wf = MyWorkflow()\\n\",\n    \"response = await wf.run(input=\\\"Hi there!\\\")\\n\",\n    \"print(response)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"![image.png](data:image/png;base64,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Oi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8ZXhpZjpVc2VyQ29tbWVudD5TY3JlZW5zaG90PC9leGlmOlVzZXJDb21tZW50PgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MjAwNjwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4xNTIwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0LzE8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NC8xPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4K7Tz0EwAAQABJREFUeAHsnQeAE8UXxp+g9N57772IIF1BBVQQFARRaYqCKIgNC39EBUWxY0ORovQidkAQpPeq0pv03rvl/74Js7fJJXfJXa5/T8Puzs7Ozv6S7G3mm/feNanT5flPaCRAAiRAAiRAAiRAAimSQLU6t5jrXrv0lxR5/bxoEiABEiABEiABEiABEiABEiABEiABEiABEiABEgiGQKpgKrEOCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACaRUAhTWU+o7z+smARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIIigCF9aAwsRIJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEBKJUBhPaW+87xuEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBoAhQWA8KEyuRAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmkVAIU1lPqO8/rJgESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCIoAhfWgMLESCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZBASiVAYT2lvvO8bhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggaAIUFgPChMrkQAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJpFQCFNZT6jvP6yYBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEgiKAIX1oDCxEgmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQEolQGE9pb7zvG4SIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIGgCFBYDwoTK5EACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACaRUAhTWU+o7z+smARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIIigCF9aAwsRIJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEBKJUBhPaW+87xuEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBoAhQWA8KEyuRAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmkVAIU1lPqO8/rJgESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCIoAhfWgMLESCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZBASiVAYT2lvvO8bhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggaAIUFgPChMrkQAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJpFQC16bUC+d1kwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJJG4C112XRtJnzCjp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qncPqXsQ4PFas2qlvlaFdCwrkwAJkAAJkAAJkAAJkAAJkAAJkAAJkEByJxCnwnratGmlbr26Ur5C+Sg5XnONSPHixczr0MFDsnTJUtm3j15AUULjThJIRgRq1KwhHR+4X+o1qK+TbzLK/n37ZcbPM2T4p59pVIuzyehKeSnhInCjhmtHaHS3bd+6RT4b9p7mTz/vLo7VOgT6994cLI/06iMlS5eJVVvhPBjX/peGr1+invU0EkhuBOCp7k9UP3fmpBzXUOxnTx2XSxfD9z2Pil/adBkkU9YckkO91DNmzuZVFX38+8pleq57UeFGYiJQrYbHS33k58OD9kC3Yjy83NesCk2U93ft6TNkkGuvvVb+RaSyc+ecKqlSpZKMmTI523bFt54t55IESIAESIAESIAESIAESIAESIAESIAEEgOBOBPW8+XLJ01vvUWyZMnsXOepU6fVi3CPHDt2TC5evGAGWZAvtkDBAlKggCd3Zt58eaVV61ayaOEiWbd2nXMsV0iABJIngXs7tJeXBvT3ujjcE7o+1FWaNW8mvXo8Jlu3bPHaz42UTSBLlqzSpl0HLwjwVA+3qG5PAKEebfd59oVE5bkOBn+sXyenT5+yXU3wZbZs2WT6D99Kjhw5ZdKEifLaK68meJ+i68C3P36vueuzyLKly+S5p591qvfp+6R07tZFzpw+I/e0biOY+EeLewLIqe4v/PuenRvl2KG9cd8BnzNAwMcL586Zt5AULu49WRR9PauC/6ULEYKhTxPcJIEEI2DDv4cS1j2UusFc2E9z5prffGfOnJEWNzd2Dnn8yafknvb+060gRdilS5dkw7p1MvyjYbJp45/OcVwhARIgARIgARIgARIgARIInkCVKpWkcpXK5oCxX48P/kDWJAESIAESCEggToR1iOot72ppBlFw5mPHjsuqFStl27ZtATuSM1cugddq6dKlTJ169etJ6tSpZfWq1QGPScgdWXRCQJly5eS/f/9T0W+znD51MiG7kyDnrlGrttx5Vxtz7nfffD1OGVSuWl2FtPby99//yJuvDZArV64kyDXzpOElgO+8W1Rfu2atHD50SCpWqiQFCxU0k24GvTFI2rVpG94Ts7UkTaD5na0kffr0XteA8O/h9FT3alw30DbO8Vz/gb67EmwbDMAiXCHhq1StIl+PHxej65kwfrwMfnWQdH24m+Dv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WbAtEiCB0AmY1BQd2zsHusV09/0F62OdWuJ1L3PXc1VJsqsU1pPsW8eOkwAJkAAJ+BI4ffq0TJ4wVubN+UXq1GsgNWvVlqzZs3nyr6sg7vFJ91W2I5RyjyO6n+2rh6g0r6eMyLPue/7otn3P7K/+yRMnZMWyJbJ4wW9y5PAhf1VYRgIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkkAwJJDcBKhm+RbwkEkhRBIxgPnaCepxvMNcd7D0q2HpJESaF9aT4rrHPJEACJEACURI4cuSwfD99qnmVKFlKSpUuK4WKFJE8efNJlmzZJF3atOrRHiGgR4R995T5Cuw4mdnjPibKHkS/87///pOLFy/KqZMn5PDBg/LX7l2ydcsmkws++qNZgwRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgATilgBTSXjzpbDuzYNbJEACJEACyYzAju3bBC8aCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACcSUQMzj2cb0jDyOBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABJIQAQrrSejNYldJgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgATinwCF9fhnzjOSAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAkkIQIU1pPQm8WukgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJxD8BCuvxz5xnJAESIAESIAESIAESIAESIAESIAESIAESIAESuErg33/+JgsSIAESiDcCwdxzgqkTbx3miUiABFIcgWDuQcHUSXHggrzg2LCjsB4kZFYjARIgARIgARIgARIgARIgARIgARIgARIgARIIP4Erly+Hv1G2SAIkQAIBCARzzwmmToDmWUwCJEACsSYQzD0omDqx7kgybSA27CisJ9MPBS+LBEiABEiABEiABEiABEiABEiABEiABEiABJICgYsXLySFbrKPJEACyYRAMPecYOokExy8DBIggURIIJh7UDB1EuGlJYouxYYdhfVE8RayEyRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiSQMglcOHcuZV44r5oESCBBCARzzwmmToJ0niclARJIEQSCuQcFUydFwIrBRcaGHYX1GADnISRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAuEhcOXKZTl/9kx4GmMrJEACJBAFAdxrcM+Jznhfio4Q95MACcQVAd6n4oqsp91g+QbqBYX1QGRYTgIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkEC8ETp88ES/n4UlIgARSNoFQ7jWh1E3ZVHn1JEAC4SQQyr0nlLrh7GNSbiu2zCisJ+V3n30nARIgARIgARIgARIggf+zdx7wUVRbGD8Qeu+9996R3nuTroKiotJFLKgoIoIFK2J9goiCooAIKIpI7016b9J7L6Gm8O53kzuZ2ewmuymwCd95v2Tu3DZ3/juz8vLdcw4JkAAJkAAJkAAJkEAiIADv0IvnziSCO+EtkAAJ+CsBfMd4461u1s/vJUOCRxIggbtFgN9T8UvaV77uVkNh3R0V1pEACZAACZAACZAACZAACZAACZAACZAACZAACdxVAoFXr8jli+fv6jV5MRIggfuDAL5b8B3jq/F7yVdi7E8CJBBTAvyeiik578bFlK/r7BTWXYnwnARIgARIgARIgARIgARIgARIgARIgARIgARI4J4QQHhOeq7fE/S8KAkkWgL4TolN6F9+LyXaR4M3RgJ+Q4DfU/H7UcSWr311yewnLJMACZAACZAACZAACZAACZAACZAACZAACZAACZDAvSQAD9FbN29KhkyZJU269PdyKbw2CZBAAiZwPfCqFtR9Cf/u6Xb5veSJDOtJgARiQ4DfU7GhF/3YuORrrkZh3ZDgkQRIgARIgARIgARIgARIgARIgARIgARIgARIwC8IQAg7f/a0FsVSp00rqVKlluQpUkjSAP450y8+IC6CBPyQQGhIsATdvi03b96QG9eu+ZRP3Zvb4feSN5TYhwRIICoC/J6Kik7s2+KbL1bIf4nG/nPiDCRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAvFAAEJW0KXbckUuxsPsnJIESIAEfCfA7yXfmXHE/UHg6MH998eNJoC75PdU/H1IzLEef2w5MwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQCIgQGE9EXyIvAUSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIH4I0BhPf7YcmYSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIFEQIDCeiL4EHkLJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEAC8UeAwnr8seXMJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACiYAAhfVE8CHyFkiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABOKPAIX1+GPLmUmABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABBIBAQrrieBD5C2QAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAnEHwEK6/HHljOTAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAkkAgIU1hPBh8hbIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESiD8CFNbjjy1nJgESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESSAQEKKwngg+Rt0ACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJBB/BCisxx9bzkwCJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJJAICFBYTwQfIm+BBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEgg/ghQWI8/tpyZBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEggERCgsJ4IPkTeAgmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQPwRSBZ/U3NmEiABEiABEiABEiABEogdgfQZMkiDRo0lVerUsnjBfDl/7pxPE5YuW1bKVagoyZIlkx3btsr2rVslNDTU6zkwrmadulK4SBFZuWypHPjvP6/H3ouOGTNlkkpVqkqRokXl2NEjsnnjRjl75sy9WEq010yRIoVUqFRZypQrJ5cvX5Ytaq2HDh6Idpxrh6LFi0uWrFn1vQbdvu3afFfPc+fJKw0bN5Zz587K8iVL5ObNm3f1+t5crHjJkpIxYybFa4MEBwd7MyTR98F7juewVJmycufOHf2unzh+3Kf7TpsunZSvWElKKL5nzpyWVcuXyxX1XNNIgARIgARIgARIgARIgARIgARIgAQSD4EkAaly3Ek8t8M7IQESIAESIAESIAES8IVApZrNdPfNa+b7Mize+/Yf9Ly079RZ0qRJ47gWhMrpU6fI2C+/cNS7njRt0VIGv/a6pE2b1tGE8eO++lJ+nfKLo971BGL+S2p8JiVUJ0mSxGqGELll8yZ5cUB/jwL9gpWrJXmy5NYYT4UL589Jx9YtPTX7VA9Rb8So96VGzVqRxu3Ytk2GDXklksD+3U8/S7HiJSL191QRGhoijWrV8NTsU/2A51+Qro90k4CAAMe4s2fPyMg33tCir6PB5aTTQw/LY08+KVmzZpekSSM+n6tXr8q2LZtl6MuDYy0aDxr8snTs0lV9/knl2rVAad24ocsqwk6TJk0qo7/6WouqKZI7P/crV67IO8OHyeoVK9yOja4SmyQ++9/Y6Lrp9pXLl8nrg19027dT14ekx1NPS5YsWR28rl27JuvWrJG3Xh/i8Xl2O6GbSm95maGNmzWX194cLilSpNRVj3XtLEePHDbN0R6//3mK2kBSLNp+6NCiQV23mxzw2T330mD9XQNx3W541+f+9Zd88M5Ie3WkcqbMmWXYyHekeo0aju8KdLxx44aM/vB9mfvnn5HGsYIESIAESIAESIAESIAESIAESIAESCDhEWAo+IT3mXHFJEACJEACJEACJJCoCbz13ijp9liPSKI6bjpVqlTy2BNPahHZE4R2HTrK8HfejSSqm/EQ0nr16+9puGD82x98KJmVYGYX1TEA4lvVatXll5m/S2oX0R/tyZUXdkr1A7E3uh934zGHr4Zr/jRtultRHXOVLV9efvr1N8mQMaNjamxaiG6N9nZX4dExmQ8n7330iTzy6GORRHVMkT17Dvn8m7FSpVo1jzOOHPWBvPDyK7ov1me39OnTS+269WTGnLmSK3cee5PX5ew5csgvM2ZJl4cf0WvENVKq586d4XnEs4BnwlVUR/8MKuLCB6PHyIMdO7kbHm1dwcKFvf6MXDeRmMlfHz5CXnjlVcmWLZuey9TjiDGNmjSRqbP+iPR82PtFVfaFF+aBmD3qk9EyQr3n4GeeMddNFlFdE205cua0xpo5PB2TqGu6s3E/TJLOapOGu2cbdW3bt5efZ8x0+12C+SCqT1Gf/wM1a0b6rkB7ahVpY6jij+80GgmQAAmQAAmQAAmQAAmQAAmQAAmQQMIn4NyWn/Dvh3dAAiRAAiRAAiRAAiSQgAlA9G6iPFlhoaF3ZMrkH2XR/HkCj2yIk6atcdNmOqz7r7/87LhbhOJ++fWhVt3WzZvlyzGjJTgoWHr27iP1GjTQbY8r790N/66TjevXW31RgKD7ytA3rLrFCxfKX7/P0qG9mzRvId17PK7F1jx58sj7n3wqg/r1sfqikDdvPut87549cvrkSevctXDihG+hpl3Hm/MPPx0j2bJn16e3g4Lkm88/k+VLl0qlqlWUAP2q3qAAEf3r8RMEXsHG5sz+Q0qr0NfRWV3FDBsM8HnE1uBpXq9hQz0NQm7/MXOG4DPMqtY/eMjrkr9AAX2tjz//Ulo2rC+3XUK7w9O9UdOm1jJWKU9whOi/cOGCVK1eXdqpZwQbG7ApYuwPE6V9i7CIDNaAaAot27aVV4cOcyu0uhv67aQfBc8CDN7yP074TpYtXaK5dn/8CSleooS+HzyT8Ch3TWUAYbt2vXoqzcECtx72uXLnti6L0PJR2eqVKyI1YwNJK3VPMPBeuniRrF21Sq9V81KbSCAg4zqjPh4tA3o9HWmOvPnzq8+loKxxM7+vvBC2Hx74GV02eUS6qBcVSA8Bw2e/Q6V4iMrcpQgY/u57UrJ0aT0Mz/bsWTNk3t9zlFgfIA3VZoOOXR7Swn3+/AVksPr8Rgx9PdIlJkz+2RLd4Z0+8bvxKgLAas0TETdq1Kqtx+B7a/H8+Zp/pElYQQIkQAIkQAIkQAIkQAIkQAIkQAIkkGAIMBR8gvmouFASIAESIAESIAESiHsC/hYKfubf/2jPWoiAA/v0ki2bNjluuveAZ6XHkz113c4dO6TPk4872iHaGW9n5EN/4pGHHO0ff/6FJXYdPXJEunfu6GiHN7QRbr8b+438MP5bR3vFypXli7HfarEUYeWb1avjaK/boKESKD/RdQP79I42pLljcAxO4JU97fc/9HowvP8zT6lQ6FusmbLnyCm//fmX1Y6NAK6bCazObgqPqMgBA1RYfhiE4SEvvuCml/dVcxYtEXiVw6b+PFm+/HS0Y/CfCxZZouus336TT95/z9E+/c85klN5KsNGvDFUFvwz19GOUOe/zJxlRTvo1Ka1CoF/2tHH08kbI9+WFq1aW807t2+XwipXPbyOsWGhSe2aVhsKiBSwcMUqzRYh1R9q3y5STm2E20fObdiEcWPl+2/H6TJ+5cyVS312f2rxFs9Siwb1IoVjf/Ptd6VZy5Z6U0ODGp69+K1JXQqz5s5T4fKzalH9ub59Ij2PhQoXkYlTpuk1uLtHhJCHtzts186d0vuJHtYVfOXVqm07HfrdRIG4dOmSXFY/BQsV0nP2eKirHDp4wJo/usKydes1e2zO+Oi9d6Pr7mjPqrz3Z6qoBmYtLw4cIP+qkPh2A5tJU6fpPhDeWzaqLzeuX7e64H5eH/6WPg9Sz0e7Zk1UyoBrVjsKSHeAzUKwU2qTTdcHwzY56Ar+IgESIAESIAESIAESIAESIAESIAESSHAE3MfES3C3wQWTAAmQAAmQAAmQAAkkdAIIC41w1bDDhw5FEtVRP/5/X2uREGV4A9sNIaYrV61qVY0cFuG5birffnOY5XkN72h4uNutUvh45Fd2FdXRD0L/mTNn9BCst4gSXu2WO9x7GXW+iIT2OXwp9+4/wBIHIQTbRXXMA1F58cIF1pR9Bgy0yt4U4OUPw0aH998e6c0Qj32w6cCI6vDu/fqzMZH62oX0lm3aONrB24jqyF3uKqqj84UL55XX8d/WuE4POTdWWA1uClWrP6BrIaJ+rbz++/R8wq0XuRmKqAlGmP37z9mRRHX0m6A2Zxir36iRKepjZxVqHqHLYbi3OvXr67L9FwRg2M1bN+3VXpXhaQ5RHbZ39+5Iojrq8YweP3YURR3KHh7ldntQeV0bKxXu3W3OfeVVq249ixciKiCawLlzZ810Ph3h6W/Ynzjue+SHZ1940RqPd8ZVVMdiwObnSRPl3NmzcuH8OWuDhFlor/4R6SSwCcdVVEe/X6f8ojeyYI5Q9Q7hO4pGAiRAAiRAAiRAAiRAAiRAAiRAAiSQcAkwFHzC/ey4chIgARIgARIgARJIVATyFywoR48ekSTqf/BCdWehoaEC7154EQcEOP8pC2HSiG07tm2T//btizQFPGQXzvtHewGjsUXr1g4BHaG6rwUGyu5dOyONNRUQq43Am17l0LabXVi/dPGivSleygj3bmzEG5FDVaMNgnjDxk21iFukWDHTPdpjj55PSZrwcNsrli2T2N5Pk+bNrWv+9MP3kbyz0YiQ6KefD+MLsRke+adOntDj7GHh8Rx4spCQYKspRG2Q8MXgRT2wdy+vNkUgf7p5XhFW353tUlEVjLnmaUeKgW4qIgAsJCRE1qgQ7a6WJWsWXXVdPZO+2o1r1+XTjz7UwzasW+txuHln0AFe1XZDGoai4c/M8WPH7E267AsvDMCGlXffGu52U0SkyaOoyKc2xRg7djRsY4A59+ZYqUrYBhxsGHn95Zc8Dvnmyy8EP66GaAXZsoWlX7io3vPJE39w7WKdu6aLsBpYIAESIAESIAESIAESIAESIAESIAESSHAEnH+NTHDL54JJgARIgARIgARIgAQSC4F9Kid5907O0Oyu9wYhG6I67NIlp3DdoFFjq/v2bZ5zLiO3OsJrwx5QOZDtnuk9uz9izeGpgHzTxlw9xLPnyKGbEBoahr7NlXhfQG0agKi/bctmmT93rm6Li19G3INg6clzF+Grr1y5LJkyZdKe0fCCds317boWeNYiDz0M4uOH777t2sXn83LlK1hj3OUDN43YEGE2LjRt0UIgwsMgpkPERP503EvNOnUj5f1Omy6dNLeFc0dOcW/tf8pLHV7wUYn29rngjYyfqAz5040d+u+AKeojojI83OFBlXqgmfypRHZ3ecDTZ8io+5pnHWkOaterLzlUiP9Tp07q/PKu6RLMReC9P2PaVHPq9ohw53nz5dNtyBGPTSV2m6RyxiMSArzfZ7tsdvGVFzYSjPnwAx1VwH6NmJTz5stvDTt88KDOc/5gx05SVEWxwGaK//bvVznTZzpCt5sByClvPPnxPGHDCDZxNGjcRCpWriI5cim2J07ofOjuPNkxTz0VfcFsSNgent8d7z7mKFehgv6OQqqJmdN/leMxEP7NWnkkARIgARIgARIgARIgARIgARIgARLwLwIU1v3r8+BqSIAESIAESIAESIAEoiDw9gdhHrjosmblCkdP5BM3dlDlV/dke/fstpog0vpiyJmcMWOY2HlCiW+uImzWcC9WhDr/4NPPpHbduo7pO3bpKq8OGy6vDBroU65zxyThJ/Zw2IEugqhr/wvnz2sxGvXIaR2dsI4Q8BAbYStU2O7YeqtjHrt3f1Sfz9Ejh1XvMG558oaJvhgPGzXyLXn/kzHa+/7DT8fIvLl/y9JFC/WmBYQmf6TH45aX/Xq1gQKbNby1eX/PidQ1NqG7MbZ/eH56TPyHEnpdDZshovJ2TpMmjR6CMOPT/pgtuZUHv93g8X727Bnp2b2bZmBvi6qMXPRN1eaS/s8N0gIxNk+ArTtbr7zd8eNqvvJauzqyRz6iU8TE8uSNSOHQoHFjeap3XyusvplvwKAXZOJ33+rc9qYOR2wkMKL4ScW/Zdu2Mvi1oZJSeaHbrb0Kg48Q7k90ezhSmP9itjQUe1R0ixdffU06dO5szWvmebj7o4Ln8IX+/UwVjyRAAiRAAiRAAiRAAiRAAiRAAiRAAgmYAIX1BPzhcekkQAIkQAIkQAIkcD8RgEdq1WrV9S0jHPxH773ruH27cLt/315Hm/3ELuqmUbmavTV49w54/gWr+5tDXrHKppApcyZdRJhwV1Hd9IGAN/rLr2VAr2dkRxSe9aa/p6N9I8Glixc8ddP1CPFt8sFnyx7mVe9pAAThbkqghiHf+Adx4K2OuRA+Gwbvevx4Mvvnk8ll48PqFSu0l/cnX3ypowC0UN7p+LEbwqpPnzrFbQ53e7/4Lr/70cdWTvk9u3ZF2gjizfVTpgzb3FCxUmWP3bOrz3Py9Bmai6vHuX3Qa28OV6kPwvLWBwQEWE3wVB/9wfuyfMkSq87fC7ly57aW+Exf96I18tf37NVbkELARD3AIPt7kyJlShk6fIQ1FzYYGNEdldmyZ5eff5sp3Tp1kKtXrlj9smQJC9GPCkQQKFO2rNWGdwbXNlZNbfj4/Jtx8lzf3qaKRxIgARIgARIgARIgARIgARIgARIggQRKIGkCXTeXTQIkQAIkQAIkQAIkcB8RqPZADeVVGpFDfNTIEZHEWRMiHlgQHtqTQdSF+AVLqYQ1byyjCj0+duIkldc9TJD8R3k3Qyx1tfTpI3Ku31KC3qcq9HXjOrWkQY3qOpezCdeOeb4c961kCPd+d53Hm3O76HzhfNTCOvLCG8O9RGV9nh1oee8uW7LIJ0/oqOZNFhC2p/fWrVtRdZNjR49Y7fDKdzWEh89sEzZd25MkSSrIdW/faOHaJ77P+yqGdes30JfB8/baYM95vD2tBRsc7AItvKcH9e8r9apXlZaNGsjH748SbDCBIYrC9z9HHZYezyaeO/MMm+teV6kC7M+Sqffno0m5gDVCDP99xm/SukkjzaZPzydk1Yrl1vL7DHhW7GkisuUIy42ODsXDPc/Pq4gOyLXesOYD0rxBPfngnbfldng6B7Cd8NPP1nwo2HkZUX3Txo2CazeoUU06tWnliETCQplVAABAAElEQVRQuWpVed0m4Dsm4wkJkAAJkAAJkAAJkAAJkAAJkAAJkECCIUCP9QTzUXGhJEACJEACJEACJHB/EoCn9UeffW55kk4YN1YWzZ8XCYYRGdGAnNDI1e3O7IIlvFmjsxTK0/qHX6ZaIcaRc/qdN4e5HWaEeqylc9vWjhDS8AiGx/Vk5QGbRwm/yPXctEVLKw9242bNZeiIkW7nNZWHDhyQpx/rrk+vXL5kqiU6sRyet8auXo3wvDV15og1Idw9DJsPXKMCmH6+rhXjQkKClVCcXMAzKsttC/N948Z1R9fnXhpsrQ8NJ0+ekP179ynOl6Rk6dKCqAK4h/oNG0lZldO9i/oMovKOd0weRyftO3eRR594Us8GhoP69RH7xgZvL5M1Wzar6wGV2uCJRx6yzuGZ/vtv03WI9klTf5UUyZPrMPHYTGD3rLYGqMKPP0yQ06dO6SrMXbBwYR3FAPnsBymudRs0kOf79bUP8duyyT2PBT4/oJ9s/Pdfa614P1994Xl5Q71LxkO/Q9euOmc6OiEHu93OnTsn3Tq2tzYp3FAbDZDzHrnTJ0z+WZIrtvCQt7MNDQm1TyErly+TIS9GRLM4e+aMfPPlF3L82DF5Zegbui/40kiABEiABEiABEiABEiABEiABEiABBI2AQrrCfvz4+pJgARIgARIgARIIFETyJkrl4yb9JMWS3Gjc/6cLd9/O87tPdsFxWLFS3gU1gspQdEYcqFHZRDhf5gyVbKFi5zIq97v6Z4ehzzUvp0gLzbCa9vXYwZA5H3vrTeVt/p4XVVHhZGeMW2qLkO4g0AalWXJmtVqPn06wgvdXm91sBVy5ooInX3uzFlbi7OIvNQQEmFLFy9ybAyw9/R1rRgbpDyAMTd+wNU1P72Zv3CRoqboyO2OqAV20f9dxdE1zzcE4/HqecFGgqyK1fujP5XBzw205ovvQqOmTeWlV4dYlxnxxuuydfNm69yXAsTZLu3aaA9zeKu7s+NHj8rfs/8Q5AOHYcMDBHd3tmvHDsGP3bARYfxPk3WEAqRZaNeho8x2kwvePsYfyi8NHCCZVZoAPFPg5M7eGf6mNGvZWnv9lyhZyuoC73S7fTH6E0tUt9cfOnhAli9dIo2bNtPVTZq3kFnTf9Xly5cv27vKsCGvOs7NCVg+1buPfh7Tp08vadOlk6jC9ZtxPJIACZAACZAACZAACZAACZAACZAACfgnAYaC98/PhasiARIgARIgARIggfueAMKkT5wyTYt+gPHv2rUyasRbHrnYxccixYp57FfcJrJduhB1CPVvvp8o+fMX0HNdvHhRnuz2sEdBGJ0uqT4I9+5OVDcL2rJpkxWKvkSpCMFv5/Ztsm3Llih/7HmwIdAhDDYsnRLsojK79/PRI4fddoUneYcuXXQbPK0/HvWu236o9HWtGIPNBsbsmxtMnTkWKFjQFAW54Y092LGTKaqc2RMiiepoPK+8j7t36WRxqVi5ijUmvgtVqlWTt95934qs8NVnY9xGVrCvA8942/YdrPzz9jaU4WGO5ymqyAqLFsy3htWuW88qe1OAePy+SqtgrK0S1l0N4nuDxk1cq+/pObzKwcWTqG4Wd0pFNIBlwKaV8EgJJ44dN836uExtIPFk9vetbv36Vrczp8M8/01FUBSRL3bu2G66WSK9VcECCZAACZAACZAACZAACZAACZAACZBAgiJAj/UE9XFxsSRAAiRAAiRAAiRwfxBIlSqV/Dhtupgc2/D6ffHZ/lHe/IplS5WHakvdp1TpMh77VqpS1Wpbv26tVXYtfPzFl1K6TNg8V65ckUeVYAtBz5PBC7thk6a6GZ7BJ084BTwzDv2SJAk7Q9nYvj17pP8zT5lTr44X1MYAeGbDCzxLlqxy4YLTGxeTJFeCIoRFGPJGm3DgusL2C2HWEUYdtmTRAo/e6miPyVp3KYERYcdhVZX3OcKbu7MiRSM2RSy2icaly5a1us/67Ter7FrAZwSvZEQZwHOEMPmXL0WEzXftHxfnxUuWlNFffq29ozHf+G/+J1N++jHKqXPnyStTZ/2uhfgXlJd7s7q1HZs2kMe7ivIih61SocbtqQ7sE6dIkdI6NZ8fKvIXKChlypVTc4bI/LlzrT6uhSULF8jwd8I2USA3vd0QIQDPBWzf3r3y1KNhaQLsfe5FuUr16pIpU2a18eKE2uQRIVy7rsX+fpk2jDHRE1CXJm1aj896OuVlbpl5aVXFiqVLtSc62q5du2Z1cVdInTqNVZ3ENodVyQIJkAAJkAAJkAAJkAAJkAAJkAAJkECCIRDxl7wEs2QulARIgARIgARIgARIIDETgBg2UeWNzpIli77NXTt3yoBeT0d7ywjbbDy4K1etKhAuXS21CtPesk0bq9o1lLhpQH7mGjVr6VMIZxDVo/JCR0fkYR7x3ij9897HH5upIh2rPvCA5dUMD/fY2I5tW63hw955xyrbC88PflmHE0cdcrS7M3CB5zQszFv9PXfdYlW3dFGEZ3DPXr3dzoVw73nCc6xjE8DhQ4esfkHBQVa5Rq2wz8aqcCkgTLixWyrffXwa1ovIBgEBAfoyP/7wvUz8LizUf1TX7ajyfhuhFSkA6tg8ojGug8rVbp6nJ5/p5XGqRk0ivMmPHztq9evVf4DOM/7m2+9G6XFetnx5a4wrqzbhzwQ6FCte3Op3rwsfjB6j2XwRnlLB03py5Mylm0JCQhxe/wdt74HJw+5ujgaNG1vVRw9HRHrYt3eP3qSCRmz+weYNT1YqfHMO2jGORgIkQAIkQAIkQAIkQAIkQAIkQAIkkHAJUFhPuJ8dV04CJEACJEACJEACiZLA+B8nS55wz9n9+/ZJ355PeHWfCMdsckhDsHzr3cji8KtvDLO8spGj3C7cmov0H/S8GLHtusrBDlHdGwEcoanhQQ4rWqy42D3jzdwQsN8Y8bY5FXsYb6vSh8K3X39t9a5W/QFB2G67hYUajwjv7Sk//fMvv2IJw4sXzo92E4H9Gt6WF82fZ3ldI9/0E08/E2no62+9ZdUtXbjQKqOwZ9cu67z3gGe1h75VYStgU4QRuW+oz8+Tp7dtSIyL8Cr//pepAmEcNvXnyTLuqy+9mm/2zJlWv+DgYFmzapV1jsKvv/xsbRTp+NDDgmu5GiIzmGcVbTOmTbO6/DjhO6s8aPBgwbPnatjE8sIrETnhEYnAbvPn/m2dHj1yxCrfjQK87x9/6ml5Y+TbkjNXmEBurvvvmjW6CO79nhtkqh3HT774yoogsF9529vtvRHDrdO+A5+TgoUKWeem0LRFS0HeeWP4POxm5zt+0k9uw/m/9uZwwbMOw7Novp/s87BMAiRAAiRAAiRAAiRAAiRAAiRAAiSQcAgkCUiVIywxY8JZM1dKAiRAAiRAAiRAAiQQRwQq1WymZ9q8JiJPcxxNHaNpxvzvG4eY9cfMGVY+cncTzvx1qiOkeJGiRXVedtN3wbx/VK7w9yRECZcQY7s8/IjlJfz6yy+JPYcyxnR7rIdAWDe2ZNFCJap7DiOOUPJLVR9jEPm693hcn8J7fvrUKfLnrJk6NHnzlq3ksZ5PWZ7411XI8nbNmjg8ac08vhzHKm9phPyGYc43h7wqa1evEoROH/XJpzpUPNqwkaBL29YoOgwet38tXKzFaHirt23WOF6EdVwUntdP9+mrr49rffPFZ/LrlF+0x+/wd0dJ5SphOdHR1rpJQ0EeeWMQln+fO98SS+HR/seM32TdmtV64wO83Vu1badCoBcwQ+S7sd/ID+O/tc59Lcxdskx7JONaTWrXdAwHt6m/z5aMKk+6sahC1KPPpx++7wj3DsG4fqPGMmf2H457NfN9//MUy1P8lto48s0XnwvC46dLl06at2ot3R9/wtoosl1FL+j3VE8zVB9//2e+9bxhg8HUyT/J+nXr9LVq1K4tXVSod6QSgIF5l3ZtVN7y0/rc/KpQqZLkyZdP5s2Z41i7abcfo+Jl72fKn/1vrAp3X02f9nioqyDnu7EXX31NOnbpok8vqsgODzZvapoEofcn/BQhdK9TQvvkST/I9i1bpG6DhvJw90etdwL3hRQL9ugOmAih+6vXqKHnBNspP06S2epdzZw5i7Tr2FHaqXzzJqLAv2vXuk1FgffGpFnA+/XzpInyz5y/pHLVaortI47vsi8+HS3T1MYLGgmQAAmQAAmQAAmQAAmQAAmQAAmQQMIlQGE94X52XDkJkAAJkAAJkAAJxJqAPwnrEBmnz/7Lp3v69MMPZMavEV66GAxP6Gf69otyHgiZo0aOiNRn/vKVOi93pAYPFRDWXxjQ32qFB/BHn30hD9R0irBWh/ACwst379TRbU50177RnadVIuuvf/xpeca66w9h+NHOnXROatd2hAo3uemRi3vksKGuXeL0/OvvvpfyFSpEOefw118TeLi7GjZOYDxE7agMmxqQ53ySzWs7qv6e2qISiu2bBDyNd61vXKeWILKCt6Y94pW4jnzxURnSJSCyQ2hoqKMb3qlxE3+0xHVHo+0EovvA3r1k966dtlrfi1HxcjdbVML6d0o4L6EEdBg+z/oPhAnwZp7HnuwpfdRmmagMovrLzw+UdatXR+qG8O2TbCknInUIr9i4fr0M6tfHbTM2c3w45jNJHh6xwG0nVTlWRTH4SaUIoJEACZAACZAACZAACZAACZAACZAACSRsAgwFn7A/P66eBEiABEiABBItAeTI/vybcfoH5fiyp3r3keX/bhAcvTWzNl/GeDv3/dwvSRLf/2l669atSMiQ3/rrz8ZIkBKTXQ25lqcor113orprX2/Ob992XgPC5ksDB+iQ4Ajv7c5OnDghj3RsHyeiOuaHV/cTjzyk8jc7w12bax87elSefrS7W1EdGwGaNG+hu0KE/OSDUWZYvB2f7fW0DoEPsdTV4HHvSVRH3wP//ScdWjYXiJ0Ire1q+HxPnzolz/XtE2tR3XVu13PjzexaH9X5HRfhO6q+aEMKgq7Ki3yb8sR2xwt18Nbu/USPSKI6xoNFx1YtZNWKFW5D4mPDxV4V/r3rg21jLarjenFpSFuAzxO2xCUtAOogVCPqhLvnAO14ll54tp9bUR3tly9d0mzm2cLdo94Y2CJihSdRHf2wsaZj65by3/79ZpjjiO8geKpTVHdg4QkJkAAJkAAJkAAJkAAJkAAJkAAJJFgC9FhPsB8dF04CJEACJEACiZeAEa7td/hc396yacMGe1WclCHeG+EeQs6EcWOjnBdies9evXUfrAfrSsjmTx7rcc0xRYoUOsx2qTJl9NT79+3Vod/t4cXj+pqu8yHneYPGjSWr8jg+o0JFr1i61BHu2rV/bM+LFi+uvOVrSZ68+XRIbwh/O7dvj+208TIeTBC2u1DhwnJLeUzv2L5NVi5b5lYg9rQA5A3H/SI0+qYN6wV57hOrYRNEhUqVBSHc4bF/5PBhmff3HLly+bLXt4xxNWrXkRQpU8gGFRL+7JkzXo+9Fx2Tq3cYodbPnzsX5eXh2d+wSVMpWqy4XLp0UbZs3KhF7ygH2RpTpUol1VWUifIVKwm+Nw4dPCjzFVtElvDWsmTJKrXq1pWSpUtrsX/Prl1qQ8ACn55nb6/FfiRAAiRAAiRAAiRAAiRAAiRAAiRAAveGAIX1e8OdVyUBEiABEiABEvBAwJ2obrrGh7juer2oxHW7qI41RdXXrNnfj4lZWPd39lwfCZAACZAACZAACZAACZAACZAACZAACZAACZBAwiEQkDRZ2rcSznK5UhIgARIgARIggcRMwJ3IvXnjBsujvFXbdoLzUydPxhkGzIWQzsZrHUecu3rHJ0ZRHRBz5SuqWZ46diDOmHIiEiABEiABEiABEiABEiABEiABEiABEiABEiABEkhsBJLF5w2lTJ1WMmXJIekyZJbUadJJsuQp4/NyXs8dHHRLblwPlMArF+XShTNy64b3If68vgg7kgAJkAAJkAAJ+ETAnahuD8tuwq8jdHtce66b65hrmKOpT6yiuk8fEDuTAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQwH1MIF481iGo5ytUUvIXKSPpM2aRlKnSSNKAeNXwffoIsRasCWvLniu/Lt9U4npIcJBP87AzCZAACZAACZBA3BCITlSH97jdqzw+PNddr2E81ytXrWblVMfdJobw7/ZPjR7rdhoskwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkIB7AnEurGfNkVeKlq6iPNTTu7+iH9ZirRDYg24rT/ZrV/1whVwSCZAACZAACSReAtGJ6ubOXYXvuyWuY33GEpuojvuisG4+XR5JgARIgARIgARIgARIgARIgARIgARIgARIgARIwDOBOBXWc+YtLHmVp3pCtYyZs8udO3fk2tVLCfUWuG4SIAESIAESSHAEWrd70MpvHp1w7Squ58qdR/7+c3ac3rPrNczk0a3N9EtoRwrrCe0T43pJgARIgARIgARIgARIgARIgARIgARIgARIgATuBYE4i88OT/Xc+YtFugeI1BfOnZTAyxfk1s3rkdrvRQXCwKdTYeCzZMstadNnciwB9xAcdFvOnznuqOcJCZAACZAACZBA/BCoVCXMI9xb4drkPUce9M0bN8TLouzXwAW8XVu8LIaTkgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJ3HMCSQJS5bgT21Ugp3rpirUjTXP04C45f/pYpHp/qsiaM5/kL1w60pJ2bVklt1TedVoEgaRJk8rqf9dYFc0aNZUrV65Y594U0qRJIx+O/lhKly4tWbJmkYCkAbJq5Urp26uPHv7rjOlSoGABXX7v7Xfl91m/ezMt+5AACZAACSRwAgi3Dk9xXywmY3yZ3/S9W9cx17vbx0o1m+lLbl4z/25fmtcjARIgARIgARIgARIgARIgARIgARIgARIgARIggQRDIGlcrDSXCgHvav/t2uj3ojrWDOEfa3U1d/fk2seb85deGSxbd27XP2s3rPM4pGKlSlY/9M+RM6fbvkkDAmTjts1W3/7PDnDbL74q06RNK+YnIJlvAQ8yZMgg8xbNlwYNG6j7yyHJ1PgkSZM47jV33jzW/NmyZ4+v2+C8JEACJEACfkbAV1Edy4/JmJjc9t26TkzWxjEkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAJ3h0CshXV4q2dWIdXtBk/1q5fP26v8uoy1Ys12wz3h3mJrWzdv0eIxBGQI0kWLFXU7ZafOnax+6NuxU0e3/WrXriXJkye3+i5etMhtP3+s7Ny1i2TMFBF6//at27L+3/Uy87cZ/rhcrokESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAENIFYC+uZsuRwoEROdX8P/+5YcPgJ1oy128313uxt3pYXLlTCty3Yfus2bdwOrVWnlqO+abOmjnNz0qxFC1OUoNtBsmunc0OA1eiHhZq1alqrunD+glStWFl69nhCfpw4yapngQRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgAT8jYBvsbzdrD5dhsyO2gvnTjrOvT158MUHddc/Rv9hDUGdqbcqvShgDvs8XgzRXbD2tOkjPKpxb6ePH/R2uNt+oSEhcvz4ccmbL69ur6U8zr/47HNHX4REz507j6OuaPFijnNzUq16VVOUPXv2WOWEUMibL5+1zLVr1lplFkiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEjAnwnEWlhPnSad4/4CL19wnHt7YgT0mAji3l4jun6ua3e9t+jGe2pfuWKFPPTIw7q5WInikbo1bd5MJImzGuHey1coL9u2bnM02MXpRQsWOtpwghzs3bp3E+Rsz5Urp1y5elUOHTwokyf9JCdPut/0ULlKZalSNUywX7N6jezYvl2dV5H2HTpIvvz5ZPbvf8ismbMiXctdRb0G9aVEiRJW0+Qff5KWrVpK1mzZJFPmiE0L2GjwdK9ndL9NGzfKxg2R89xbk7gpFCteXB7p9ojkL5BfUqRMKSdPnJTVq1bptbp27/5od0mdJo2unvj9DxIcHOzo0rxlC8mfP7+uWzh/gRw6dMjRjs/hgRo1dN2BAwdkMaIQRGHe8sQ9IN88bPu2beJus0Gp0qWkTt26us/hQ4dlwfz5uoxfrdu0ltx5wjZkzFDh9C9euKDW+YC0adtW8hcsIAf+OyBLlyyR5UuXWWNYIAESIAF/JVBZ/XfI11zmMRnj6/3fjWv4uib2JwESIAESIAESIAESIAESIAESIAESIAESIAESIAESuPsEYi2sJ0ue0rHqWzevO85jcxJTz/OYXtN17a73FtN5Z0yfYQnrqVOnlqxZs8r58xE56Nu0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VNeD/Px8mTH9Z9HZ65p69TrT/CzpHw3GJ7VrZ1bZuWOHHVTXgo0bNsia1avMczpDPsqYvW4lX9tZ7fVTXwyoW6+eNGvWXMLDw52nyCOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQAALMGM9gB9eZRl6o8RE81Z0yXZPM9KnTf1RLhk+QqpHVDdnm1tLxHu6/1q1a5sBbj035ftvi1SZ+tOP0japnVmnQcNGsn7dWrOOr+20cWhoqAy79DJp1ry5fW0tz8jIkC8nfC67d+/SQxICCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCASoADPWA/TBVaZh165T17ydgykpUlBQUOTWtm7ZYpdpALykVLduPfO0znY/dPBgkapZmZmSlZllltete/y6euBrO217y213SPMWLcyg+uFDh4zrHjLvIzIyUq67caTUqlXymLUPEgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+K8AM9b999kEzcgSEmqZ93r48CGP95ydnW0Gqs2l1o3A+b69ez3W00IrQK4B9OJSalqqRERGiBXQL2u7mjVrmpf64vPxsnnTRjMfXr263PmXeyQsLEzO6XuuTJo4objhUI4AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAn4uQGDdzx9QMAwvOub4/uo607u4pLPMNRjunGXuqa41oz01NdXTabPsyOHDUqdOHWMm+fGAvhb62q5ho0b2dXZs32bns7Oy5K3XX5O4+HjJMV4MICGAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQOAKsBR84D67yjfyKsXfks5W1+RhpXiXRvZS8n/WdznpdpCfX7jsvK/ttm3bavd6x133SM9eZ0h0dIxZpjPt9+7ZIwc9LElvNyKDAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAJ+L0Bg3e8fUeUf4NGjR82bjDdmdxeXwquHm6f27St+GXitkHzggFkvOvr4LHjzwO2f2NhYsyQlJdk+42u7lORkWTBvntmPzqjve14/ufPue+Sv/3e/nH/BUImNi7OvQQYBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAJTgMB6YD63SjVqDU5rio31HISuHhEh1oz1EwXWrfPVjT3Oi0vW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t1z1iywtQYAAAQIECBAgQIAAAQIECBCoaQEz1mtaWP0ECBAgQIAAAQIEqlHghfuvK1Xbiw+uXp681IXcyeL5s+I3J28Xo//3WHopWTq+W24P8iQA3aRJ01xgdFl6bdmSRWvemp0XFxVl6fISE955OZblZlAnRxIc7953UBZUnz1tQlz74yNj7vRJa1XRuXu/tHxyTz6onhRK+pfPS76T/dTzRzLz/Y/f2z83vtUB9XxQfdG8mXHFd/eLmZNXLxmf3LdyxfL87WV+58e5csWqWdJrFlpztvf66lvz/to+X7liRdbk8uVljykpkOyLXvJYUY7TuHdG5hxXl0+Xfv80qF60cmU8dvff44LTVr8MUbLeJL0hbSXlH/rXdXHj78+PpO7kyAfVkz789dJzIlku3kGAAAECBAgQIECAAAECBAgQqC2BJs1a9yiurca0Q4AAAQIECBAgUL8ERuy8ahnt1196tE47Vl/6UacINdx406bNokf/odF7060jWU591pRxMeujD6q91WQp+B79h0THrr0iCXIne6LPmzm52tvJV9iu00ax8earlib/cPSr6csE+Wu+a0YgWY6938ChsVHPfrF44fz4YPTrMSm3JH3+5YSaaHXAkK1ji213ibdffS63X/y7NdGEOgkQIECAAAECBAgQIECAAIEGLlDVv0FaCr6B/0AMjwABAgQIECBAgEAikMy4njbx3fRTkyILZk+N5FNbRzIr/92XH66t5rSTE5gyYUz6qU2MCWPfiuTjIECAAAECBAgQIECAAAECBAjUlYCl4OtKXrsECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUBACAusF8Zh0kgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgTqSkBgva7ktUuAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBSEgsF4Qj0knCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCuBATW60peuwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQEAIC6wXxmHSSAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBOpKQGC9ruS1S4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIFISCwXhCPSScJECBAgAABAgQIECBAgAABAgQI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AIGGKNC0IQ7KmAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQHUJCKxXl6R6CBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBBCgisN8jHalAECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUF0CAuvVJakeAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGiQAgLrDfKxGhQBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVJeAwHp1SaqHAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBBqkgMB6g3ysBkWAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC1SUgsF5dkuohQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgQYpILDeIB+rQREgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAdQkIrFeXpHoIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoEEKCKw3yMdqUAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQXQLNq6si9RAgQIAAAQIECBAgUHMCJ15wcyxbujDmz/oopox7M0Y9e08UFxfXXINqJkCAAAECBAgQIECAAAECBAgQIEAgExBYzygkCBAgQIAAAQIECNRfgcEj9ijVuQO++qO47NSdBddLqTghQIAAAQIECBAgQIAAAQIECBAgUDMCloKvGVe1EiBAgAABAgQIEKhWgesvOCbu/tPZMf7tF9N6O3XrE9vseWS1tqEyAgQIECBAgAABAgQIECBAgAABAgTKFhBYL9tFLgECBAgQIECAAIF6JTD+rRfj1cduixsuPC6KilamfeszcKt61UedIUCAAAECBAgQIECAAAECBAgQINBQBQTWG+qTNS4CBAgQIECAAIEGKZAE1ZcunJeOrdNGfRrkGA2KAAECBAgQIECAAAECBAgQIECAQH0TEFivb09EfwgQIECAAAECBAisR2DlyuVpiSZNm62npMsECBAgQIAAAQIECBAgQIAAAQIECFSHgMB6dSiqgwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQarIDAeoN9tAZGgAABAgQIECDQUAWKi4vSobXt2LWhDtG4CBAgQIAAAQIECBAgQIAAAQIECNQrAYH1evU4dIYAAQIECBAgQIDA+gWWLJiTFuo/ZNv1F1aCAAECBAgQIECAAAECBAgQIECAAIEqCwisV5lQBQQIECBAgAABAgRqV2Ds68+kDTZr3iL2POpbtdu41ggQIECAAAECBAgQIECAAAECBAg0QoHmjXDMhkyAAAECBAgQIECgoAUev/Wy6Ny9X2y188Gx/1fOif2+fHYsX7Y0xrz2ZNx22ekFPTadJ0CAAAECBAgQIECAAAECBAgQIFAfBcxYr49PRZ8IECBAgAABAgQIlCOwcsXymPL+G7Fo/sy0VJMmTaNl63bRsVuvcu5yiQABAgQIECBAgAABAgQIECBAgACBygqYsV5ZOfcRIECAAAECBAgQqCOBHQ86IQ4+8fy09SnjRsWdV5wZMyePjeLi4jrqkWYJECBAgAABAgQIECBAgAABAgQINGwBgfWG/XyNjgABAgQIECBAoAEKfGbvL2aj+uuFX4plSxZl5xIECBAgQIAAAQIECBAgQIAAAQIECFS/gKXgq99UjQQIECBAgAABAgRqVKBjt95p/dM/HC2oXqPSKidAgAABAgQIECBAgAABAgQIECCwSkBg3S+BAAECBAgQIECAQIEKzJ46oUB7rtsECBAgQIAAAQIECBAgQIAAAQIECktAYL2wnpfeEiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEAtCwis1zK45ggQIECAAAECBAhUVaBl63ZpFUUrV1S1KvcTIECAAAECBAgQIECAAAECBAgQIFABAYH1CiApQoAAAQIECBAgQKC+CHTvOzhat+2QdmfGpDH1pVv6QYAAAQIECBAgQIAAAQIECBAgQKBBCzRv0KMzOAIECBAgQIAAAQINROA7VzwRbdp1jA5demYjevul/2RpCQIECBAgQIAAAQIECBAgQIAAAQIEak5AYL3mbNVMgAABAgQIECBAoNoEevQbktW1dPH8eOQfv4npE0dneRIECBAgQIAAAQIECBAgQIAAAQIECNScgMB6zdmqmQABAgQIECBAgEC1Cfz29F1j5YoVsWjujCgqWllt9aqIAAECBAgQIECAAAECBAgQIECAAIH1Cwisr99ICQIECBAgQIAAAQJ1LjB3+qQ674MOECBAgAABAgQIECBAgAABAgQIEGisAk0b68CNmwABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVERAYL0iSsoQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQKMVEFhvtI/ewAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgIgIC6xVRUoYAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEGq2AwHqjffQGToAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVERBYr4iSMgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQaAUE1hvtozdwAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiIgMB6RZSUIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFGKyCw3mgfvYETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQEUEBNYroqQMAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECDRageaNduQGToAAAQIECBAgQKCaBXpusnkcfOIFsXjB7Jg3c3K8P/LpGP/2S9XciuoIECBAgAABAgQIECBAgAABAgQIEKhtAYH12hbXHgECBAgQIECAQIMV6NKjfwzeZvdsfHse+a14/ek7444/fC/LkyBAgAABAgQIECBAgAABAgQIECBAoPAELAVfeM9MjwkQIECAAAECBOqpwNiRT8XNvzklHrj+glgwZ1rayxF7HhVtO3Sppz3WLQIECBAgQIAAAQIECBAgQIAAAQIEKiIgsF4RJWUIECBAgAABAgQIVEBg5Yrl8e5/H4mXHrwhDbDnb+nRf2g+6ZsAAQIECBAgQIAAAQIECBAgQIAAgQIUEFgvwIemywQIECBAgAABAvVfYMaksVknO/fol6UlCBAgQIAAAQIECBAgQIAAAQIECBAoPAGB9cJ7ZnpMgAABAgQIECBQAAIrl3+S9bJp02ZZWoIAAQIECBAgQIAAAQIECBAgQIAAgcITEFgvvGemxwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQiwIC67WIrSkCBAgQIECAAIHGI1BcXJQNtn2njbK0BAECBAgQIECAAAECBAgQIECAAAEChScgsF54z0yPCRAgQIAAAQIECkCguLg4iopWpj3dcueDC6DHukiAAAECBAgQIECAAAECBAgQIECAwLoEBNbXJSOfAAECBAgQIECAQBUFZkwak9bQd/DwGLj1LlWsze0ECBAgQIAAAQIECBAgQIAAAQIECNSVgMB6XclrlwABAgQIECBAoMEL3HzRKTF57OvpOE/5+e3x8zsmxPk3j44dDzqhwY/dAAkQIECAAAECBAgQIECAAAECBAg0JAGB9Yb0NI2FAAECBAgQIECgXgksmj8rpk18N5YtXZz2q2nTZtGqTbto26FzveqnzhAgQIAAAQIECBAgQIAAAQIECBAgUL5A8/Ivu0qAAAECBAgQIECAQGUFjjv76hj6mX3S21/+z9/j6buuigWzp1a2OvcRIECAAAECBAgQIECAAAECBAgQIFBHAgLrdQSvWQIECBAgQIAAgYYvMGj47ukgF8yZFvdfe17DH7AREiBAgAABAgQIECBAgAABAgQIEGigApaCb6AP1rAIECBAgAABAgTqVqBJkybRrHmLtBOjnruvbjujdQIECBAgQIAAAQIECBAgQIAAAQIEqiQgsF4lPjcTIECAAAECBAgQKFugSZPV/9Se8eF7ZReSS4AAAQIECBAgQIAAAQIECBAgQIBAQQis/mtfQXRXJwkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQO0KCKzXrrfWCBAgQIAAAQIEGolA245dspF+vGRhlpYgQIAAAQIECBAgQIAAAQIECBAgQKDwBATWC++Z6TEBAgQIECBAgEABCAzf/fCsl7OmjMvSEgQIECBAgAABAgQIECBAgAABAgQIFJ5A88Lrsh4TIECAAAECBAgQqJ8C/YaMiKO/d0W0ad852nZYNWN92dLFMeujD+pnh/WKAAECBAgQIECAAAECBAgQIECAAIEKCQisV4hJIQIECBAgQIAAAQLrF2jbsWt06z0wKzh76vi4648/iKKVK7I8CQIECBAgQIAAAQIECBAgQIAAAQIECk9AYL3wnpkeEyBAgAABAgQI1FOBsa89GVd8d99YPH92LF4wu572UrcIECBAgAABAgQIECBAgAABAgQIENhQAYH1DRVTngABAgQIECBAgMA6BIqLi2PGpDHruCqbAAECBAgQIECAAAECBAgQIECAAIFCFWhaqB3XbwIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUBsCAuu1oawNAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEChYAYH1gn10Ok6AAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECtSEgsF4bytogQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgYIVEFgv2Een4wQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQGwIC67WhrA0CBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQKFgBgfWCfXQ6ToAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQK1ISCwXhvK2iBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBghUQWC/YR6fjBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAbAgLrtaGsDQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAoWAGB9YJ9dDpOgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABArUh0Lw2GtEGAQIECBAgQIAAgfokcPCJF0S3PgNj/swpMXvahHj1sdti2dJF9amL+kKAAAECBAgQIECAAAECBAgQIECAQD0SEFivRw9DVwgQIECAAAECBGpHYMudD44uPfpljR30f+fF5afvFvNygXYHAQIECBAgQIAAAQIECBAgQIAAAQIE1hSwFPyaIs4JECBAgAABAgQavMBNF50U//r9d+KVR25Kx9q0abM4+MTzG/y4DZAAAQIECBAgQIAAAQIECBAgQIAAgcoJmLFeOTd3ESBAgAABAgQIFLDA9ImjI/m88czd0XfQNtFn0LDo3n9oAY9I1wkQIECAAAECBAgQIECAAAECBAgQqEkBM9ZrUlfdBAgQIECAAAEC9V5gzvSJaR/bdexa7/uqgwQIECBAgAABAgQIECBAgAABAgQI1I2AwHrduGuVAAECBAgQIECgngisWP5J2pMmTfzTuJ48Et0gQIAAAQIECBAgQIAAAQIECBAgUO8E/PWw3j0SHSJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACB+iQgsF6fnoa+ECBAgAABAgQI1LpAcXFR2mbL1m1rvW0NEiBAgAABAgQIECBAgAABAgQIECBQGAIC64XxnPSSAAECBAgQIECghgQWzJ6a1ty8RavotckWNdSKagkQIECAAAECBAgQIECAAAECBAgQKGQBgfVCfnr6ToAAAQIECBAgUGWBMa8+mdVxyMkXRhJgdxAgQIAAAQIECBAgQIAAAQIECBAgQKCkgMB6SQ1pAgQIECBAgACBRicwcfQr8fCNv4plSxfHpsN2i5/+8/248LYxcdY1LzU6CwMmQIAAAQIECBAgQIAAAQIECBAgQKBsAYH1sl3kEiBAgAABAgQINCKBqRPeiXkzJ2UjbtGyTbTvvFF2LkGAAAECBAgQIECAAAECBAgQIECAQOMWaN64h2/0BAgQIECAAAECjV2gW++BceIFN6cMSxfPj1sv+UZMfPe/sXLF8sZOY/wECBAgQIAAAQIECBAgQIAAAQIECHwqILDup0CAAAECBAgQINCoBYbv/oVs/Pf++UfxwZvPZ+cSBAgQIECAAAECBAgQIECAAAECBAgQSAQsBe93QIAAAQIECBAg0KgFuvbaJB1/UdHKeOflhxq1hcETIECAAAECBAgQIECAAAECBAgQIFC2gMB62S5yCRAgQIAAAQIEGolAkyar/kn88eIFUbRyRSMZtWESIECAAAECBAgQIECAAAECBAgQILAhAgLrG6KlLAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAg0OgGB9Ub3yA2YAAECBAgQIECgpEDbDp3T0+KiopLZ0gQIECBAgAABAgQIECBAgAABAgQIEMgEBNYzCgkCBAgQIECAAIHGJtC8RavYePMd0mHPnTGpsQ3feAkQIECAAAECBAgQIECAAAECBAgQqKBA8wqWU4wAAQIECBAgQIBAgxE45swrY5Mtd4gOXXpG06bN0nG9/dK/G8z4DIQAAQIECBAgQIAAAQIECBAgQIAAgeoVEFivXk+1ESBAgAABAgQIFIBAv6HbRqdufdKerlyxPEY++a947p6rC6DnukiAAAECBAgQIECAAAECBAgQIECAQF0ICKzXhbo2CRAgQIAAAQIE6lTgL+d+IVq2bhfzZ30USWDdQYAAAQIECBAgQIAAAQIECBAgQIAAgfIEBNbL03GNAAECBAgQIECgQQosmjcrN67k4yBAgAABAgQIECBAgAABAgQIECBAgMD6BZquv4gSBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECg8QoIrDfeZ2/kBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFABAYH1CiApQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKNV0BgvfE+eyMnQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgQoICKxXAEkRAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGi8AgLrjffZGzkBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIVEBAYL0CSIoQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQOMVEFhvvM/eyAkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECgAgIC6xVAUoQAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEGq+AwHrjffZGToAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIVEBBYrwCSIgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQeAUE1hvvszdyAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiAgMB6BZAUIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIHGKyCw3nifvZETIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQAUEBNYrgKQIAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECDReAYH1xvvsjZwAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEKiAgsF4BJEUIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoPEKCKw33mdv5AQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQAQGB9QogKUKAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECjVdAYL3xPnsjJ0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEKCAisVwBJEQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBBovAIC64332Rs5AQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRAQGC9AkiKECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDjFRBYb7zP3sgJECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAoAICAusVQFKEAAECBAgQIECgZgWWfbwkbaBNuw4125DaCRAgQIAAAQIECBAgQIAAAQIECBBodAL5vzvm/w5ZGQCB9cqouYcAAQIECBAgQKBaBRbOn5PW16Vbr2qtV2UECBAgQIAAAQIECBAgQIAAAQIECBDI/90x/3fIyogIrFdGzT0ECBAgQIAAAQLVKjB7+uS0vh59BkT+7dFqbUBlBAgQIECAAAECBAgQIECAAAECBAg0SoHk743J3x2TI/93yPRkA/8jsL6BYIoTIECAAAECBAhUv8DSJQtjxkcT0ooHDBkuuF79xGokQIAAAQIECBAgQIAAAQIECBAg0OgEkqB68vfG5Ej+/pj8HbKyR/PK3ug+AgQIECBAgAABAtUp8NGHY6Nl6zbRuWvP2GzYzuk/dOfOnhZLF1f+H7vV2T91ESBAgAABAgQIECBAgAABAgQIECBQGAJJQD1Z/j0/U33enOmR/P2xKofAelX03EuAAAECBAgQIFCtAhPGjIo+Gw9J/8Gb/KM3/w/fam1EZQQIECBAgAABAgQIECBAgAABAgQINBqBZKZ6VYPqCZbAeqP5yRgoAQIECBAgQKAwBJJ/5M6dNS269ewXHTp1jVat2xZGx/WSAAECBAgQIECAAAECBAgQIECAAIF6IbDs4yWxcP6cdE/1qiz/XnIwAuslNaQJECBAgAABAgTqhUDyj93J49+tF33RCQIECBAgQIAAAQIECBAgQIAAAQIECDRFQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECKxbQGB93TauECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBEFj3IyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAuUICKyXg+MSAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQWPcbIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5QgIrJeD4xIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBBY9xsgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLlCAisl4PjEgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEFj3GyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAuUICKyXg+MSAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQWPcbIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5QgIrJeD4xIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBBY9xsgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLlCAisl4PjEgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEFj3GyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAuUICKyXg+MSAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQWPcbIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5QgIrJeD4xIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBBY9xsgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLlCAisl4PjEgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEFj3GyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAuUICKyXg+MSAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQWPcbIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5QgIrJeD4xIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBBY9xsgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLlCAisl4PjEgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEFj3GyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAuUICKyXg+MSAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQWPcbIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5QgIrJeD4xIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBBY9xsgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLlCAisl4PjEgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEFj3GyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAuUICKyXg+MSAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAQWPcbIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC5QgIrJeD4xIBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEBBY9xsgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQLlCAisl4PjEgECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQEFj3GyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAuUICKyXg+MSAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBD4f/J3crtSEGuCAAAAAElFTkSuQmCC)\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.12.3\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/server/README.md",
    "content": "# Server Examples\n\nExpose workflows as HTTP APIs with `llama_agents.server.WorkflowServer`. Run standalone or mount inside an existing FastAPI / Starlette app.\n\n## Examples\n\n| File | What it shows |\n| --- | --- |\n| [`server_example.py`](server_example.py) | The minimal standalone `WorkflowServer`. **Start here.** |\n| [`server_example.ipynb`](server_example.ipynb) | Same example as a walkthrough notebook, with explanations and a client call. |\n| [`server.py`](server.py) | Mount `WorkflowServer` inside an existing FastAPI app alongside your own routes. |\n| [`fastapi_server_example.ipynb`](fastapi_server_example.ipynb) | Notebook version of the FastAPI integration pattern. |\n\n## Running\n\n```bash\nuv run examples/server/server_example.py\n# then in another terminal\ncurl -X POST http://localhost:8000/workflows/echo/run -d '{\"start_event\": {\"message\": \"hi\"}}'\n```\n\nSee also the [`client/`](../client/) examples for calling a running server from Python.\n"
  },
  {
    "path": "examples/server/fastapi_server_example.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# FastAPI Example\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"H_jVQJE-_mCO\"\n   },\n   \"source\": [\n    \"Example demonstrating how to integrate WorkflowServer into an existing FastAPI application.\\n\",\n    \"\\n\",\n    \"This example shows how to:\\n\",\n    \"1. Create a FastAPI application with existing routes\\n\",\n    \"2. Set up WorkflowServer with workflows\\n\",\n    \"3. Mount the workflow server as a sub-application\\n\",\n    \"4. Run the combined application\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true,\n    \"id\": \"jnPCUjPk_Kfq\"\n   },\n   \"outputs\": [],\n   \"source\": \"%pip install llama-agents-server fastapi\"\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"V5j--gYldhud\"\n   },\n   \"source\": [\n    \"## Run the server in background\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"L5AsbMnk_xuG\",\n    \"outputId\": \"35ba5e76-3e81-43c2-d2cb-504378ed9d41\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     Started server process [26386]\\n\",\n      \"INFO:     Waiting for application startup.\\n\",\n      \"INFO:     Application startup complete.\\n\",\n      \"INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Server started with PID: 26386\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"import subprocess\\n\",\n    \"import time\\n\",\n    \"\\n\",\n    \"# Start server in background using subprocess\\n\",\n    \"server_process = subprocess.Popen([\\\"python\\\", \\\"server.py\\\"])\\n\",\n    \"time.sleep(2)  # Wait for server to start\\n\",\n    \"print(f\\\"Server started with PID: {server_process.pid}\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"bPbCXF_udl4K\"\n   },\n   \"source\": [\n    \"## Interact with the server\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"H_NVM5ovmyk_\",\n    \"outputId\": \"17738424-95fd-4869-8b0c-19419160ad59\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65035 - \\\"GET /users/123 HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"user_id\\\":123,\\\"name\\\":\\\"User 123\\\",\\\"email\\\":\\\"user123@example.com\\\"}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Confirm existing routes are there\\n\",\n    \"!curl -s http://localhost:8000/users/123\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"Fm1Z0YGmgmRH\",\n    \"outputId\": \"7150949c-769a-4f6d-d5a8-c2cbb74c599e\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65039 - \\\"GET /wf-server/health HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"status\\\":\\\"healthy\\\",\\\"loaded_workflows\\\":0,\\\"active_workflows\\\":0,\\\"idle_workflows\\\":0}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Hit the health endpoint to see the workflow server is available at the path /wf-server\\n\",\n    \"!curl http://localhost:8000/wf-server/health\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 11,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"TD6tz50jeAkf\",\n    \"outputId\": \"8c2bac46-dc74-4dbd-fe80-2fb4eaac47e5\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65043 - \\\"GET /wf-server/workflows HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"workflows\\\":[\\\"user_processing\\\",\\\"notification\\\"]}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# List available workflows\\n\",\n    \"!curl http://localhost:8000/wf-server/workflows\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 12,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"wJD4rifveJMt\",\n    \"outputId\": \"c2b124f9-234d-4e10-b257-164e6199c282\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65050 - \\\"POST /wf-server/workflows/user_processing/run HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"handler_id\\\":\\\"Mu2qZvQzVu\\\",\\\"workflow_name\\\":\\\"user_processing\\\",\\\"run_id\\\":\\\"7hMHiP24AL\\\",\\\"error\\\":null,\\\"result\\\":{\\\"value\\\":{\\\"result\\\":{\\\"processed_name\\\":\\\"ALICE\\\",\\\"domain\\\":\\\"company.com\\\",\\\"status\\\":\\\"processed\\\"}},\\\"qualified_name\\\":\\\"workflows.events.StopEvent\\\",\\\"type\\\":\\\"StopEvent\\\",\\\"types\\\":null},\\\"status\\\":\\\"completed\\\",\\\"started_at\\\":\\\"2026-01-29T22:34:33.157083+00:00\\\",\\\"updated_at\\\":\\\"2026-01-29T22:34:33.157792+00:00\\\",\\\"completed_at\\\":\\\"2026-01-29T22:34:33.157792+00:00\\\"}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Run user processing workflow\\n\",\n    \"!curl -X POST http://localhost:8000/wf-server/workflows/user_processing/run \\\\\\n\",\n    \"  -H \\\"Content-Type: application/json\\\" \\\\\\n\",\n    \"  -d '{\\\"kwargs\\\": {\\\"user_data\\\": {\\\"name\\\": \\\"alice\\\", \\\"email\\\": \\\"alice@company.com\\\"}}}'\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.9.18\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/server/server.py",
    "content": "import uvicorn\nfrom fastapi import FastAPI\nfrom llama_agents.server import WorkflowServer\nfrom pydantic import BaseModel\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\n\nclass UserModel(BaseModel):\n    name: str\n    email: str\n\n\n# Existing FastAPI application with some routes\napp = FastAPI(title=\"My API with Workflows\", version=\"1.0.0\")\n\n\n# Existing API routes\n@app.get(\"/\")\nasync def root() -> dict:\n    return {\"message\": \"Welcome to My API\"}\n\n\n@app.get(\"/users/{user_id}\")\nasync def get_user(user_id: int) -> dict:\n    return {\n        \"user_id\": user_id,\n        \"name\": f\"User {user_id}\",\n        \"email\": f\"user{user_id}@example.com\",\n    }\n\n\n@app.post(\"/users\")\nasync def create_user(user: UserModel) -> dict:\n    return {\"message\": f\"Created user {user.name}\", \"user\": user}\n\n\n# Define workflows\nclass UserProcessingWorkflow(Workflow):\n    @step\n    async def process_user(self, ev: StartEvent) -> StopEvent:\n        user_data = getattr(ev, \"user_data\", {})\n        name = user_data.get(\"name\", \"Unknown\")\n        email = user_data.get(\"email\", \"unknown@example.com\")\n\n        # Simulate some processing\n        processed_data = {\n            \"processed_name\": name.upper(),\n            \"domain\": email.split(\"@\")[1] if \"@\" in email else \"unknown\",\n            \"status\": \"processed\",\n        }\n\n        return StopEvent(result=processed_data)\n\n\nclass NotificationWorkflow(Workflow):\n    @step\n    async def send_notification(self, ev: StartEvent) -> StopEvent:\n        message = getattr(ev, \"message\", \"Default notification\")\n        recipient = getattr(ev, \"recipient\", \"admin@example.com\")\n\n        # Simulate sending notification\n        result = {\n            \"notification_id\": \"notif_123\",\n            \"message\": message,\n            \"recipient\": recipient,\n            \"sent_at\": \"2024-01-01T12:00:00Z\",\n            \"status\": \"sent\",\n        }\n\n        return StopEvent(result=result)\n\n\ndef main() -> None:\n    # Create workflow server\n    workflow_server = WorkflowServer()\n\n    # Register workflows\n    workflow_server.add_workflow(\"user_processing\", UserProcessingWorkflow())\n    workflow_server.add_workflow(\"notification\", NotificationWorkflow())\n\n    # Mount workflow server as sub-application\n    app.mount(\"/wf-server\", workflow_server.app)\n\n    # run the FastAPI server\n    uvicorn.run(app, host=\"0.0.0.0\", port=8000, log_level=\"info\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/server/server_example.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Workflow Server Example\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"H_jVQJE-_mCO\"\n   },\n   \"source\": [\n    \"Example demonstrating how to use the WorkflowServer with event streaming.\\n\",\n    \"\\n\",\n    \"This example shows how to:\\n\",\n    \"1. Create workflows that emit streaming events\\n\",\n    \"2. Set up the server with event streaming support\\n\",\n    \"3. Register workflows\\n\",\n    \"4. Run the server\\n\",\n    \"5. Make HTTP requests to execute workflows\\n\",\n    \"6. Stream real-time events from running workflows using the /events endpoint\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true,\n    \"id\": \"jnPCUjPk_Kfq\"\n   },\n   \"outputs\": [],\n   \"source\": \"%pip install llama-agents-server\"\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"V5j--gYldhud\"\n   },\n   \"source\": [\n    \"## Run the server in background\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"L5AsbMnk_xuG\",\n    \"outputId\": \"0cb5ec84-575e-4484-90ab-94ff73e241d8\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     Started server process [27939]\\n\",\n      \"INFO:     Waiting for application startup.\\n\",\n      \"INFO:     Application startup complete.\\n\",\n      \"INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Server started with PID: 27939\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"import subprocess\\n\",\n    \"import time\\n\",\n    \"\\n\",\n    \"# Start server in background using subprocess\\n\",\n    \"server_process = subprocess.Popen([\\\"python\\\", \\\"server.py\\\"])\\n\",\n    \"time.sleep(2)  # Wait for server to start\\n\",\n    \"print(f\\\"Server started with PID: {server_process.pid}\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"bPbCXF_udl4K\"\n   },\n   \"source\": [\n    \"## Interact with the server\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"Fm1Z0YGmgmRH\",\n    \"outputId\": \"f4c87973-1034-4fe3-c5f4-82d8c0d78db2\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65182 - \\\"GET /health HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"status\\\":\\\"healthy\\\",\\\"loaded_workflows\\\":0,\\\"active_workflows\\\":0,\\\"idle_workflows\\\":0}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Hit the health endpoint to see the server is up and running\\n\",\n    \"!curl http://localhost:8000/health\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"TD6tz50jeAkf\",\n    \"outputId\": \"2a622940-7e0b-445a-9292-e89420026ef6\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65184 - \\\"GET /workflows HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"workflows\\\":[\\\"greeting\\\",\\\"math\\\",\\\"processing\\\"]}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# List available workflows\\n\",\n    \"!curl http://localhost:8000/workflows\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"wJD4rifveJMt\",\n    \"outputId\": \"ab382fc4-8e94-4779-826f-0db4c4986f69\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65190 - \\\"POST /workflows/greeting/run HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"handler_id\\\":\\\"3podAAguhA\\\",\\\"workflow_name\\\":\\\"greeting\\\",\\\"run_id\\\":\\\"KD7Y2S0e0h\\\",\\\"error\\\":null,\\\"result\\\":{\\\"value\\\":{\\\"result\\\":\\\"Hello, Alice!\\\"},\\\"qualified_name\\\":\\\"workflows.events.StopEvent\\\",\\\"type\\\":\\\"StopEvent\\\",\\\"types\\\":null},\\\"status\\\":\\\"completed\\\",\\\"started_at\\\":\\\"2026-01-29T22:36:48.596733+00:00\\\",\\\"updated_at\\\":\\\"2026-01-29T22:36:49.500898+00:00\\\",\\\"completed_at\\\":\\\"2026-01-29T22:36:49.500898+00:00\\\"}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Run greeting workflow\\n\",\n    \"!curl -X POST http://localhost:8000/workflows/greeting/run \\\\\\n\",\n    \"  -H \\\"Content-Type: application/json\\\" \\\\\\n\",\n    \"  -d '{\\\"kwargs\\\": {\\\"name\\\": \\\"Alice\\\"}}'\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"UkO28ZgxeOyT\",\n    \"outputId\": \"b0543b29-e102-4ba9-a44c-4115c89c0e44\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65196 - \\\"POST /workflows/math/run HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"handler_id\\\":\\\"kITlArjbWm\\\",\\\"workflow_name\\\":\\\"math\\\",\\\"run_id\\\":\\\"t3jXb1nGtV\\\",\\\"error\\\":null,\\\"result\\\":{\\\"value\\\":{\\\"result\\\":{\\\"a\\\":10,\\\"b\\\":5,\\\"operation\\\":\\\"multiply\\\",\\\"result\\\":50}},\\\"qualified_name\\\":\\\"workflows.events.StopEvent\\\",\\\"type\\\":\\\"StopEvent\\\",\\\"types\\\":null},\\\"status\\\":\\\"completed\\\",\\\"started_at\\\":\\\"2026-01-29T22:36:56.341122+00:00\\\",\\\"updated_at\\\":\\\"2026-01-29T22:36:56.341812+00:00\\\",\\\"completed_at\\\":\\\"2026-01-29T22:36:56.341812+00:00\\\"}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Run math workflow\\n\",\n    \"!curl -X POST http://localhost:8000/workflows/math/run \\\\\\n\",\n    \"  -H \\\"Content-Type: application/json\\\" \\\\\\n\",\n    \"  -d '{\\\"kwargs\\\": {\\\"a\\\": 10, \\\"b\\\": 5, \\\"operation\\\": \\\"multiply\\\"}}'\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 36,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"-PQ6vt56eTwp\",\n    \"outputId\": \"7c7f0007-ecd5-4c68-8275-0cbecc15c4d8\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Got handler id: vuyyRzWikr\\n\",\n      \"{\\\"result\\\":{\\\"a\\\":100,\\\"b\\\":25,\\\"operation\\\":\\\"divide\\\",\\\"result\\\":4.0}}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"%%bash\\n\",\n    \"# Run workflow with nowait\\n\",\n    \"handler_id=$(curl -sX POST http://localhost:8000/workflows/math/run-nowait \\\\\\n\",\n    \"  -H \\\"Content-Type: application/json\\\" \\\\\\n\",\n    \"  -d '{\\\"kwargs\\\": {\\\"a\\\": 100, \\\"b\\\": 25, \\\"operation\\\": \\\"divide\\\"}}' | jq -r \\\".handler_id\\\" )\\n\",\n    \"printf \\\"Got handler id: ${handler_id}\\\\n\\\\n\\\"\\n\",\n    \"\\n\",\n    \"# Wait for the workflow to run in background\\n\",\n    \"sleep 1\\n\",\n    \"\\n\",\n    \"# Fetch the result asynchronously\\n\",\n    \"curl -s http://localhost:8000/results/${handler_id}\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"iR4-fBOujIXd\",\n    \"outputId\": \"b35d418f-bbd6-47b9-b0de-deac1ac26bd1\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"INFO:     127.0.0.1:65202 - \\\"POST /workflows/greeting/run-nowait HTTP/1.1\\\" 200 OK\\n\",\n      \"Got handler id: gbqSi1fzGJ\\n\",\n      \"\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Streaming events...\\n\",\n      \"INFO:     127.0.0.1:65204 - \\\"GET /events/gbqSi1fzGJ?sse=true HTTP/1.1\\\" 200 OK\\n\",\n      \"data: {\\\"value\\\":{\\\"sequence\\\":0},\\\"qualified_name\\\":\\\"__main__.StreamEvent\\\",\\\"type\\\":\\\"StreamEvent\\\",\\\"types\\\":null}\\n\",\n      \"\\n\",\n      \"data: {\\\"value\\\":{\\\"sequence\\\":1},\\\"qualified_name\\\":\\\"__main__.StreamEvent\\\",\\\"type\\\":\\\"StreamEvent\\\",\\\"types\\\":null}\\n\",\n      \"\\n\",\n      \"data: {\\\"value\\\":{\\\"sequence\\\":2},\\\"qualified_name\\\":\\\"__main__.StreamEvent\\\",\\\"type\\\":\\\"StreamEvent\\\",\\\"types\\\":null}\\n\",\n      \"\\n\",\n      \"data: {\\\"value\\\":{\\\"result\\\":\\\"Hello, Async User!\\\"},\\\"qualified_name\\\":\\\"workflows.events.StopEvent\\\",\\\"type\\\":\\\"StopEvent\\\",\\\"types\\\":null}\\n\",\n      \"\\n\",\n      \"\\n\",\n      \"Final result:\\n\",\n      \"INFO:     127.0.0.1:65206 - \\\"GET /results/gbqSi1fzGJ HTTP/1.1\\\" 200 OK\\n\",\n      \"{\\\"handler_id\\\":\\\"gbqSi1fzGJ\\\",\\\"workflow_name\\\":\\\"greeting\\\",\\\"run_id\\\":\\\"M5AtcoxBty\\\",\\\"error\\\":null,\\\"result\\\":\\\"Hello, Async User!\\\",\\\"status\\\":\\\"completed\\\",\\\"started_at\\\":\\\"2026-01-29T22:37:02.375925+00:00\\\",\\\"updated_at\\\":\\\"2026-01-29T22:37:03.279696+00:00\\\",\\\"completed_at\\\":\\\"2026-01-29T22:37:03.279696+00:00\\\"}\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"%%bash\\n\",\n    \"# Stream events from workflow\\n\",\n    \"\\n\",\n    \"# 1. Run workflow with nowait\\n\",\n    \"handler_id=$(curl -sX POST http://localhost:8000/workflows/greeting/run-nowait \\\\\\n\",\n    \"  -H \\\"Content-Type: application/json\\\" \\\\\\n\",\n    \"  -d '{\\\"kwargs\\\": {\\\"name\\\": \\\"Async User\\\"}}' | jq -r \\\".handler_id\\\" )\\n\",\n    \"printf \\\"Got handler id: ${handler_id}\\\\n\\\\n\\\"\\n\",\n    \"\\n\",\n    \"# Wait for the workflow to run in background\\n\",\n    \"sleep 1\\n\",\n    \"\\n\",\n    \"printf \\\"Streaming events...\\\\n\\\"\\n\",\n    \"# 2. Stream events using Server-Sent Events using SSE format\\n\",\n    \"curl -s http://localhost:8000/events/$handler_id?sse=true\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"printf \\\"\\\\nFinal result:\\\\n\\\"\\n\",\n    \"# 3. Get the final result after events complete\\n\",\n    \"curl -s http://localhost:8000/results/$handler_id\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.9.18\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/server/server_example.py",
    "content": "import asyncio\n\nfrom llama_agents.server import WorkflowServer\nfrom workflows import Workflow, step\nfrom workflows.context import Context\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\n\n\nclass StreamEvent(Event):\n    sequence: int\n\n\nclass GreetingInput(StartEvent):\n    greeting: str\n    name: str\n    exclamation_marks: int\n    formal: bool\n\n\nclass GreetingOutput(StopEvent):\n    full_greeting: str\n    is_formal: bool\n    length: int\n\n\n# Define a simple workflow\nclass GreetingWorkflow(Workflow):\n    @step\n    async def greet(self, ctx: Context, ev: GreetingInput) -> GreetingOutput:\n        for i in range(3):\n            ctx.write_event_to_stream(StreamEvent(sequence=i))\n            await asyncio.sleep(0.3)\n\n        name = ev.name\n        greeting = ev.greeting\n        excl_marks = ev.exclamation_marks\n        formal = ev.formal\n        if formal:\n            greeting = \"Good Morning Your Honor\"\n        name = name + \"!\" * excl_marks\n        return GreetingOutput(\n            full_greeting=f\"{greeting}, {name}\",\n            length=len(f\"{greeting}, {name}\"),\n            is_formal=formal if isinstance(formal, bool) else False,\n        )\n\n\nclass ProgressEvent(Event):\n    step: str\n    progress: int\n    message: str\n\n\nclass MathWorkflow(Workflow):\n    @step\n    async def calculate(self, ev: StartEvent) -> StopEvent:\n        a = getattr(ev, \"a\", 0)\n        b = getattr(ev, \"b\", 0)\n        operation = getattr(ev, \"operation\", \"add\")\n\n        if operation == \"add\":\n            result = a + b\n        elif operation == \"multiply\":\n            result = a * b\n        elif operation == \"subtract\":\n            result = a - b\n        elif operation == \"divide\":\n            result = a / b if b != 0 else None\n        else:\n            result = None\n\n        return StopEvent(\n            result={\"a\": a, \"b\": b, \"operation\": operation, \"result\": result}\n        )\n\n\nclass ProcessingWorkflow(Workflow):\n    \"\"\"Example workflow that demonstrates event streaming with progress updates.\"\"\"\n\n    @step\n    async def process(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        items = getattr(ev, \"things\", [\"item1\", \"item2\", \"item3\", \"item4\", \"item5\"])\n\n        ctx.write_event_to_stream(\n            ProgressEvent(\n                step=\"start\",\n                progress=0,\n                message=f\"Starting processing of {len(items)} items\",\n            )\n        )\n\n        results = []\n        for i, item in enumerate(items):\n            # Simulate processing time\n            await asyncio.sleep(0.5)\n\n            # Emit progress event\n            progress = int((i + 1) / len(items) * 100)\n            ctx.write_event_to_stream(\n                ProgressEvent(\n                    step=\"processing\",\n                    progress=progress,\n                    message=f\"Processed {item} ({i + 1}/{len(items)})\",\n                )\n            )\n\n            results.append(f\"processed_{item}\")\n\n        ctx.write_event_to_stream(\n            ProgressEvent(\n                step=\"complete\",\n                progress=100,\n                message=\"Processing completed successfully\",\n            )\n        )\n\n        return StopEvent(result={\"processed_items\": results, \"total\": len(results)})\n\n\nclass ComplicatedInput(StartEvent):\n    age: int\n    name: str\n    terrestrian: bool\n    language: str\n\n\nclass ChildTerrestrialEvent(Event):\n    greeting: str\n\n\nclass AdultTerrestrialEvent(Event):\n    greeting: str\n\n\nclass ExtraTerrestrialEvent(Event):\n    language: str\n    greeting: str\n\n\nclass ComplicatedWorkflow(Workflow):\n    @step\n    async def first_step(\n        self,\n        ev: ComplicatedInput,\n        ctx: Context,\n    ) -> ChildTerrestrialEvent | AdultTerrestrialEvent | ExtraTerrestrialEvent:\n        ctx.write_event_to_stream(ev)\n        await asyncio.sleep(1)\n        if ev.age < 18 and ev.terrestrian:\n            ctx.write_event_to_stream(\n                ChildTerrestrialEvent(\n                    greeting=f\"Hello, terrestrial child named {ev.name}\"\n                )\n            )\n            return ChildTerrestrialEvent(\n                greeting=f\"Hello, terrestrial child named {ev.name}\"\n            )\n        elif ev.age >= 18 and ev.terrestrian:\n            ctx.write_event_to_stream(\n                AdultTerrestrialEvent(\n                    greeting=f\"My regards, terrestrial adult named {ev.name}\"\n                )\n            )\n            return AdultTerrestrialEvent(\n                greeting=f\"My regards, terrestrial adult named {ev.name}\"\n            )\n        else:\n            if ev.language.lower() == \"martian\":\n                ctx.write_event_to_stream(\n                    ExtraTerrestrialEvent(greeting=\"Ifmmp uifsf!\", language=\"martian\")\n                )\n                return ExtraTerrestrialEvent(\n                    greeting=\"Ifmmp uifsf!\", language=\"martian\"\n                )\n            elif ev.language.lower() == \"venusian\":\n                ctx.write_event_to_stream(\n                    ExtraTerrestrialEvent(greeting=\"!ereht olleH\", language=\"venusian\")\n                )\n                return ExtraTerrestrialEvent(\n                    greeting=\"!ereht olleH\", language=\"venusian\"\n                )\n            else:\n                ctx.write_event_to_stream(\n                    ExtraTerrestrialEvent(\n                        greeting=\"Sorry, I do not speak your language\",\n                        language=ev.language,\n                    )\n                )\n                return ExtraTerrestrialEvent(\n                    greeting=\"Sorry, I do not speak your language\", language=ev.language\n                )\n\n    @step\n    async def terrestrial_child_step(self, ev: ChildTerrestrialEvent) -> StopEvent:\n        await asyncio.sleep(1)\n        return StopEvent(result=\"Hello back, old person\")\n\n    @step\n    async def terrestrial_adult_step(self, ev: AdultTerrestrialEvent) -> StopEvent:\n        await asyncio.sleep(1)\n        return StopEvent(result=\"Hello back, young fella\")\n\n    @step\n    async def extraterrestrial_step(self, ev: ExtraTerrestrialEvent) -> StopEvent:\n        await asyncio.sleep(1)\n        if ev.language == \"martian\":\n            return StopEvent(result=\"Ifmmp cbdl!\")\n        elif ev.language == \"venusian\":\n            return StopEvent(result=\"kcab olleH\")\n        else:\n            return StopEvent(result=\"Xyubpifhabpmfsh!\")\n\n\nclass RequestEvent(InputRequiredEvent):\n    prompt: str\n\n\nclass ResponseEvent(HumanResponseEvent):\n    response: str\n\n\nclass RunEvent(StartEvent):\n    input_str: str\n\n\nclass HumanInTheLoopWorkflow(Workflow):\n    @step\n    async def prompt_human(self, ev: RunEvent) -> RequestEvent:\n        await asyncio.sleep(1)\n        return RequestEvent(\n            prompt=f\"Wow, pretty crazy you just said '{ev.input_str}'. What is your name anyways?\"\n        )\n\n    @step\n    async def greet_human(self, ctx: Context, ev: ResponseEvent) -> StopEvent:\n        follow_up = f\"Nice to meet you, {ev.response}!\"\n        return StopEvent(result=follow_up)\n\n\nasync def main() -> None:\n    server = WorkflowServer()\n\n    # Register workflows\n    server.add_workflow(\n        \"greeting\",\n        GreetingWorkflow(),\n    )\n    server.add_workflow(\n        \"processing\",\n        ProcessingWorkflow(),\n    )\n    server.add_workflow(\n        \"math\",\n        MathWorkflow(),\n    )\n    server.add_workflow(\n        \"complicated\",\n        ComplicatedWorkflow(),\n    )\n    server.add_workflow(\n        \"human_in_the_loop\",\n        HumanInTheLoopWorkflow(),\n    )\n\n    await server.serve(host=\"0.0.0.0\", port=8000)\n\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except KeyboardInterrupt:\n        pass\n"
  },
  {
    "path": "examples/state_management_with_vector_databases.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"JtU4X05zuYFU\"\n   },\n   \"source\": [\n    \"# Vector Databases For Workflow State Management\\n\",\n    \"\\n\",\n    \"Workflows are notoriously event-driven code solutions, but it is more and more crucial, for production-grade applications, that they also have write and read access to a global state where they can store and fetch data that are relevant to their run.\\n\",\n    \"\\n\",\n    \"[llama-index-workflows](https://github.com/run-llama/llama-agents) offer a perfect solution for customized, asynchronous and lockable [state management](https://docs.llamaindex.ai/en/latest/module_guides/workflow/#adding-typed-state), but the state lacks persistency across different sessions - and this is exactly where databases, and especially vector databases, enter the game.\\n\",\n    \"\\n\",\n    \"With a database, you can take snapshots of the workflow state at the end of a run and store them into it, to retrieve them in later runs and inform the behavior of the workflow itself.\\n\",\n    \"\\n\",\n    \"This is key for resource management, but also to create a first proof-of-concept of self-learning workflows.\\n\",\n    \"\\n\",\n    \"In this examples, we will see how we can combine [Qdrant](https://qdrant.tech) vector database services with [OpenAI](https://openai.com) LLM capabilties (leveraging remote MCP support for searching DeepWiki and structured generation for parsing the ouputs).\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"VHPUdFb8wZZt\"\n   },\n   \"source\": [\n    \"### 1. Install needed dependencies\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"JPrURgHgngB7\",\n    \"outputId\": \"0420e854-001b-4a58-cb41-4f9fdc3cc531\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install -q llama-index-workflows qdrant-client sentence-transformers openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"JV1KuhKOwl6r\"\n   },\n   \"source\": [\n    \"### 2. Define a Raw Vector Database Client\\n\",\n    \"\\n\",\n    \"To fulfil our state-management requirements, we need to write a raw vector database client that is able to create a database collection, upload data points (not only text, but also metadata - since our state can easily be transformed into a dictionary) and retrieve those data points with similarity search.\\n\",\n    \"\\n\",\n    \"We will use dense vector search to keep things plain and simple, and we will employ [all-MiniLM-L6-v2](sentence-transformers/all-MiniLM-L6-v2) as an embedding model, but you can easily change these settings to add layers of complexity (hybrid search, a better embedding model...) to the vector database client.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"R1pmhM4SoDfT\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import json\\n\",\n    \"from typing import Any\\n\",\n    \"\\n\",\n    \"from qdrant_client import AsyncQdrantClient, models\\n\",\n    \"from sentence_transformers import SentenceTransformer\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class QdrantVectorDatabase:\\n\",\n    \"    def __init__(\\n\",\n    \"        self,\\n\",\n    \"        client: AsyncQdrantClient,\\n\",\n    \"        model: SentenceTransformer,\\n\",\n    \"        collection_name: str,\\n\",\n    \"    ):\\n\",\n    \"        self._has_vectors = False\\n\",\n    \"        self.client = client\\n\",\n    \"        self.model = model\\n\",\n    \"        self.collection_name = collection_name\\n\",\n    \"\\n\",\n    \"    async def create_collection(self):\\n\",\n    \"        await self.client.create_collection(\\n\",\n    \"            collection_name=self.collection_name,\\n\",\n    \"            vectors_config=models.VectorParams(\\n\",\n    \"                size=self.model.get_sentence_embedding_dimension() or 384,\\n\",\n    \"                distance=models.Distance.COSINE,\\n\",\n    \"            ),\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    async def upload(\\n\",\n    \"        self, texts: list[str], metadatas: list[dict[str, Any]], ids: list[str]\\n\",\n    \"    ) -> None:\\n\",\n    \"        self._has_vectors = True\\n\",\n    \"        embeddings = self.model.encode(texts).tolist()\\n\",\n    \"        for i, embedding in enumerate(embeddings):\\n\",\n    \"            await self.client.upsert(\\n\",\n    \"                collection_name=self.collection_name,\\n\",\n    \"                points=[\\n\",\n    \"                    models.PointStruct(\\n\",\n    \"                        id=ids[i],\\n\",\n    \"                        vector=embedding,\\n\",\n    \"                        payload=metadatas[i],\\n\",\n    \"                    )\\n\",\n    \"                ],\\n\",\n    \"            )\\n\",\n    \"\\n\",\n    \"    async def search(self, query: str, limit: int, threshold: float = 0.75) -> str:\\n\",\n    \"        if not self._has_vectors:\\n\",\n    \"            return \\\"\\\"\\n\",\n    \"        embedding = self.model.encode(query).tolist()\\n\",\n    \"        results = await self.client.search(\\n\",\n    \"            self.collection_name, query_vector=embedding, limit=limit\\n\",\n    \"        )\\n\",\n    \"        payloads = [hit.payload for hit in results if hit.score > threshold]\\n\",\n    \"        return json.dumps(payloads, indent=4)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"wXHesf5lxc-K\"\n   },\n   \"source\": [\n    \"Let's now create the collection and verify that the collection creation was successful.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"8Q5XrYx-sJl9\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"qdrant_client = AsyncQdrantClient(\\\":memory:\\\")\\n\",\n    \"model = SentenceTransformer(\\\"all-MiniLM-L6-v2\\\")\\n\",\n    \"collection_name = \\\"workflow_collection\\\"\\n\",\n    \"vdb = QdrantVectorDatabase(qdrant_client, model, collection_name)\\n\",\n    \"await vdb.create_collection()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"VCDpqYBhsjx1\",\n    \"outputId\": \"bc71bcd8-9be1-48cc-f337-4acfe8db5481\"\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"True\"\n      ]\n     },\n     \"execution_count\": 4,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"await qdrant_client.collection_exists(\\\"workflow_collection\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"608CBS7Vxpyf\"\n   },\n   \"source\": [\n    \"### 3. Design the LLM Client\\n\",\n    \"\\n\",\n    \"Since we also need customized functions for the LLM client (like remote MCP DeepWiki search and structured output generation), we will also design an LLM client using `AsyncOpenAI` as a starting point.\\n\",\n    \"\\n\",\n    \"We will also add a method with which our LLM client can evaluate the relevance of the retrieved context for our workflow runs.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"CAnqjLgsstHZ\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from dataclasses import dataclass\\n\",\n    \"\\n\",\n    \"from openai import AsyncOpenAI\\n\",\n    \"from pydantic import BaseModel, Field\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ContextRelevance(BaseModel):\\n\",\n    \"    relevance: int = Field(\\n\",\n    \"        description=\\\"Relevance of the context based on the user message, expressed as a number between 1 and 100\\\",\\n\",\n    \"        ge=1,\\n\",\n    \"        le=100,\\n\",\n    \"    )\\n\",\n    \"    reasons: str = Field(description=\\\"Reasons for the evaluation\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class DeepWikiOutput(BaseModel):\\n\",\n    \"    summary: str = Field(description=\\\"Summary of the research output\\\")\\n\",\n    \"    focal_points: list[str] = Field(description=\\\"Focal points of the research output\\\")\\n\",\n    \"    references: list[str] = Field(\\n\",\n    \"        description=\\\"References contained in the reseaerch output\\\", default_factory=list\\n\",\n    \"    )\\n\",\n    \"    similar_topics: list[str] = Field(\\n\",\n    \"        description=\\\"Topics similar to the one of the research output\\\"\\n\",\n    \"    )\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"@dataclass\\n\",\n    \"class OpenAILlm:\\n\",\n    \"    llm: AsyncOpenAI\\n\",\n    \"\\n\",\n    \"    async def deep_wiki(self, message: str, context: str | None = None) -> str:\\n\",\n    \"        if not context:\\n\",\n    \"            response = await self.llm.responses.create(\\n\",\n    \"                model=\\\"gpt-4.1\\\",\\n\",\n    \"                tools=[\\n\",\n    \"                    {\\n\",\n    \"                        \\\"type\\\": \\\"mcp\\\",\\n\",\n    \"                        \\\"server_label\\\": \\\"deepwiki\\\",\\n\",\n    \"                        \\\"server_url\\\": \\\"https://mcp.deepwiki.com/mcp\\\",\\n\",\n    \"                        \\\"require_approval\\\": \\\"never\\\",\\n\",\n    \"                    },\\n\",\n    \"                ],\\n\",\n    \"                input=message,\\n\",\n    \"            )\\n\",\n    \"        else:\\n\",\n    \"            response = await self.llm.responses.create(\\n\",\n    \"                model=\\\"gpt-4.1\\\",\\n\",\n    \"                tools=[\\n\",\n    \"                    {\\n\",\n    \"                        \\\"type\\\": \\\"mcp\\\",\\n\",\n    \"                        \\\"server_label\\\": \\\"deepwiki\\\",\\n\",\n    \"                        \\\"server_url\\\": \\\"https://mcp.deepwiki.com/mcp\\\",\\n\",\n    \"                        \\\"require_approval\\\": \\\"never\\\",\\n\",\n    \"                    },\\n\",\n    \"                ],\\n\",\n    \"                input=\\\"<context>\\\\n\\\\t\\\"\\n\",\n    \"                + context\\n\",\n    \"                + \\\"\\\\n</context>\\\\n<user_message>\\\\n\\\\t\\\"\\n\",\n    \"                + message\\n\",\n    \"                + \\\"\\\\n</user_message>\\\\n<instructions>\\\\n\\\\tReply to the user message based on the contextual information\\\\n</instructions>\\\",\\n\",\n    \"            )\\n\",\n    \"        return await self.format_deep_wiki_output(response.output_text)\\n\",\n    \"\\n\",\n    \"    async def classify_context_relevance(self, message: str, context: str) -> str:\\n\",\n    \"        response = await self.llm.responses.parse(\\n\",\n    \"            model=\\\"gpt-4.1\\\",\\n\",\n    \"            input=\\\"<context>\\\\n\\\\t\\\"\\n\",\n    \"            + context\\n\",\n    \"            + \\\"\\\\n</context>\\\\n<user_message>\\\\n\\\\t\\\"\\n\",\n    \"            + message\\n\",\n    \"            + \\\"\\\\n</user_message>\\\\n<instructions>\\\\n\\\\tEvaluate the relevance of the context in relation to the user message from 1 to 100, and give reasons for this evaluation\\\\n</instructions>\\\",\\n\",\n    \"            text_format=ContextRelevance,\\n\",\n    \"        )\\n\",\n    \"        return response.output_parsed\\n\",\n    \"\\n\",\n    \"    async def format_deep_wiki_output(self, output: str):\\n\",\n    \"        response = await self.llm.responses.parse(\\n\",\n    \"            model=\\\"gpt-4.1\\\",\\n\",\n    \"            input=\\\"<research_output>\\\\n\\\\t\\\"\\n\",\n    \"            + output\\n\",\n    \"            + \\\"\\\\n</research_output>\\\\n<instructions>\\\\n\\\\tFormat the output so that you highlight a summary, the focal points, the references contained into it and further topics to explore similar to the one in the ouput\\\\n</instructions>\\\",\\n\",\n    \"            text_format=DeepWikiOutput,\\n\",\n    \"        )\\n\",\n    \"        return response.output_parsed\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"cgKTPQLxyNv8\"\n   },\n   \"source\": [\n    \"### 4. Create Resources\\n\",\n    \"\\n\",\n    \"[Resources](https://docs.llamaindex.ai/en/latest/module_guides/workflow/#resources) are a way, in workflows, to perform dependency injection: you define functions to get external serices and make them available, step-wise, within the workflow itself.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"icrEAytxxX2v\",\n    \"outputId\": \"cb4e2a79-8ea5-416b-d655-c761b5149a60\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Enter your OpenAI API key: ··········\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from getpass import getpass\\n\",\n    \"\\n\",\n    \"llm = AsyncOpenAI(api_key=getpass(\\\"Enter your OpenAI API key: \\\"))\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"iI5vi2E8zD4r\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"openai_llm = OpenAILlm(llm)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_llm(**kwargs):\\n\",\n    \"    return openai_llm\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_vdb(**kwargs):\\n\",\n    \"    return vdb\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Wg1SispXy9CP\"\n   },\n   \"source\": [\n    \"### 5. Define the Workflow\\n\",\n    \"\\n\",\n    \"We now need define the workflow: not only the steps themselves, but also the events that will drive the execution.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"1lJTzuvhyF_I\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from workflows.events import Event, StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ResearchQuestionEvent(StartEvent):\\n\",\n    \"    question: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ResearchEvent(StopEvent, DeepWikiOutput):\\n\",\n    \"    pass\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class RetrieveContextEvent(Event):\\n\",\n    \"    context: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ContextRelevanceEvent(Event, ContextRelevance):\\n\",\n    \"    context: str | None\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"VCLsfGrhyssh\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import uuid\\n\",\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowState(BaseModel):\\n\",\n    \"    question: str = Field(description=\\\"Question\\\", default_factory=str)\\n\",\n    \"    summary: str = Field(\\n\",\n    \"        description=\\\"Summary of the research output\\\", default_factory=str\\n\",\n    \"    )\\n\",\n    \"    focal_points: list[str] = Field(\\n\",\n    \"        description=\\\"Focal points of the research output\\\", default_factory=list\\n\",\n    \"    )\\n\",\n    \"    references: list[str] = Field(\\n\",\n    \"        description=\\\"References contained in the reseaerch output\\\", default_factory=list\\n\",\n    \"    )\\n\",\n    \"    similar_topics: list[str] = Field(\\n\",\n    \"        description=\\\"Topics similar to the one of the research output\\\",\\n\",\n    \"        default_factory=list,\\n\",\n    \"    )\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class DeepWikiWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def research_question(\\n\",\n    \"        self,\\n\",\n    \"        ev: ResearchQuestionEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        vdb: Annotated[QdrantVectorDatabase, Resource(get_vdb)],\\n\",\n    \"    ) -> ContextRelevanceEvent | RetrieveContextEvent:\\n\",\n    \"        ctx.write_event_to_stream(ev)\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.question = ev.question\\n\",\n    \"\\n\",\n    \"        results = await vdb.search(ev.question, 5)\\n\",\n    \"        if not results:\\n\",\n    \"            ctx.write_event_to_stream(\\n\",\n    \"                ContextRelevanceEvent(\\n\",\n    \"                    relevance=1, reasons=\\\"No context found\\\", context=None\\n\",\n    \"                )\\n\",\n    \"            )\\n\",\n    \"            return ContextRelevanceEvent(\\n\",\n    \"                relevance=1, reasons=\\\"No context found\\\", context=None\\n\",\n    \"            )\\n\",\n    \"        ctx.write_event_to_stream(RetrieveContextEvent(context=results))\\n\",\n    \"        return RetrieveContextEvent(context=results)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def evaluate_context(\\n\",\n    \"        self,\\n\",\n    \"        ev: RetrieveContextEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        llm: Annotated[OpenAILlm, Resource(get_llm)],\\n\",\n    \"    ) -> ContextRelevanceEvent:\\n\",\n    \"        state = await ctx.store.get_state()\\n\",\n    \"        relevance = await llm.classify_context_relevance(state.question, ev.context)\\n\",\n    \"        if relevance.relevance >= 75:\\n\",\n    \"            ctx.write_event_to_stream(\\n\",\n    \"                ContextRelevanceEvent(\\n\",\n    \"                    relevance=relevance.relevance,\\n\",\n    \"                    reasons=relevance.reasons,\\n\",\n    \"                    context=ev.context,\\n\",\n    \"                )\\n\",\n    \"            )\\n\",\n    \"            return ContextRelevanceEvent(\\n\",\n    \"                relevance=relevance.relevance,\\n\",\n    \"                reasons=relevance.reasons,\\n\",\n    \"                context=ev.context,\\n\",\n    \"            )\\n\",\n    \"        ctx.write_event_to_stream(\\n\",\n    \"            ContextRelevanceEvent(\\n\",\n    \"                relevance=relevance.relevance, reasons=relevance.reasons, context=None\\n\",\n    \"            )\\n\",\n    \"        )\\n\",\n    \"        return ContextRelevanceEvent(\\n\",\n    \"            relevance=relevance.relevance, reasons=relevance.reasons, context=None\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def research(\\n\",\n    \"        self,\\n\",\n    \"        ev: ContextRelevanceEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        llm: Annotated[OpenAILlm, Resource(get_llm)],\\n\",\n    \"        vdb: Annotated[QdrantVectorDatabase, Resource(get_vdb)],\\n\",\n    \"    ) -> ResearchEvent:\\n\",\n    \"        static_state = await ctx.store.get_state()\\n\",\n    \"        result = await llm.deep_wiki(static_state.question, ev.context)\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.summary = result.summary\\n\",\n    \"            state.focal_points = result.focal_points\\n\",\n    \"            state.references = result.references\\n\",\n    \"            state.similar_topics = result.similar_topics\\n\",\n    \"\\n\",\n    \"        await vdb.upload([state.question], [state.model_dump()], [str(uuid.uuid4())])\\n\",\n    \"        return ResearchEvent(\\n\",\n    \"            summary=result.summary,\\n\",\n    \"            focal_points=result.focal_points,\\n\",\n    \"            references=result.references,\\n\",\n    \"            similar_topics=result.similar_topics,\\n\",\n    \"        )\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"P85Zc3hLzHN_\"\n   },\n   \"source\": [\n    \"As you can see, the workflow has three key steps:\\n\",\n    \"\\n\",\n    \"1. The first one, triggered by a question from the user, retrieves previous workflow states stored in the vector database. As you can see, the context is here retrieved through similarity search on the research question: this is actually the same way [semantic caches](https://qdrant.tech/blog/hitchhikers-guide/#semantic-caching) work - by storing the questions as embedding and the answers and metadata, and finding similar questions to the one the user already asked. If the context is mot there (maybe the workflow is empty, maybe the context is not similar enough), we go directly to step (3), else we perform context evaluation in step (2)\\n\",\n    \"2. The retrieved context is evaluated for its relevance by the LLM using structured output: the LLM is constrained into producing a score from 1 to 100 and to provide reasons for the scoring\\n\",\n    \"3. The research step is performed: the user message and the context (if there) are passed to the LLM, which, leveraging remote MCP connection to DeepWiki, performs the research on the user's question. When the response is ready, we \\\"take a snapshot of the state\\\" and we upload it, with the associated question, to the vector database.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"BuclAe9J03xN\"\n   },\n   \"source\": [\n    \"### 6. Run the Workflow\\n\",\n    \"\\n\",\n    \"We will now perform two runs of the same workflow, with very similar questions, to see how our state-management system works with the vector database!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"5s5iQQgW3ZAc\",\n    \"outputId\": \"6e1549d5-e397-4e29-b4d2-d8c70915a0ca\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Starting working on the question What transport protocols are supported in the 2025-03-26 version of the Model Context Protocol (MCP) spec?\\n\",\n      \"Context relevance: 1\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"wf = DeepWikiWorkflow(timeout=600)\\n\",\n    \"handler = wf.run(\\n\",\n    \"    start_event=ResearchQuestionEvent(\\n\",\n    \"        question=\\\"What transport protocols are supported in the 2025-03-26 version of the Model Context Protocol (MCP) spec?\\\"\\n\",\n    \"    )\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"async for event in handler.stream_events():\\n\",\n    \"    if isinstance(event, ResearchQuestionEvent):\\n\",\n    \"        print(\\\"Starting working on the question\\\", event.question)\\n\",\n    \"    elif isinstance(event, ContextRelevanceEvent):\\n\",\n    \"        print(f\\\"Context relevance: {event.relevance}\\\")\\n\",\n    \"    elif isinstance(event, RetrieveContextEvent):\\n\",\n    \"        print(f\\\"Context: {event.context}\\\")\\n\",\n    \"\\n\",\n    \"result = await handler\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"y9pce24T56Eu\",\n    \"outputId\": \"515d6771-6cd8-4710-dd95-19c8e30022a8\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Summary: The 2025-03-26 version of the Model Context Protocol (MCP) specification outlines four supported transport protocols—HTTP/HTTPS, WebSocket, gRPC, and MQTT—for various messaging and interoperability needs. Raw TCP has been deprecated due to security and interoperability considerations.\\n\",\n      \"Focal Points:\\n\",\n      \"-  HTTP/HTTPS is the primary MCP messaging transport.\\n\",\n      \"- WebSocket allows for real-time, bidirectional communication.\\n\",\n      \"- gRPC provides an efficient binary protocol with strong typing.\\n\",\n      \"- MQTT, now stable, targets lightweight, IoT, and edge scenarios.\\n\",\n      \"- Raw TCP transport is deprecated and will be removed in future specifications.\\n\",\n      \"References:\\n\",\n      \"-  Section 7, ‘Transport Protocols,’ of the MCP 2025-03-26 spec\\n\",\n      \"Similar Topics:\\n\",\n      \"-  Comparison of transport protocols in machine learning frameworks\\n\",\n      \"- Protocol interoperability and security in distributed systems\\n\",\n      \"- IoT messaging protocols (MQTT, AMQP, CoAP)\\n\",\n      \"- Transition strategies for protocol deprecation in API standards\\n\",\n      \"- Streaming and real-time messaging protocols (WebSocket, Server-Sent Events)\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(\\\"Summary:\\\", result.summary)\\n\",\n    \"print(\\\"Focal Points:\\\\n- \\\", \\\"\\\\n- \\\".join(result.focal_points))\\n\",\n    \"print(\\\"References:\\\\n- \\\", \\\"\\\\n- \\\".join(result.references))\\n\",\n    \"print(\\\"Similar Topics:\\\\n- \\\", \\\"\\\\n- \\\".join(result.similar_topics))\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"7Rss_njN6aWo\",\n    \"outputId\": \"4e99ecf9-53e2-4e95-84e5-3e1509c5988e\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stderr\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"/tmp/ipython-input-2-1548765145.py:42: DeprecationWarning: `search` method is deprecated and will be removed in the future. Use `query_points` instead.\\n\",\n      \"  results = await self.client.search(self.collection_name, query_vector=embedding, limit=limit)\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Starting working on the question Which transport protocols does the Model Context Protocol specification version 2025-03-26 support?\\n\",\n      \"Context: [\\n\",\n      \"    {\\n\",\n      \"        \\\"question\\\": \\\"What transport protocols are supported in the 2025-03-26 version of the Model Context Protocol (MCP) spec?\\\",\\n\",\n      \"        \\\"summary\\\": \\\"The 2025-03-26 version of the Model Context Protocol (MCP) specification outlines four supported transport protocols\\\\u2014HTTP/HTTPS, WebSocket, gRPC, and MQTT\\\\u2014for various messaging and interoperability needs. Raw TCP has been deprecated due to security and interoperability considerations.\\\",\\n\",\n      \"        \\\"focal_points\\\": [\\n\",\n      \"            \\\"HTTP/HTTPS is the primary MCP messaging transport.\\\",\\n\",\n      \"            \\\"WebSocket allows for real-time, bidirectional communication.\\\",\\n\",\n      \"            \\\"gRPC provides an efficient binary protocol with strong typing.\\\",\\n\",\n      \"            \\\"MQTT, now stable, targets lightweight, IoT, and edge scenarios.\\\",\\n\",\n      \"            \\\"Raw TCP transport is deprecated and will be removed in future specifications.\\\"\\n\",\n      \"        ],\\n\",\n      \"        \\\"references\\\": [\\n\",\n      \"            \\\"Section 7, \\\\u2018Transport Protocols,\\\\u2019 of the MCP 2025-03-26 spec\\\"\\n\",\n      \"        ],\\n\",\n      \"        \\\"similar_topics\\\": [\\n\",\n      \"            \\\"Comparison of transport protocols in machine learning frameworks\\\",\\n\",\n      \"            \\\"Protocol interoperability and security in distributed systems\\\",\\n\",\n      \"            \\\"IoT messaging protocols (MQTT, AMQP, CoAP)\\\",\\n\",\n      \"            \\\"Transition strategies for protocol deprecation in API standards\\\",\\n\",\n      \"            \\\"Streaming and real-time messaging protocols (WebSocket, Server-Sent Events)\\\"\\n\",\n      \"        ]\\n\",\n      \"    }\\n\",\n      \"]\\n\",\n      \"Context relevance: 100\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"handler = wf.run(\\n\",\n    \"    start_event=ResearchQuestionEvent(\\n\",\n    \"        question=\\\"Which transport protocols does the Model Context Protocol specification version 2025-03-26 support?\\\"\\n\",\n    \"    )\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"async for event in handler.stream_events():\\n\",\n    \"    if isinstance(event, ResearchQuestionEvent):\\n\",\n    \"        print(\\\"Starting working on the question\\\", event.question)\\n\",\n    \"    elif isinstance(event, ContextRelevanceEvent):\\n\",\n    \"        print(f\\\"Context relevance: {event.relevance}\\\")\\n\",\n    \"    elif isinstance(event, RetrieveContextEvent):\\n\",\n    \"        print(f\\\"Context: {event.context}\\\")\\n\",\n    \"\\n\",\n    \"result = await handler\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"1z8vm7yN1FbU\"\n   },\n   \"source\": [\n    \"As you can see, we have successfully retrieved the state-as-a-context from the vector database, and we successfully rated it as highly relevant and used it to augment the generation of our research report!\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"f26Qg9zR61X7\",\n    \"outputId\": \"20bd12ce-bab4-4d0c-c352-6d10be4a4439\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Summary: The Model Context Protocol (MCP) specification version 2025-03-26 enumerates its supported transport protocols, emphasizing modern, secure, and efficient messaging. It supports HTTP/HTTPS as the main transport, with WebSocket, gRPC, and a now-stable MQTT option for lightweight and IoT usage. Raw TCP transport is deprecated and set for future removal for security and interoperability reasons.\\n\",\n      \"Focal Points:\\n\",\n      \"-  Supported transport protocols: HTTP/HTTPS, WebSocket, gRPC, MQTT\\n\",\n      \"- MQTT has reached stable status and is aimed at IoT and edge scenarios\\n\",\n      \"- Raw TCP transport is deprecated due to security and interoperability concerns\\n\",\n      \"- Anticipated removal of raw TCP transport in upcoming MCP versions\\n\",\n      \"References:\\n\",\n      \"-  Section 7, ‘Transport Protocols,’ of the MCP 2025-03-26 spec\\n\",\n      \"Similar Topics:\\n\",\n      \"-  Protocol deprecation and migration strategies\\n\",\n      \"- Transport protocol security best practices\\n\",\n      \"- Comparative analysis of transport protocols for IoT\\n\",\n      \"- HTTP/2 and HTTP/3 in modern protocol suites\\n\",\n      \"- Interoperability challenges in distributed systems\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(\\\"Summary:\\\", result.summary)\\n\",\n    \"print(\\\"Focal Points:\\\\n- \\\", \\\"\\\\n- \\\".join(result.focal_points))\\n\",\n    \"print(\\\"References:\\\\n- \\\", \\\"\\\\n- \\\".join(result.references))\\n\",\n    \"print(\\\"Similar Topics:\\\\n- \\\", \\\"\\\\n- \\\".join(result.similar_topics))\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": [],\n   \"toc_visible\": true\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"name\": \"python\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/streaming_internal_events.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"e6128a94\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Streaming Internal Events\\n\",\n    \"\\n\",\n    \"Event streaming is intrisinc to workflows and very easy to implement, as you can see in this example code:\\n\",\n    \"\\n\",\n    \"```python\\n\",\n    \"class SpecialEvent(Event):\\n\",\n    \"    pass\\n\",\n    \"\\n\",\n    \"class OtherEvent(Event):\\n\",\n    \"    pass\\n\",\n    \"\\n\",\n    \"class MyWorkflow(Workflow):\\n\",\n    \"    ...\\n\",\n    \"\\n\",\n    \"wf = MyWorkflow(...)\\n\",\n    \"\\n\",\n    \"handler = wf.run()\\n\",\n    \"\\n\",\n    \"async for event in handler.stream_events():\\n\",\n    \"    if isinstance(event, SpecialEvent):\\n\",\n    \"        print(\\\"This is a special event, hurray!\\\")\\n\",\n    \"    else:\\n\",\n    \"        print(\\\"Not a special event :(\\\")\\n\",\n    \"```\\n\",\n    \"\\n\",\n    \"Beyond streaming user-defined events, workflows can also stream internal events, such as changes in the state of the current step, input and output events, modifications of the workflow state and variation in the content of internal queues.\\n\",\n    \"\\n\",\n    \"In the following example, we will see how we can leverage internal events streaming to expose details about the current workflow execution - while the workflow is running!\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"6ab2e4e6\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 1. Install needed dependencies\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"f7601894\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"! pip install llama-index-workflows llama-cloud-services llama-index-llms-openai\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"2b991bab\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 2. Define events, workflow state and resources\\n\",\n    \"\\n\",\n    \"In order for our workflow to work, we will need three things:\\n\",\n    \"\\n\",\n    \"- Events classes defining the flow\\n\",\n    \"- A workflow state representation\\n\",\n    \"- External resources to inject into the workflow when needed\\n\",\n    \"\\n\",\n    \"We will build a workflow that takes a document as input, extracts its raw text content and returns a summary based on that text.\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"0275eb8e\",\n   \"metadata\": {},\n   \"source\": [\n    \"### 2.1 Events\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"id\": \"1468c3f3\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from workflows.events import Event, StartEvent, StopEvent\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class InputDocumentEvent(StartEvent):\\n\",\n    \"    document_path: str\\n\",\n    \"    summary_prompt: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class ParsedDocumentEvent(Event):\\n\",\n    \"    document_content: str\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class SummaryEvent(StopEvent):\\n\",\n    \"    document_summary: str\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"03092946\",\n   \"metadata\": {},\n   \"source\": [\n    \"### 2.2 State\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"id\": \"fb90c035\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from pydantic import BaseModel\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class WorkflowState(BaseModel):\\n\",\n    \"    summary_prompt: str = \\\"\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"c7fd34ee\",\n   \"metadata\": {},\n   \"source\": [\n    \"### 2.3 Resources\\n\",\n    \"\\n\",\n    \"For resources, we will use LlamaParse as a document parser, so you will need to set a `LLAMA_CLOUD_API_KEY` in your environment. If you do not have a LlamaCloud API key, you can [get one here](https://cloud.llamaindex.ai).\\n\",\n    \"\\n\",\n    \"Also, you will need an OpenAI API key to use GPT-5 as a document summarizer.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"8edd1990\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import os\\n\",\n    \"\\n\",\n    \"os.environ[\\\"LLAMA_CLOUD_API_KEY\\\"] = \\\"llx-...\\\"\\n\",\n    \"os.environ[\\\"OPENAI_API_KEY\\\"] = \\\"sk-...\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"id\": \"393a057a\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from llama_cloud_services import LlamaParse\\n\",\n    \"from llama_cloud_services.parse import ResultType\\n\",\n    \"from llama_index.llms.openai import OpenAIResponses\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_document_parser(*args, **kwargs) -> LlamaParse:\\n\",\n    \"    # we will use LlamaParse in agentic mode\\n\",\n    \"    return LlamaParse(\\n\",\n    \"        parse_mode=\\\"parse_page_with_agent\\\",\\n\",\n    \"        model=\\\"openai-gpt-4-1-mini\\\",\\n\",\n    \"        high_res_ocr=True,\\n\",\n    \"        adaptive_long_table=True,\\n\",\n    \"        outlined_table_extraction=True,\\n\",\n    \"        output_tables_as_HTML=True,\\n\",\n    \"        result_type=ResultType.MD,\\n\",\n    \"    )\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"async def get_llm_summary(*args, **kwargs) -> OpenAIResponses:\\n\",\n    \"    return OpenAIResponses(model=\\\"gpt-5-mini\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"4503f1ae\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 3. Define the workflow\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"id\": \"08d5d329\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from typing import Annotated\\n\",\n    \"\\n\",\n    \"from workflows import Context, Workflow, step\\n\",\n    \"from workflows.resource import Resource\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class SummaryWorkflow(Workflow):\\n\",\n    \"    @step\\n\",\n    \"    async def get_document_content(\\n\",\n    \"        self,\\n\",\n    \"        ev: InputDocumentEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        document_parser: Annotated[LlamaParse, Resource(get_document_parser)],\\n\",\n    \"    ) -> ParsedDocumentEvent:\\n\",\n    \"        async with ctx.store.edit_state() as state:\\n\",\n    \"            state.summary_prompt = ev.summary_prompt\\n\",\n    \"        result = await document_parser.aparse(ev.document_path)\\n\",\n    \"        content = []\\n\",\n    \"        if isinstance(result, list):\\n\",\n    \"            for r in result:\\n\",\n    \"                content.extend((await r.aget_markdown_documents()))\\n\",\n    \"        else:\\n\",\n    \"            content.extend((await result.aget_markdown_documents()))\\n\",\n    \"        text_content = \\\"\\\"\\n\",\n    \"        for document in content:\\n\",\n    \"            text_content += document.text + \\\"\\\\n\\\\n---\\\\n\\\\n\\\"\\n\",\n    \"        ctx.write_event_to_stream(ParsedDocumentEvent(document_content=text_content))\\n\",\n    \"        return ParsedDocumentEvent(document_content=text_content)\\n\",\n    \"\\n\",\n    \"    @step\\n\",\n    \"    async def summarize_document(\\n\",\n    \"        self,\\n\",\n    \"        ev: ParsedDocumentEvent,\\n\",\n    \"        ctx: Context[WorkflowState],\\n\",\n    \"        llm: Annotated[OpenAIResponses, Resource(get_llm_summary)],\\n\",\n    \"    ) -> SummaryEvent:\\n\",\n    \"        state = await ctx.store.get_state()\\n\",\n    \"        summary_prompt = state.summary_prompt\\n\",\n    \"        summary_res = await llm.acomplete(\\n\",\n    \"            f\\\"Please create a summary of the following document:\\\\n\\\\n'''\\\\n{ev.document_content}\\\\n'''\\\\n\\\\nFollowing these instructions: {summary_prompt}\\\"\\n\",\n    \"        )\\n\",\n    \"        return SummaryEvent(document_summary=summary_res.text)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"56d0f9fc\",\n   \"metadata\": {},\n   \"source\": [\n    \"## 4. Stream Events\\n\",\n    \"\\n\",\n    \"In order to stream the internal events, we will pass `expose_internal = True` to the `stream_events` method on the workflow handler.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"11957bdb\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!curl https://arxiv.org/pdf/2506.05176 -L -o qwen3_embed_paper.pdf\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"id\": \"28ae264f\",\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Name of current step: get_document_content\\n\",\n      \"State of current step: running\\n\",\n      \"Input event for current step: InputDocumentEvent\\n\",\n      \"Output event of current step: No output event yet\\n\",\n      \"Started parsing the file under job_id 995119d9-d55f-4872-aa2a-bded8ab3daf9\\n\",\n      \"..Document has been successfully parsed!\\n\",\n      \"Name of current step: get_document_content\\n\",\n      \"State of current step: not_running\\n\",\n      \"Input event for current step: <class '__main__.InputDocumentEvent'>\\n\",\n      \"Output event of current step: <class '__main__.ParsedDocumentEvent'>\\n\",\n      \"Name of current step: summarize_document\\n\",\n      \"State of current step: running\\n\",\n      \"Input event for current step: ParsedDocumentEvent\\n\",\n      \"Output event of current step: No output event yet\\n\",\n      \"Name of current step: summarize_document\\n\",\n      \"State of current step: not_running\\n\",\n      \"Input event for current step: <class '__main__.ParsedDocumentEvent'>\\n\",\n      \"Output event of current step: <class '__main__.SummaryEvent'>\\n\",\n      \"Here is a concise scientific summary of the Qwen3 Embedding technical report.\\n\",\n      \"\\n\",\n      \"Overview\\n\",\n      \"- The authors introduce the Qwen3 Embedding series: instruction-aware text embedding and point-wise reranking models built on the Qwen3 foundation LLMs. Models are provided at three sizes (0.6B, 4B, 8B parameters) and released under Apache 2.0.\\n\",\n      \"- Goals: produce high-quality, multilingual embeddings and rerankers that perform well across retrieval, STS, classification, code retrieval, long documents and instruction-following retrieval tasks.\\n\",\n      \"\\n\",\n      \"Model design and inference\\n\",\n      \"- Backbone: dense (causal-attention) Qwen3 LLMs (0.6B / 4B / 8B), long context (32K), embedding dimensions: 1024 / 2560 / 4096 respectively; embedding models support customizable output dimension (MRL).\\n\",\n      \"- Embedding inference: instruction + query placed in input; embedding taken from final-layer hidden state at the appended [EOS] token.\\n\",\n      \"- Reranking inference: instruction + query + candidate document presented in a chat template; model produces a binary “yes” / “no” distribution and the relevance score is computed from the probability of “yes”.\\n\",\n      \"\\n\",\n      \"Training recipe\\n\",\n      \"- Multi-stage pipeline for embedding models:\\n\",\n      \"  1. Large-scale weakly supervised pre-training on synthetic pair data (∼150M pairs) synthesized by Qwen3-32B with prompts controlling task, language, query type, length, difficulty and persona/role.\\n\",\n      \"  2. Supervised fine-tuning on high-quality labeled data (∼7M) combined with filtered synthetic examples (~12M pairs selected by cosine similarity > 0.7).\\n\",\n      \"  3. Model merging: spherical linear interpolation (slerp) across multiple fine-tuning checkpoints to improve robustness and generalization.\\n\",\n      \"- Reranker training skips the weak-supervision pretraining and uses supervised fine-tuning plus model merging.\\n\",\n      \"- Objectives: embeddings optimized with an InfoNCE-style contrastive loss (cosine similarity, temperature, in-batch negatives with a mask to mitigate false negatives). Rerankers optimized with a supervised log-loss on the yes/no label.\\n\",\n      \"\\n\",\n      \"Synthetic data generation\\n\",\n      \"- Two-stage generation per document: (1) configure query type / difficulty / persona via an LLM-assisted selection, (2) generate queries from that configuration. This enables controlled, diverse multilingual synthetic pairs across retrieval, bitext mining, STS and classification tasks.\\n\",\n      \"\\n\",\n      \"Evaluation and results\\n\",\n      \"- Benchmarks: MMTEB (massive multilingual extension of MTEB), MTEB (English), CMTEB (Chinese), MTEB-Code, FollowIR and other retrieval/reranking sets.\\n\",\n      \"- Embeddings: Qwen3-Embedding-4B and -8B achieve state-of-the-art scores across MMTEB/MTEB/CMTEB and code retrieval. Representative numbers: Qwen3-Embedding-8B attains 70.58 on the MTEB Multilingual benchmark and 80.68 on MTEB Code (reported comparisons to leading open-source and commercial baselines, including Gemini Embedding).\\n\",\n      \"- Rerankers: all Qwen3-Reranker models improve over baseline rerankers and over raw embedding retrieval. The larger rerankers (4B/8B) deliver the best ranking performance; the 8B reranker improves ranking performance by multiple points versus the 0.6B variant (the report cites ~+3.0 points over many tasks).\\n\",\n      \"- Practical pipeline: retrieval is performed with embeddings to produce a top-100 list, then rerankers refine ordering; this was used for fair reranker comparisons.\\n\",\n      \"\\n\",\n      \"Ablations and analysis\\n\",\n      \"- Synthetic pretraining is important: training only on synthetic data yields reasonable performance, but removing the weak-supervision stage degrades final results. Example (0.6B model): MMTEB mean(task) drops substantially when synthetic pretraining is removed.\\n\",\n      \"- Model merging helps: disabling model merging reduces performance compared to the merged model, confirming merging’s role in robustness/generalization.\\n\",\n      \"\\n\",\n      \"Conclusions and release\\n\",\n      \"- The Qwen3 Embedding series demonstrates that large LLMs can be effectively used both as backbones and as controllable generators of large-scale, high-quality synthetic training data, producing state-of-the-art multilingual and code embeddings and strong rerankers.\\n\",\n      \"- Models (0.6B / 4B / 8B for both embedding and reranking) and code are publicly available to encourage reproducibility and community use.\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"from workflows.events import StepStateChanged\\n\",\n    \"\\n\",\n    \"wf = SummaryWorkflow(timeout=600)\\n\",\n    \"handler = wf.run(\\n\",\n    \"    start_event=InputDocumentEvent(\\n\",\n    \"        document_path=\\\"qwen3_embed_paper.pdf\\\",\\n\",\n    \"        summary_prompt=\\\"This is a paper, so you should summarize it while still maintaining a scientific tone and its core concepts and findings\\\",\\n\",\n    \"    )\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"async for event in handler.stream_events(expose_internal=True):\\n\",\n    \"    if isinstance(event, StepStateChanged):\\n\",\n    \"        print(\\\"Name of current step:\\\", event.name)\\n\",\n    \"        print(\\\"State of current step:\\\", event.step_state.value)\\n\",\n    \"        print(\\\"Input event for current step:\\\", event.input_event_name)\\n\",\n    \"        print(\\n\",\n    \"            \\\"Output event of current step:\\\",\\n\",\n    \"            event.output_event_name or \\\"No output event yet\\\",\\n\",\n    \"        )\\n\",\n    \"    elif isinstance(event, ParsedDocumentEvent):\\n\",\n    \"        print(\\\"Document has been successfully parsed!\\\")\\n\",\n    \"    else:\\n\",\n    \"        continue\\n\",\n    \"\\n\",\n    \"result = await handler\\n\",\n    \"print(result.document_summary)\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.11.10\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 5\n}\n"
  },
  {
    "path": "examples/visualization/resource_nodes_example.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nExample demonstrating resource nodes in workflow graph visualization.\n\nThis example shows how resources (dependencies injected via Annotated types)\nare rendered in both Mermaid and Pyvis diagrams.\n\nRun this script to generate:\n- workflow_with_resources.mermaid - A Mermaid diagram file\n- workflow_with_resources.html - An interactive Pyvis HTML visualization\n\nYou can view the Mermaid diagram at https://mermaid.live/ by pasting the contents.\nThe HTML file can be opened directly in any web browser.\n\"\"\"\n\nimport argparse\nfrom typing import Annotated\n\nfrom llama_index.utils.workflow import (\n    draw_all_possible_flows,\n    draw_all_possible_flows_mermaid,\n)\nfrom workflows import Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.resource import Resource\n\n# --- Mock resource types ---\n\n\nclass DatabaseClient:\n    \"\"\"A database client for persistent storage.\"\"\"\n\n    def __init__(self, connection_string: str = \"postgres://localhost/db\"):\n        self.connection_string = connection_string\n\n    def query(self, sql: str) -> list:\n        \"\"\"Execute a SQL query.\"\"\"\n        return []\n\n\nclass CacheClient:\n    \"\"\"A cache client for fast data retrieval.\"\"\"\n\n    def __init__(self, host: str = \"localhost\", port: int = 6379):\n        self.host = host\n        self.port = port\n\n    def get(self, key: str) -> str | None:\n        \"\"\"Get a value from cache.\"\"\"\n        return None\n\n    def set(self, key: str, value: str) -> None:\n        \"\"\"Set a value in cache.\"\"\"\n        pass\n\n\nclass LLMClient:\n    \"\"\"A client for interacting with a large language model.\"\"\"\n\n    def __init__(self, api_key: str = \"sk-...\"):\n        self.api_key = api_key\n\n    async def complete(self, prompt: str) -> str:\n        \"\"\"Generate a completion for the given prompt.\"\"\"\n        return f\"Response to: {prompt}\"\n\n\n# --- Resource factory functions ---\n\n\ndef get_database_client() -> DatabaseClient:\n    \"\"\"Factory function to create a database client.\n\n    This function creates a PostgreSQL database client configured\n    for the application's data storage needs.\n    \"\"\"\n    return DatabaseClient(connection_string=\"postgres://localhost/myapp\")\n\n\ndef get_cache_client() -> CacheClient:\n    \"\"\"Factory function to create a Redis cache client.\n\n    Provides fast caching for frequently accessed data.\n    \"\"\"\n    return CacheClient(host=\"localhost\", port=6379)\n\n\ndef get_llm_client() -> LLMClient:\n    \"\"\"Factory function to create an LLM client.\n\n    Creates a client for the language model API.\n    \"\"\"\n    return LLMClient(api_key=\"sk-example-key\")\n\n\n# --- Event types ---\n\n\nclass QueryProcessedEvent(Event):\n    \"\"\"Event emitted after processing a user query.\"\"\"\n\n    query: str\n    cached: bool = False\n\n\nclass ContextRetrievedEvent(Event):\n    \"\"\"Event emitted after retrieving context from the database.\"\"\"\n\n    context: str\n\n\nclass ResponseGeneratedEvent(Event):\n    \"\"\"Event emitted after generating an LLM response.\"\"\"\n\n    response: str\n\n\n# --- Workflow with resources ---\n\n\nclass RAGWorkflow(Workflow):\n    \"\"\"A RAG (Retrieval-Augmented Generation) workflow demonstrating resource usage.\n\n    This workflow shows how different steps can depend on shared resources\n    like database clients, cache clients, and LLM clients.\n    \"\"\"\n\n    @step\n    async def process_query(\n        self,\n        ev: StartEvent,\n        cache: Annotated[CacheClient, Resource(get_cache_client)],\n    ) -> QueryProcessedEvent:\n        \"\"\"Process the incoming query, checking cache first.\"\"\"\n        query = getattr(ev, \"query\", \"default query\")\n\n        # Check if query result is cached\n        cached_result = cache.get(f\"query:{query}\")\n        if cached_result:\n            return QueryProcessedEvent(query=query, cached=True)\n\n        return QueryProcessedEvent(query=query, cached=False)\n\n    @step\n    async def retrieve_context(\n        self,\n        ev: QueryProcessedEvent,\n        db: Annotated[DatabaseClient, Resource(get_database_client)],\n        cache: Annotated[CacheClient, Resource(get_cache_client)],\n    ) -> ContextRetrievedEvent:\n        \"\"\"Retrieve relevant context from the database.\"\"\"\n        if ev.cached:\n            context = \"Cached context\"\n        else:\n            # Query the database for relevant documents\n            results = db.query(\n                f\"SELECT content FROM documents WHERE query = '{ev.query}'\"\n            )\n            context = \" \".join(str(r) for r in results) or \"No context found\"\n\n            # Cache the result\n            cache.set(f\"context:{ev.query}\", context)\n\n        return ContextRetrievedEvent(context=context)\n\n    @step\n    async def generate_response(\n        self,\n        ev: ContextRetrievedEvent,\n        llm: Annotated[LLMClient, Resource(get_llm_client)],\n    ) -> ResponseGeneratedEvent:\n        \"\"\"Generate a response using the LLM with the retrieved context.\"\"\"\n        prompt = f\"Context: {ev.context}\\n\\nGenerate a response.\"\n        response = await llm.complete(prompt)\n        return ResponseGeneratedEvent(response=response)\n\n    @step\n    async def finalize_response(\n        self,\n        ev: ResponseGeneratedEvent,\n        cache: Annotated[CacheClient, Resource(get_cache_client)],\n    ) -> StopEvent:\n        \"\"\"Finalize and cache the response.\"\"\"\n        # Cache the final response\n        cache.set(\"last_response\", ev.response)\n        return StopEvent(result=ev.response)\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=\"Generate workflow visualizations with resource nodes\"\n    )\n    parser.add_argument(\n        \"--output-dir\",\n        default=\".\",\n        help=\"Directory to save output files (default: current directory)\",\n    )\n    parser.add_argument(\n        \"--mermaid-only\",\n        action=\"store_true\",\n        help=\"Only generate Mermaid output (print to stdout)\",\n    )\n    args = parser.parse_args()\n\n    # Create the workflow\n    workflow = RAGWorkflow()\n\n    print(\"=\" * 60)\n    print(\"Workflow Graph Visualization with Resource Nodes\")\n    print(\"=\" * 60)\n\n    # Generate Mermaid diagram\n    mermaid_file = f\"{args.output_dir}/workflow_with_resources.mermaid\"\n    mermaid_output = draw_all_possible_flows_mermaid(\n        workflow,\n        filename=\"\" if args.mermaid_only else mermaid_file,\n    )\n\n    print(\"\\n--- Mermaid Diagram ---\")\n    print(mermaid_output)\n    print()\n\n    if not args.mermaid_only:\n        print(f\"Mermaid diagram saved to: {mermaid_file}\")\n\n        # Generate Pyvis HTML\n        html_file = f\"{args.output_dir}/workflow_with_resources.html\"\n        draw_all_possible_flows(workflow, filename=html_file)\n        print(f\"Interactive Pyvis diagram saved to: {html_file}\")\n\n    print(\"\\n--- Resource Nodes Summary ---\")\n    print(\n        \"\"\"\nThe diagram shows:\n- HEXAGON nodes (plum color): Resource dependencies\n  - DatabaseClient: Database connection via get_database_client()\n  - CacheClient: Cache connection via get_cache_client()\n  - LLMClient: LLM API client via get_llm_client()\n\n- Edge labels on resource connections show the variable name used in the step\n  e.g., \"db\", \"cache\", \"llm\"\n\n- Resources are deduplicated: CacheClient appears once even though\n  it's used by multiple steps (process_query, retrieve_context, finalize_response)\n\nTo view the Mermaid diagram:\n  1. Go to https://mermaid.live/\n  2. Paste the diagram content above\n  3. See the interactive visualization\n\nTo view the Pyvis diagram:\n  1. Open workflow_with_resources.html in a web browser\n  2. Hover over nodes to see metadata (type, getter, source location, docstring)\n  3. Drag nodes to rearrange the layout\n\"\"\"\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "operator/.gitignore",
    "content": "cover.out\n"
  },
  {
    "path": "operator/.golangci.yml",
    "content": "version: \"2\"\nrun:\n  allow-parallel-runners: true\nlinters:\n  default: none\n  enable:\n    - copyloopvar\n    - dupl\n    - errcheck\n    - ginkgolinter\n    - goconst\n    - gocyclo\n    - govet\n    - ineffassign\n    - lll\n    - misspell\n    - nakedret\n    - prealloc\n    - revive\n    - staticcheck\n    - unconvert\n    - unparam\n    - unused\n  settings:\n    revive:\n      rules:\n        - name: comment-spacings\n        - name: import-shadowing\n  exclusions:\n    generated: lax\n    rules:\n      - linters:\n          - lll\n        path: api/*\n      - linters:\n          - dupl\n          - lll\n        path: internal/*\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  enable:\n    - gofmt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": "operator/AGENTS.md",
    "content": "## Operator (Go)\n\nMakefile lives in `operator/`. Run targets with `make -C operator <target>` from repo root, or `make <target>` from within `operator/`.\n\nSetup:\n- Generate CRDs/RBAC: `make -C operator operator-manifests`\n- Generate deepcopy: `make -C operator operator-generate`\n\nChecks (run all before pushing):\n- Lint: `make -C operator operator-lint`\n- Unit tests (fast, no envtest): `make -C operator operator-unit-test`\n- Integration tests (envtest): `make -C operator operator-test`\n\nRunning specific tests:\n- Unit: `cd operator && go test -tags='!integration' ./internal/controller/ -run TestMyTest -v`\n- Integration: `eval \"$($(go env GOPATH)/bin/setup-envtest use -p env)\" && cd operator && go test -tags=integration ./internal/controller/ -run 'TestControllers/MyTest' -v`\n- Unit tests use `//go:build !integration`, integration tests use `//go:build integration`\n\nTest patterns:\n- Unit tests use Go `testing` + fake client (`sigs.k8s.io/controller-runtime/pkg/client/fake`)\n- Integration tests use Ginkgo/Gomega + envtest (real API server). Test names are `TestControllers/description`.\n- Shared helpers are in `test_utils_test.go` (e.g. `NewTestReconciler`, `NewLlama`, `CreateAndReconcile`, `CompleteBuild`)\n\nLocal run:\n- `make -C operator operator-run` (requires kubeconfig)\n"
  },
  {
    "path": "operator/CHANGELOG.md",
    "content": "# llama-agents-operator\n\n## 0.11.1\n\n### Patch Changes\n\n- fdc1c48: Publish llama-agents-operator arm images\n\n## 0.11.0\n\n### Minor Changes\n\n- 3e2e7b8: Run all containers as non-root with hardened security contexts\n\n## 0.10.2\n\n### Patch Changes\n\n- 782939b: Strip legacy appserver- prefix from image tags to fix image pull failures\n\n## 0.10.1\n\n### Patch Changes\n\n- de92a8b: Fix hardcoded watch namespace fallback to use pod's actual namespace\n\n## 0.10.0\n\n### Minor Changes\n\n- 58e7942: Rename Docker image repos to per-component names (llama-agents-<component>) with plain version tags\n\n### Patch Changes\n\n- ea577a1: Disable nginx access log for the file server sidecar\n\n## 0.9.0\n\n### Minor Changes\n\n- e2f3abd: Rename deployment name to display_name, add optional explicit id on create\n\n## 0.8.0\n\n### Minor Changes\n\n- e24ebda: Move CRDs to Helm crds/ directory for install-only lifecycle, add llama-agents-crds chart for CRD upgrades, and make build API host configurable via LLAMA_DEPLOY_BUILD_API_HOST env var.\n\n## 0.7.2\n\n## 0.7.1\n\n## 0.7.0\n\n### Patch Changes\n\n- 9641415: Add dulwich-based git serving for internal repos. Users can push code via `llamactl push` and build pods clone via the build API. Bare repos are stored as tarballs in S3.\n- 7e241b6: Refactor LlamaDeployment controller for readability and increase coverage\n\n## 0.6.5\n\n### Patch Changes\n\n- 9bb95fe: Fix infinite build retry loop where failed builds for deployments with generation > failedRolloutGeneration would endlessly delete and recreate jobs\n\n## 0.6.4\n\n### Patch Changes\n\n- f3a38d0: Fix build job template overlay to merge resource requirements instead of replacing, so template-specified fields (e.g. ephemeral-storage) don't wipe out default CPU/memory requests and limits\n- f3a38d0: Fix suspended deployments getting stuck in Pending/Building phase. Skip builds, capacity gates, and phase resets for suspended deployments. Add `status.lastBuiltGeneration` to allow explicit pre-builds via `spec.buildGeneration` bump while suspended.\n\n## 0.6.3\n\n### Patch Changes\n\n- 4127101: Enable build retries by cleaning up stale failed jobs when the CR generation advances past the failed generation\n- 4127101: Fix build job resource fallback to inherit app container resources when no dedicated build container is defined in the template overlay\n- 2e1b600: Fix max concurrent rollouts gate to include PhaseBuilding deployments, preventing build jobs from bypassing the concurrency limit\n- 4127101: Preserve PhaseBuilding during reconciliation to prevent status flip-flopping while a build job is running\n- 4127101: Add self-watch so rollout-gated CRs wake immediately when another CR transitions out of a rolling phase, instead of waiting for the jitter timer\n- 4127101: Strip build containers from runtime deployment overlay to prevent strategic merge from adding an invalid container to the pod spec\n\n## 0.6.2\n\n## 0.6.1\n\n## 0.6.0\n\n## 0.5.3\n\n## 0.5.2\n\n## 0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- ac74af4: Run build separately as a 1x time process per deployment update. Build stored in s3. Allows for fast unsuspend, and better future support for replication\n"
  },
  {
    "path": "operator/Makefile",
    "content": ".PHONY: help\n\n# Default target\nhelp: ## Show this help message\n\t@echo \"Available commands:\"\n\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-20s\\033[0m %s\\n\", $$1, $$2}'\n\n##@ Operator\n\n# Operator build variables\nOPERATOR_IMG ?= llamaindex/llama-agents-operator:latest\nLOCALBIN ?= $(shell pwd)/bin\nCONTROLLER_GEN ?= $(LOCALBIN)/controller-gen\nCONTROLLER_TOOLS_VERSION ?= v0.18.0\nGOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint\nGOLANGCI_LINT_VERSION ?= v2.2.1\n\n$(LOCALBIN):\n\tmkdir -p $(LOCALBIN)\n\n.PHONY: controller-gen\ncontroller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.\n$(CONTROLLER_GEN): $(LOCALBIN)\n\ttest -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \\\n\tGOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)\n\n.PHONY: golangci-lint\ngolangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.\n$(GOLANGCI_LINT): $(LOCALBIN)\n\ttest -s $(LOCALBIN)/golangci-lint && $(LOCALBIN)/golangci-lint --version | grep -q $(GOLANGCI_LINT_VERSION) || \\\n\tGOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)\n\n.PHONY: operator-manifests\noperator-manifests: controller-gen ## Generate operator CRDs and RBAC manifests\n\t$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths=\"./...\" output:crd:artifacts:config=config/crd/bases\n\t@echo \"Processing generated manifests for Helm templating...\"\n\t@uv run ../scripts/process_manifests.py\n\n.PHONY: operator-manifests-check\noperator-manifests-check: operator-manifests ## Verify generated manifests are up to date\n\t@git diff --exit-code config/ ../charts/llama-agents/crds/ ../charts/llama-agents-crds/files/ ../charts/llama-agents/templates/rbac.yaml || \\\n\t\t(echo \"ERROR: Generated manifests are out of date. Run 'make -C operator operator-manifests' and commit.\" && exit 1)\n\n.PHONY: operator-generate\noperator-generate: controller-gen ## Generate operator code (DeepCopy methods)\n\t$(CONTROLLER_GEN) object paths=\"./...\"\n\n.PHONY: operator-build\noperator-build: operator-manifests operator-generate ## Build operator binary\n\tgo fmt ./... && go vet ./... && go build -o bin/manager cmd/main.go\n\n.PHONY: operator-lint\noperator-lint: golangci-lint ## Run operator linting\n\t@$(GOLANGCI_LINT) run\n\n.PHONY: operator-test\noperator-test: ## Run operator integration tests (envtest, build tag: integration)\n\t@echo \"Installing envtest...\"\n\t@go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest\n\t@echo \"Running operator integration tests with envtest...\"\n\t@eval \"$$($(shell go env GOPATH)/bin/setup-envtest use -p env)\" && go test -tags=integration ./... -coverprofile cover.out\n\n.PHONY: operator-unit-test\noperator-unit-test: ## Run fast operator unit tests (no envtest)\n\t@go test -tags='!integration' ./...\n\n.PHONY: operator-docker-build\noperator-docker-build: ## Build operator Docker image\n\tdocker build -t $(OPERATOR_IMG) -f ../docker/operator.Dockerfile ..\n\n.PHONY: operator-run\noperator-run: operator-manifests operator-generate ## Run operator locally (requires kubeconfig)\n\tgo run ./cmd/main.go\n\n\n##@ Helm Chart Validation\n\nCHART_DIR := ../charts/llama-agents\nCRD_CHART_DIR := ../charts/llama-agents-crds\n\n.PHONY: kube-ensure-kind-context\nkube-ensure-kind-context: ## Ensure kubectl context points to a kind cluster\n\t@set -e; \\\n\tCUR=$$(kubectl config current-context 2>/dev/null || true); \\\n\tif echo $$CUR | grep -q '^kind-'; then \\\n\t  echo \"Using kind context: $$CUR\"; \\\n\telse \\\n\t  FIRST_KIND=$$(kubectl config get-contexts -o name | grep '^kind-' | head -n1); \\\n\t  if [ -n \"$$FIRST_KIND\" ]; then \\\n\t    echo \"Switching kubectl context to $$FIRST_KIND\"; \\\n\t    kubectl config use-context $$FIRST_KIND >/dev/null; \\\n\t  else \\\n\t    echo \"No kind context found. Please create one (e.g., kind create cluster)\"; \\\n\t    exit 1; \\\n\t  fi; \\\n\tfi\n\n.PHONY: helm-lint\nhelm-lint: ## Lint Helm chart with default values\n\thelm lint $(CHART_DIR)\n\n.PHONY: helm-lint-dev\nhelm-lint-dev: ## Lint Helm chart with dev values\n\thelm lint $(CHART_DIR) -f ./tilt/helm/values-dev.yaml\n\n.PHONY: helm-unittest-install\nhelm-unittest-install: ## Install helm-unittest plugin\n\t@if ! helm plugin list | grep -q \"unittest\"; then \\\n\t\thelm plugin install --verify=false https://github.com/helm-unittest/helm-unittest; \\\n\tfi\n\n.PHONY: helm-unittest\nhelm-unittest: helm-unittest-install ## Run helm unittest\n\thelm unittest $(CHART_DIR)\n\n.PHONY: helm-template\nhelm-template: ## Render templates with default values\n\thelm template $(CHART_DIR) --set controlPlane.objectStorage.s3.bucket=test-bucket >/dev/null\n\n.PHONY: helm-template-dev\nhelm-template-dev: ## Render templates with dev values\n\thelm template $(CHART_DIR) -f ./tilt/helm/values-dev.yaml >/dev/null\n\n.PHONY: helm-dry-run\nhelm-dry-run: kube-ensure-kind-context ## Server-side dry-run apply (requires kube cluster, default values)\n\thelm template $(CHART_DIR) --set controlPlane.objectStorage.s3.bucket=test-bucket | kubectl apply --dry-run=server -f -\n\n.PHONY: helm-dry-run-dev\nhelm-dry-run-dev: kube-ensure-kind-context ## Server-side dry-run apply (requires kube cluster, dev values)\n\thelm template $(CHART_DIR) -f ./tilt/helm/values-dev.yaml | kubectl apply --dry-run=server -f -\n\n.PHONY: helm-lint-crds\nhelm-lint-crds: ## Lint CRD chart\n\thelm lint $(CRD_CHART_DIR)\n\n.PHONY: helm-template-crds\nhelm-template-crds: ## Render CRD chart templates\n\thelm template $(CRD_CHART_DIR) >/dev/null\n\n.PHONY: helm-crds-prom-operator\nhelm-crds-prom-operator: kube-ensure-kind-context ## Install Prometheus Operator CRDs (for ServiceMonitor)\n\t# Use server-side apply to avoid storing huge last-applied annotations on CRDs\n\tkubectl apply --server-side -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/bundle.yaml\n\t# Wait for CRD to be established before proceeding\n\ttimeout=120; \\\n\twhile [ $$timeout -gt 0 ]; do \\\n\t\tif kubectl get crd servicemonitors.monitoring.coreos.com -o jsonpath='{range .status.conditions[*]}{.type}={.status}{\"\\n\"}{end}' 2>/dev/null | grep -q '^Established=True$$'; then \\\n\t\t\texit 0; \\\n\t\tfi; \\\n\t\tsleep 2; \\\n\t\ttimeout=$$((timeout-2)); \\\n\tdone; \\\n\techo \"Timed out waiting for crd/servicemonitors.monitoring.coreos.com to become Established\" >&2; \\\n\texit 1\n\n.PHONY: helm-docs-install\nhelm-docs-install: ## Install helm-docs via go install\n\tgo install github.com/norwoodj/helm-docs/cmd/helm-docs@latest\n\n.PHONY: helm-docs\nhelm-docs: ## Generate Helm chart README from values.yaml comments\n\thelm-docs --chart-search-root $(CHART_DIR) --sort-values-order file\n\n.PHONY: helm-docs-check\nhelm-docs-check: ## Verify Helm chart README is up to date\n\thelm-docs --chart-search-root $(CHART_DIR) --sort-values-order file\n\tgit diff --exit-code $(CHART_DIR)/README.md\n\n.PHONY: helm-validate\nhelm-validate: helm-lint helm-lint-dev helm-lint-crds helm-template helm-template-dev helm-template-crds helm-unittest helm-dry-run helm-dry-run-dev ## Run full Helm validation suite\n"
  },
  {
    "path": "operator/Tiltfile",
    "content": "# Tiltfile for llama-agents local development\n# Supports kind (default) and docker-desktop targets.\n# Pass target via: tilt up -- --target=docker-desktop\n\nconfig.define_string('target', args=True, usage='Cluster target: kind or docker-desktop')\nconfig.define_string('apps-namespace', args=False, usage='Namespace for LlamaDeployment CRs and app resources. Unset = release namespace.')\ncfg = config.parse()\ntarget = cfg.get('target', 'kind')\napps_namespace = cfg.get('apps-namespace', '')\n\nK8S_CONTEXTS = {\n    'kind': 'kind-kind',\n    'docker-desktop': 'docker-desktop',\n}\nallow_k8s_contexts(K8S_CONTEXTS[target])\n\n# ---------------------------------------------------------------------------\n# Docker images\n# ---------------------------------------------------------------------------\n\n# Control plane (Python) — Tilt auto-injects into the deployment's image: field\ndocker_build(\n    'llama-agents-control-plane',\n    context='..',\n    dockerfile='../docker/Dockerfile',\n    target='controlplane',\n    only=[\n        './packages/',\n        './docker/Dockerfile',\n        './pyproject.toml',\n        './uv.lock',\n        './README.md',\n    ],\n)\n\n# Operator (Go) — Tilt auto-injects into the deployment's image: field\ndocker_build(\n    'llama-agents-operator',\n    context='..',\n    dockerfile='../docker/operator.Dockerfile',\n    only=[\n        './operator/',\n        './docker/operator.Dockerfile',\n    ],\n)\n\n# App server — used by the operator at runtime (not in helm-rendered YAML).\n# The operator reads repo/tag from separate env vars, so Tilt can't auto-inject.\n# We build with a fixed tag and load into the cluster manually (kind only).\nAPPSERVER_TAG = 'tilt-dev'\nAPPSERVER_IMG = 'llama-agents-appserver:%s' % APPSERVER_TAG\n\nif target == 'kind':\n    _appserver_cmd = 'docker build -t %s -f ../docker/Dockerfile --target appserver .. && kind load docker-image %s --name kind' % (APPSERVER_IMG, APPSERVER_IMG)\nelse:\n    # docker-desktop shares the Docker daemon — no load step needed\n    _appserver_cmd = 'docker build -t %s -f ../docker/Dockerfile --target appserver ..' % APPSERVER_IMG\n\nlocal_resource(\n    'appserver-image',\n    cmd=_appserver_cmd,\n    deps=[\n        '../packages/',\n        '../docker/Dockerfile',\n        '../pyproject.toml',\n        '../uv.lock',\n    ],\n)\n\n# ---------------------------------------------------------------------------\n# Dev infrastructure\n# ---------------------------------------------------------------------------\n\n# Prometheus Operator + Prometheus instance for ServiceMonitor-based scraping.\n# Installed outside Tilt's resource management to avoid label conflicts.\nlocal(\n    'tilt/scripts/install-prometheus.sh %s' % K8S_CONTEXTS[target],\n    quiet=True,\n)\n\n# SeaweedFS for S3-compatible backup storage\nk8s_yaml('tilt/k8s-manifests/seaweedfs.yaml')\n\nk8s_yaml('tilt/k8s-manifests/object-storage-secrets.yaml')\nk8s_yaml('tilt/k8s-manifests/backup-encryption-secrets.yaml')\n\n# Optional control plane secrets from .env (GITHUB_APP_PRIVATE_KEY, GITHUB_APP_CLIENT_ID, GITHUB_APP_NAME, GITHUB_APP_SECRET)\nif os.path.exists('../.env'):\n    k8s_yaml(local('python3 tilt/env-to-secret.py', quiet=True))\n\n\n# ---------------------------------------------------------------------------\n# CRDs — installed via the llama-agents-crds chart so the chart is exercised\n# in the dev loop. The keep annotation (default) ensures CRDs survive\n# `tilt down` / `helm uninstall` and CRs are not garbage-collected.\n# ---------------------------------------------------------------------------\n\nlocal(\n    'helm upgrade --install llama-agents-crds ../charts/llama-agents-crds -n llama-agents --create-namespace',\n    quiet=True,\n)\n\n# ---------------------------------------------------------------------------\n# Helm deployment\n# ---------------------------------------------------------------------------\n\nhelm_sets = [\n    # Controlplane + operator: repos match docker_build names so Tilt\n    # auto-replaces the image: field with its content-addressed ref.\n    'images.controlPlane.repository=llama-agents-control-plane',\n    'images.controlPlane.tag=unused-tilt-will-replace',\n    'images.operator.repository=llama-agents-operator',\n    'images.operator.tag=unused-tilt-will-replace',\n    # Appserver: operator reads these as env vars at runtime.\n    # Fixed tag matches what local_resource loads into kind.\n    'images.appserver.repository=llama-agents-appserver',\n    'images.appserver.tag=%s' % APPSERVER_TAG,\n    'images.appserver.pullPolicy=Never',\n]\nif apps_namespace:\n    helm_sets.append('apps.namespace=%s' % apps_namespace)\n\nk8s_yaml(\n    helm(\n        '../charts/llama-agents',\n        name='llama-agents',\n        namespace='llama-agents',\n        values=['tilt/helm/values-dev.yaml'],\n        set=helm_sets,\n    ),\n)\n\n# ---------------------------------------------------------------------------\n# Resource configuration and port forwards\n# ---------------------------------------------------------------------------\n\n# Control plane deployment — forward the service port to localhost:8011\nk8s_resource(\n    'llama-agents-control-plane',\n    port_forwards='8011:8000',\n)\n\n# SeaweedFS — S3-compatible storage for backup testing\nk8s_resource('seaweedfs')\n\n# Create the S3 bucket inside SeaweedFS once it's running\nlocal_resource(\n    'seaweedfs-init',\n    cmd=\"for i in 1 2 3 4 5; do kubectl exec -n llama-agents deploy/seaweedfs -- sh -c \\\"echo 's3.bucket.create -name backups' | weed shell -master=localhost:9333\\\" && exit 0; echo \\\"seaweedfs-init attempt $i failed, retrying...\\\"; sleep 3; done; echo 'seaweedfs-init failed after 5 attempts'; exit 1\",\n    resource_deps=['seaweedfs'],\n)\n\n# Operator deployment — depends on appserver image being built first\nk8s_resource(\n    'llama-agents-operator',\n    resource_deps=['appserver-image'],\n    port_forwards='8080:8080',\n)\n\n# ---------------------------------------------------------------------------\n# Maintenance\n# ---------------------------------------------------------------------------\n\n# Prune orphaned containerd snapshots inside the kind node every 6 hours.\n# Only relevant for kind — its containerd layers grow unbounded.\nif target == 'kind':\n    k8s_yaml('tilt/k8s-manifests/kind-gc-cronjob.yaml')\n    k8s_resource('kind-gc', labels=['infra'])\n"
  },
  {
    "path": "operator/api/v1/groupversion_info.go",
    "content": "// Package v1 contains API Schema definitions for the deploy v1 API group\n// +kubebuilder:object:generate=true\n// +groupName=deploy.llamaindex.ai\npackage v1\n\nimport (\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"sigs.k8s.io/controller-runtime/pkg/scheme\"\n)\n\nvar (\n\t// GroupVersion is group version used to register these objects.\n\tGroupVersion = schema.GroupVersion{Group: \"deploy.llamaindex.ai\", Version: \"v1\"}\n\n\t// SchemeBuilder is used to add go types to the GroupVersionKind scheme.\n\tSchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}\n\n\t// AddToScheme adds the types in this group-version to the given scheme.\n\tAddToScheme = SchemeBuilder.AddToScheme\n)\n"
  },
  {
    "path": "operator/api/v1/llamadeployment_types.go",
    "content": "package v1\n\nimport (\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!\n// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.\n\n// LlamaDeploymentSpec defines the desired state of LlamaDeployment.\ntype LlamaDeploymentSpec struct {\n\t// ProjectId is the project ID\n\tProjectId string `json:\"projectId\"`\n\n\t// DisplayName is the user-facing deployment label\n\tDisplayName string `json:\"displayName,omitempty\"`\n\n\t// Name is the deployment name (DEPRECATED: use DisplayName)\n\tName string `json:\"name,omitempty\"`\n\n\t// RepoUrl is the URL of the repository to deploy\n\tRepoUrl string `json:\"repoUrl\"`\n\n\t// DeploymentFilePath is the path to the deployment file within the repository\n\t// +kubebuilder:default=\"llama_deployment.yml\"\n\tDeploymentFilePath string `json:\"deploymentFilePath,omitempty\"`\n\n\t// GitRef is the git reference (commit SHA, branch, or tag) to deploy\n\tGitRef string `json:\"gitRef,omitempty\"`\n\n\t// A resolved git sha for the git ref\n\tGitSha string `json:\"gitSha,omitempty\"`\n\n\t// SecretName is the name of the Kubernetes Secret containing PAT and deployment secrets\n\tSecretName string `json:\"secretName,omitempty\"`\n\n\t// Image is the container image registry and name (e.g., \"llamaindex/llama-agents-appserver\")\n\t// If not specified, defaults to environment variable or \"llamaindex/llama-agents-appserver\"\n\tImage string `json:\"image,omitempty\"`\n\n\t// ImageTag is the container image tag\n\t// If not specified, defaults to environment variable or \"latest\"\n\tImageTag string `json:\"imageTag,omitempty\"`\n\n\t// StaticAssetsPath is an optional path (relative to /opt/app) containing\n\t// prebuilt UI assets to be served under /deployments/<deployment-id>/ui\n\tStaticAssetsPath string `json:\"staticAssetsPath,omitempty\"`\n\n\t// TemplateName optionally specifies a LlamaDeploymentTemplate to apply.\n\t// When empty, the operator will look up a template named \"default\".\n\tTemplateName string `json:\"templateName,omitempty\"`\n\n\t// Suspended scales the underlying Deployment to 0 replicas when true.\n\t// Setting suspended to false (or removing the field) restores replicas to 1.\n\tSuspended bool `json:\"suspended,omitempty\"`\n\n\t// BuildGeneration is a monotonically increasing counter that forces a new\n\t// build when incremented, even if all other inputs (gitSha, imageTag, etc.)\n\t// are unchanged. This allows retrying a failed build caused by transient\n\t// errors (e.g. network failures) without requiring a new git commit.\n\tBuildGeneration int64 `json:\"buildGeneration,omitempty\"`\n}\n\n// LlamaDeploymentStatus defines the observed state of LlamaDeployment.\ntype LlamaDeploymentStatus struct {\n\t// Phase represents the current phase of the deployment\n\t// +kubebuilder:validation:Enum=Pending;Running;Failed;RollingOut;RolloutFailed;Suspended;Building;BuildFailed;AwaitingCode\n\tPhase string `json:\"phase,omitempty\"`\n\n\t// Message is a human-readable message indicating details about the current status\n\tMessage string `json:\"message,omitempty\"`\n\n\t// LastUpdated is the timestamp of the last status update\n\tLastUpdated *metav1.Time `json:\"lastUpdated,omitempty\"`\n\n\t// AuthToken is a cryptographically secure token for this deployment\n\tAuthToken string `json:\"authToken,omitempty\"`\n\n\t// SchemaVersion is the version of the CRD schema used when this resource was last reconciled\n\tSchemaVersion string `json:\"schemaVersion,omitempty\"`\n\n\t// LastReconciledGeneration tracks the generation that was last successfully reconciled\n\tLastReconciledGeneration int64 `json:\"lastReconciledGeneration,omitempty\"`\n\n\t// ReleaseHistory keeps the last 20 released git shas with timestamps\n\tReleaseHistory []ReleaseHistoryEntry `json:\"releaseHistory,omitempty\"`\n\n\t// RolloutStartedAt is the timestamp when the current rollout began.\n\t// Set when the phase transitions to Pending or RollingOut, cleared on Running or failure.\n\tRolloutStartedAt *metav1.Time `json:\"rolloutStartedAt,omitempty\"`\n\n\t// FailedRolloutGeneration records the LlamaDeployment generation whose rollout\n\t// timed out. This prevents the operator from re-attempting the same failing rollout.\n\tFailedRolloutGeneration int64 `json:\"failedRolloutGeneration,omitempty\"`\n\n\t// SecretCheckRetries tracks how many times we've retried finding the Secret.\n\t// This handles informer cache lag when the Secret is created just before the CR.\n\tSecretCheckRetries int32 `json:\"secretCheckRetries,omitempty\"`\n\n\t// BuildId is the content-addressed identifier for the current build artifact\n\tBuildId string `json:\"buildId,omitempty\"`\n\n\t// BuildStatus tracks the state of the current build job\n\t// +kubebuilder:validation:Enum=Pending;Running;Succeeded;Failed\n\tBuildStatus string `json:\"buildStatus,omitempty\"`\n\n\t// LastBuiltGeneration is the spec.buildGeneration value that was last\n\t// successfully built. When spec.buildGeneration differs from this value,\n\t// a new build is triggered even if the deployment is suspended.\n\tLastBuiltGeneration int64 `json:\"lastBuiltGeneration,omitempty\"`\n}\n\n// ReleaseHistoryEntry represents a single released version entry\ntype ReleaseHistoryEntry struct {\n\t// GitSha is the released git commit SHA\n\tGitSha string `json:\"gitSha\"`\n\t// ImageTag is the appserver image tag used for this release\n\tImageTag string `json:\"imageTag,omitempty\"`\n\t// ReleasedAt is the timestamp when this version was released\n\tReleasedAt metav1.Time `json:\"releasedAt\"`\n}\n\n// +kubebuilder:object:root=true\n// +kubebuilder:subresource:status\n// +kubebuilder:printcolumn:name=\"Project ID\",type=string,JSONPath=`.spec.projectId`\n// +kubebuilder:printcolumn:name=\"Name\",type=string,JSONPath=`.spec.displayName`\n// +kubebuilder:printcolumn:name=\"Repo\",type=string,JSONPath=`.spec.repoUrl`\n// +kubebuilder:printcolumn:name=\"Phase\",type=string,JSONPath=`.status.phase`\n// +kubebuilder:printcolumn:name=\"Age\",type=date,JSONPath=`.metadata.creationTimestamp`\n\n// LlamaDeployment is the Schema for the llamadeployments API.\ntype LlamaDeployment struct {\n\tmetav1.TypeMeta   `json:\",inline\"`\n\tmetav1.ObjectMeta `json:\"metadata,omitempty\"`\n\n\tSpec   LlamaDeploymentSpec   `json:\"spec,omitempty\"`\n\tStatus LlamaDeploymentStatus `json:\"status,omitempty\"`\n}\n\n// +kubebuilder:object:root=true\n\n// LlamaDeploymentList contains a list of LlamaDeployment.\ntype LlamaDeploymentList struct {\n\tmetav1.TypeMeta `json:\",inline\"`\n\tmetav1.ListMeta `json:\"metadata,omitempty\"`\n\tItems           []LlamaDeployment `json:\"items\"`\n}\n\nfunc init() {\n\tSchemeBuilder.Register(&LlamaDeployment{}, &LlamaDeploymentList{})\n}\n"
  },
  {
    "path": "operator/api/v1/llamadeploymenttemplate_types.go",
    "content": "package v1\n\nimport (\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// LlamaDeploymentTemplateSpec defines the desired overlay for a LlamaDeployment's PodTemplate.\n// This is intended to carry scheduling-related fields like node selectors, tolerations, and\n// affinity, but supports any partial PodTemplateSpec. Fields set here will take precedence\n// over the operator-computed defaults when merged.\ntype LlamaDeploymentTemplateSpec struct {\n\t// PodSpec holds a partial PodTemplateSpec to be merged into the generated PodTemplate.\n\t// +optional\n\t// +kubebuilder:pruning:PreserveUnknownFields\n\t// +kubebuilder:validation:Schemaless\n\tPodSpec corev1.PodTemplateSpec `json:\"podSpec,omitempty\"`\n}\n\n// +kubebuilder:object:root=true\n// +kubebuilder:subresource:status\n// +kubebuilder:printcolumn:name=\"Age\",type=date,JSONPath=`.metadata.creationTimestamp`\n\n// LlamaDeploymentTemplate configures default Pod template fields for LlamaDeployments.\n// The resource name is referenced by LlamaDeployment.spec.templateName. A special name\n// \"default\" is used as a fallback when no templateName is provided.\ntype LlamaDeploymentTemplate struct {\n\tmetav1.TypeMeta   `json:\",inline\"`\n\tmetav1.ObjectMeta `json:\"metadata,omitempty\"`\n\n\tSpec   LlamaDeploymentTemplateSpec `json:\"spec,omitempty\"`\n\tStatus metav1.ConditionStatus      `json:\"status,omitempty\"`\n}\n\n// +kubebuilder:object:root=true\n\n// LlamaDeploymentTemplateList contains a list of LlamaDeploymentTemplate.\ntype LlamaDeploymentTemplateList struct {\n\tmetav1.TypeMeta `json:\",inline\"`\n\tmetav1.ListMeta `json:\"metadata,omitempty\"`\n\tItems           []LlamaDeploymentTemplate `json:\"items\"`\n}\n\nfunc init() {\n\tSchemeBuilder.Register(&LlamaDeploymentTemplate{}, &LlamaDeploymentTemplateList{})\n}\n"
  },
  {
    "path": "operator/api/v1/zz_generated.deepcopy.go",
    "content": "//go:build !ignore_autogenerated\n\n// Code generated by controller-gen. DO NOT EDIT.\n\npackage v1\n\nimport (\n\truntime \"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *LlamaDeployment) DeepCopyInto(out *LlamaDeployment) {\n\t*out = *in\n\tout.TypeMeta = in.TypeMeta\n\tin.ObjectMeta.DeepCopyInto(&out.ObjectMeta)\n\tout.Spec = in.Spec\n\tin.Status.DeepCopyInto(&out.Status)\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaDeployment.\nfunc (in *LlamaDeployment) DeepCopy() *LlamaDeployment {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(LlamaDeployment)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n\n// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.\nfunc (in *LlamaDeployment) DeepCopyObject() runtime.Object {\n\tif c := in.DeepCopy(); c != nil {\n\t\treturn c\n\t}\n\treturn nil\n}\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *LlamaDeploymentList) DeepCopyInto(out *LlamaDeploymentList) {\n\t*out = *in\n\tout.TypeMeta = in.TypeMeta\n\tin.ListMeta.DeepCopyInto(&out.ListMeta)\n\tif in.Items != nil {\n\t\tin, out := &in.Items, &out.Items\n\t\t*out = make([]LlamaDeployment, len(*in))\n\t\tfor i := range *in {\n\t\t\t(*in)[i].DeepCopyInto(&(*out)[i])\n\t\t}\n\t}\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaDeploymentList.\nfunc (in *LlamaDeploymentList) DeepCopy() *LlamaDeploymentList {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(LlamaDeploymentList)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n\n// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.\nfunc (in *LlamaDeploymentList) DeepCopyObject() runtime.Object {\n\tif c := in.DeepCopy(); c != nil {\n\t\treturn c\n\t}\n\treturn nil\n}\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *LlamaDeploymentSpec) DeepCopyInto(out *LlamaDeploymentSpec) {\n\t*out = *in\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaDeploymentSpec.\nfunc (in *LlamaDeploymentSpec) DeepCopy() *LlamaDeploymentSpec {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(LlamaDeploymentSpec)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *LlamaDeploymentStatus) DeepCopyInto(out *LlamaDeploymentStatus) {\n\t*out = *in\n\tif in.LastUpdated != nil {\n\t\tin, out := &in.LastUpdated, &out.LastUpdated\n\t\t*out = (*in).DeepCopy()\n\t}\n\tif in.ReleaseHistory != nil {\n\t\tin, out := &in.ReleaseHistory, &out.ReleaseHistory\n\t\t*out = make([]ReleaseHistoryEntry, len(*in))\n\t\tfor i := range *in {\n\t\t\t(*in)[i].DeepCopyInto(&(*out)[i])\n\t\t}\n\t}\n\tif in.RolloutStartedAt != nil {\n\t\tin, out := &in.RolloutStartedAt, &out.RolloutStartedAt\n\t\t*out = (*in).DeepCopy()\n\t}\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaDeploymentStatus.\nfunc (in *LlamaDeploymentStatus) DeepCopy() *LlamaDeploymentStatus {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(LlamaDeploymentStatus)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *LlamaDeploymentTemplate) DeepCopyInto(out *LlamaDeploymentTemplate) {\n\t*out = *in\n\tout.TypeMeta = in.TypeMeta\n\tin.ObjectMeta.DeepCopyInto(&out.ObjectMeta)\n\tin.Spec.DeepCopyInto(&out.Spec)\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaDeploymentTemplate.\nfunc (in *LlamaDeploymentTemplate) DeepCopy() *LlamaDeploymentTemplate {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(LlamaDeploymentTemplate)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n\n// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.\nfunc (in *LlamaDeploymentTemplate) DeepCopyObject() runtime.Object {\n\tif c := in.DeepCopy(); c != nil {\n\t\treturn c\n\t}\n\treturn nil\n}\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *LlamaDeploymentTemplateList) DeepCopyInto(out *LlamaDeploymentTemplateList) {\n\t*out = *in\n\tout.TypeMeta = in.TypeMeta\n\tin.ListMeta.DeepCopyInto(&out.ListMeta)\n\tif in.Items != nil {\n\t\tin, out := &in.Items, &out.Items\n\t\t*out = make([]LlamaDeploymentTemplate, len(*in))\n\t\tfor i := range *in {\n\t\t\t(*in)[i].DeepCopyInto(&(*out)[i])\n\t\t}\n\t}\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaDeploymentTemplateList.\nfunc (in *LlamaDeploymentTemplateList) DeepCopy() *LlamaDeploymentTemplateList {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(LlamaDeploymentTemplateList)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n\n// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.\nfunc (in *LlamaDeploymentTemplateList) DeepCopyObject() runtime.Object {\n\tif c := in.DeepCopy(); c != nil {\n\t\treturn c\n\t}\n\treturn nil\n}\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *LlamaDeploymentTemplateSpec) DeepCopyInto(out *LlamaDeploymentTemplateSpec) {\n\t*out = *in\n\tin.PodSpec.DeepCopyInto(&out.PodSpec)\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaDeploymentTemplateSpec.\nfunc (in *LlamaDeploymentTemplateSpec) DeepCopy() *LlamaDeploymentTemplateSpec {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(LlamaDeploymentTemplateSpec)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n\n// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\nfunc (in *ReleaseHistoryEntry) DeepCopyInto(out *ReleaseHistoryEntry) {\n\t*out = *in\n\tin.ReleasedAt.DeepCopyInto(&out.ReleasedAt)\n}\n\n// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReleaseHistoryEntry.\nfunc (in *ReleaseHistoryEntry) DeepCopy() *ReleaseHistoryEntry {\n\tif in == nil {\n\t\treturn nil\n\t}\n\tout := new(ReleaseHistoryEntry)\n\tin.DeepCopyInto(out)\n\treturn out\n}\n"
  },
  {
    "path": "operator/cmd/main.go",
    "content": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"flag\"\n\t\"net/http\"\n\t\"net/http/pprof\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)\n\t// to ensure that exec-entrypoint and run can make use of them.\n\t_ \"k8s.io/client-go/plugin/pkg/client/auth\"\n\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\tutilruntime \"k8s.io/apimachinery/pkg/util/runtime\"\n\tclientgoscheme \"k8s.io/client-go/kubernetes/scheme\"\n\tctrl \"sigs.k8s.io/controller-runtime\"\n\t\"sigs.k8s.io/controller-runtime/pkg/cache\"\n\t\"sigs.k8s.io/controller-runtime/pkg/certwatcher\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/healthz\"\n\t\"sigs.k8s.io/controller-runtime/pkg/log/zap\"\n\tctrlmetrics \"sigs.k8s.io/controller-runtime/pkg/metrics\"\n\t\"sigs.k8s.io/controller-runtime/pkg/metrics/filters\"\n\tmetricsserver \"sigs.k8s.io/controller-runtime/pkg/metrics/server\"\n\t\"sigs.k8s.io/controller-runtime/pkg/webhook\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n\t\"llama-agents-operator/internal/controller\"\n\t// +kubebuilder:scaffold:imports\n)\n\nvar (\n\tscheme   = runtime.NewScheme()\n\tsetupLog = ctrl.Log.WithName(\"setup\")\n)\n\ntype Config struct {\n\tMetricsAddr          string\n\tProbeAddr            string\n\tEnableLeaderElection bool\n\tSecureMetrics        bool\n\tEnableHTTP2          bool\n\n\t// Webhook certificate options\n\tWebhookCertPath string\n\tWebhookCertName string\n\tWebhookCertKey  string\n\n\t// Metrics certificate options\n\tMetricsCertPath string\n\tMetricsCertName string\n\tMetricsCertKey  string\n}\n\nfunc init() {\n\tutilruntime.Must(clientgoscheme.AddToScheme(scheme))\n\n\tutilruntime.Must(llamadeployv1.AddToScheme(scheme))\n\t// +kubebuilder:scaffold:scheme\n}\n\n// nolint:gocyclo\nfunc main() {\n\tvar config Config\n\tvar tlsOpts []func(*tls.Config)\n\n\t// Parse flags using standard flag package\n\tflag.StringVar(&config.MetricsAddr, \"metrics-bind-address\", \"0\", \"The address the metrics endpoint binds to. \"+\n\t\t\"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.\")\n\tflag.StringVar(&config.ProbeAddr, \"health-probe-bind-address\", \":8081\", \"The address the probe endpoint binds to.\")\n\tflag.BoolVar(&config.EnableLeaderElection, \"leader-elect\", false,\n\t\t\"Enable leader election for controller manager. \"+\n\t\t\t\"Enabling this will ensure there is only one active controller manager.\")\n\tflag.BoolVar(&config.SecureMetrics, \"metrics-secure\", true,\n\t\t\"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.\")\n\tflag.StringVar(&config.WebhookCertPath, \"webhook-cert-path\", \"\",\n\t\t\"The directory that contains the webhook certificate.\")\n\tflag.StringVar(&config.WebhookCertName, \"webhook-cert-name\", \"tls.crt\", \"The name of the webhook certificate file.\")\n\tflag.StringVar(&config.WebhookCertKey, \"webhook-cert-key\", \"tls.key\", \"The name of the webhook key file.\")\n\tflag.StringVar(&config.MetricsCertPath, \"metrics-cert-path\", \"\",\n\t\t\"The directory that contains the metrics server certificate.\")\n\tflag.StringVar(&config.MetricsCertName, \"metrics-cert-name\", \"tls.crt\",\n\t\t\"The name of the metrics server certificate file.\")\n\tflag.StringVar(&config.MetricsCertKey, \"metrics-cert-key\", \"tls.key\", \"The name of the metrics server key file.\")\n\tflag.BoolVar(&config.EnableHTTP2, \"enable-http2\", false,\n\t\t\"If set, HTTP/2 will be enabled for the metrics and webhook servers\")\n\n\topts := zap.Options{\n\t\tDevelopment: false,\n\t}\n\topts.BindFlags(flag.CommandLine)\n\tflag.Parse()\n\n\tctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))\n\n\t// if the enable-http2 flag is false (the default), http/2 should be disabled\n\t// due to its vulnerabilities. More specifically, disabling http/2 will\n\t// prevent from being vulnerable to the HTTP/2 Stream Cancellation and\n\t// Rapid Reset CVEs. For more information see:\n\t// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3\n\t// - https://github.com/advisories/GHSA-4374-p667-p6c8\n\tdisableHTTP2 := func(c *tls.Config) {\n\t\tsetupLog.Info(\"disabling http/2\")\n\t\tc.NextProtos = []string{\"http/1.1\"}\n\t}\n\n\tif !config.EnableHTTP2 {\n\t\ttlsOpts = append(tlsOpts, disableHTTP2)\n\t}\n\n\t// Create watchers for metrics and webhooks certificates\n\tvar metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher\n\n\t// Initial webhook TLS options\n\twebhookTLSOpts := tlsOpts\n\n\tif len(config.WebhookCertPath) > 0 {\n\t\tsetupLog.Info(\"Initializing webhook certificate watcher using provided certificates\",\n\t\t\t\"webhook-cert-path\", config.WebhookCertPath,\n\t\t\t\"webhook-cert-name\", config.WebhookCertName,\n\t\t\t\"webhook-cert-key\", config.WebhookCertKey)\n\n\t\tvar err error\n\t\twebhookCertWatcher, err = certwatcher.New(\n\t\t\tfilepath.Join(config.WebhookCertPath, config.WebhookCertName),\n\t\t\tfilepath.Join(config.WebhookCertPath, config.WebhookCertKey),\n\t\t)\n\t\tif err != nil {\n\t\t\tsetupLog.Error(err, \"Failed to initialize webhook certificate watcher\")\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\twebhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) {\n\t\t\tconfig.GetCertificate = webhookCertWatcher.GetCertificate\n\t\t})\n\t}\n\n\twebhookServer := webhook.NewServer(webhook.Options{\n\t\tTLSOpts: webhookTLSOpts,\n\t})\n\n\t// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.\n\t// More info:\n\t// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server\n\t// - https://book.kubebuilder.io/reference/metrics.html\n\tmetricsServerOptions := metricsserver.Options{\n\t\tBindAddress:   config.MetricsAddr,\n\t\tSecureServing: config.SecureMetrics,\n\t\tTLSOpts:       tlsOpts,\n\t}\n\n\tif config.SecureMetrics {\n\t\t// FilterProvider is used to protect the metrics endpoint with authn/authz.\n\t\t// These configurations ensure that only authorized users and service accounts\n\t\t// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:\n\t\t// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization\n\t\tmetricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization\n\t}\n\n\t// If the certificate is not specified, controller-runtime will automatically\n\t// generate self-signed certificates for the metrics server. While convenient for development and testing,\n\t// this setup is not recommended for production.\n\t//\n\t// TODO(user): If you enable certManager, uncomment the following lines:\n\t// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates\n\t// managed by cert-manager for the metrics server.\n\t// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.\n\tif len(config.MetricsCertPath) > 0 {\n\t\tsetupLog.Info(\"Initializing metrics certificate watcher using provided certificates\",\n\t\t\t\"metrics-cert-path\", config.MetricsCertPath,\n\t\t\t\"metrics-cert-name\", config.MetricsCertName,\n\t\t\t\"metrics-cert-key\", config.MetricsCertKey)\n\n\t\tvar err error\n\t\tmetricsCertWatcher, err = certwatcher.New(\n\t\t\tfilepath.Join(config.MetricsCertPath, config.MetricsCertName),\n\t\t\tfilepath.Join(config.MetricsCertPath, config.MetricsCertKey),\n\t\t)\n\t\tif err != nil {\n\t\t\tsetupLog.Error(err, \"to initialize metrics certificate watcher\", \"error\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tmetricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) {\n\t\t\tconfig.GetCertificate = metricsCertWatcher.GetCertificate\n\t\t})\n\t}\n\n\t// Get the namespace to watch from environment or default to current namespace\n\twatchNamespace := os.Getenv(\"WATCH_NAMESPACE\")\n\tif watchNamespace == \"\" {\n\t\t// Try to read the namespace from the service account mount\n\t\tif ns, err := os.ReadFile(\"/var/run/secrets/kubernetes.io/serviceaccount/namespace\"); err == nil {\n\t\t\twatchNamespace = string(ns)\n\t\t} else {\n\t\t\twatchNamespace = \"default\"\n\t\t}\n\t}\n\n\tmgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{\n\t\tScheme:                 scheme,\n\t\tMetrics:                metricsServerOptions,\n\t\tWebhookServer:          webhookServer,\n\t\tHealthProbeBindAddress: config.ProbeAddr,\n\t\tLeaderElection:         config.EnableLeaderElection,\n\t\tLeaderElectionID:       \"5095c351.deploy.llamaindex.ai\",\n\t\t// Limit the manager to watch only the specified namespace\n\t\tCache: cache.Options{\n\t\t\tDefaultNamespaces: map[string]cache.Config{\n\t\t\t\twatchNamespace: {},\n\t\t\t},\n\t\t},\n\t\t// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily\n\t\t// when the Manager ends. This requires the binary to immediately end when the\n\t\t// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly\n\t\t// speeds up voluntary leader transitions as the new leader don't have to wait\n\t\t// LeaseDuration time first.\n\t\t//\n\t\t// In the default scaffold provided, the program ends immediately after\n\t\t// the manager stops, so would be fine to enable this option. However,\n\t\t// if you are doing or is intended to do any operation such as perform cleanups\n\t\t// after the manager stops then its usage might be unsafe.\n\t\t// LeaderElectionReleaseOnCancel: true,\n\t})\n\tif err != nil {\n\t\tsetupLog.Error(err, \"unable to start manager\")\n\t\tos.Exit(1)\n\t}\n\n\t// Create a non-cached client for operations on types we don't want in the\n\t// informer cache (e.g. ReplicaSets).  With hundreds of Deployments the RS\n\t// informer alone can consume gigabytes of memory.\n\tdirectClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme})\n\tif err != nil {\n\t\tsetupLog.Error(err, \"unable to create direct API client\")\n\t\tos.Exit(1)\n\t}\n\n\t// Parse max concurrent rollouts from env var\n\tmaxConcurrentRollouts := 0\n\tif raw := os.Getenv(\"LLAMA_DEPLOY_MAX_CONCURRENT_ROLLOUTS\"); raw != \"\" {\n\t\tif v, err := strconv.Atoi(raw); err == nil && v >= 0 {\n\t\t\tmaxConcurrentRollouts = v\n\t\t\tsetupLog.Info(\"Max concurrent rollouts configured\", \"limit\", v)\n\t\t}\n\t}\n\n\t// Parse max deployments limit from env var\n\tmaxDeployments := 0\n\tif raw := os.Getenv(\"LLAMA_DEPLOY_MAX_DEPLOYMENTS\"); raw != \"\" {\n\t\tif v, err := strconv.Atoi(raw); err == nil && v >= 0 {\n\t\t\tmaxDeployments = v\n\t\t\tsetupLog.Info(\"Max deployments configured\", \"limit\", v)\n\t\t}\n\t}\n\n\t// Register phase metrics collector\n\tctrlmetrics.Registry.MustRegister(controller.NewPhaseCollector(mgr.GetClient()))\n\tctrlmetrics.Registry.MustRegister(controller.NewCapacityCollector(mgr.GetClient(), maxDeployments))\n\tif err := (&controller.LlamaDeploymentReconciler{\n\t\tClient:                mgr.GetClient(),\n\t\tScheme:                mgr.GetScheme(),\n\t\tRecorder:              mgr.GetEventRecorderFor(\"llamadeployment-controller\"),\n\t\tDirectClient:          directClient,\n\t\tMaxConcurrentRollouts: maxConcurrentRollouts,\n\t\tMaxDeployments:        maxDeployments,\n\t}).SetupWithManager(mgr); err != nil {\n\t\tsetupLog.Error(err, \"unable to create controller\", \"controller\", \"LlamaDeployment\")\n\t\tos.Exit(1)\n\t}\n\t// +kubebuilder:scaffold:builder\n\n\tif metricsCertWatcher != nil {\n\t\tsetupLog.Info(\"Adding metrics certificate watcher to manager\")\n\t\tif err := mgr.Add(metricsCertWatcher); err != nil {\n\t\t\tsetupLog.Error(err, \"unable to add metrics certificate watcher to manager\")\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tif webhookCertWatcher != nil {\n\t\tsetupLog.Info(\"Adding webhook certificate watcher to manager\")\n\t\tif err := mgr.Add(webhookCertWatcher); err != nil {\n\t\t\tsetupLog.Error(err, \"unable to add webhook certificate watcher to manager\")\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tif err := mgr.AddHealthzCheck(\"healthz\", healthz.Ping); err != nil {\n\t\tsetupLog.Error(err, \"unable to set up health check\")\n\t\tos.Exit(1)\n\t}\n\tif err := mgr.AddReadyzCheck(\"readyz\", healthz.Ping); err != nil {\n\t\tsetupLog.Error(err, \"unable to set up ready check\")\n\t\tos.Exit(1)\n\t}\n\n\t// Start pprof server for memory profiling.\n\t// Usage: kubectl port-forward <pod> 6060:6060\n\t//   curl -s http://localhost:6060/debug/pprof/heap > heap.prof\n\t//   go tool pprof heap.prof\n\tpprofMux := http.NewServeMux()\n\tpprofMux.HandleFunc(\"/debug/pprof/\", pprof.Index)\n\tpprofMux.HandleFunc(\"/debug/pprof/cmdline\", pprof.Cmdline)\n\tpprofMux.HandleFunc(\"/debug/pprof/profile\", pprof.Profile)\n\tpprofMux.HandleFunc(\"/debug/pprof/symbol\", pprof.Symbol)\n\tpprofMux.HandleFunc(\"/debug/pprof/trace\", pprof.Trace)\n\tgo func() {\n\t\tpprofAddr := \":6060\"\n\t\tsetupLog.Info(\"starting pprof server\", \"addr\", pprofAddr)\n\t\tif err := http.ListenAndServe(pprofAddr, pprofMux); err != nil {\n\t\t\tsetupLog.Error(err, \"pprof server failed\")\n\t\t}\n\t}()\n\n\tsetupLog.Info(\"starting manager\")\n\tif err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {\n\t\tsetupLog.Error(err, \"problem running manager\")\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "operator/config/crd/bases/deploy.llamaindex.ai_llamadeployments.yaml",
    "content": "---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  annotations:\n    controller-gen.kubebuilder.io/version: v0.18.0\n  name: llamadeployments.deploy.llamaindex.ai\nspec:\n  group: deploy.llamaindex.ai\n  names:\n    kind: LlamaDeployment\n    listKind: LlamaDeploymentList\n    plural: llamadeployments\n    singular: llamadeployment\n  scope: Namespaced\n  versions:\n  - additionalPrinterColumns:\n    - jsonPath: .spec.projectId\n      name: Project ID\n      type: string\n    - jsonPath: .spec.displayName\n      name: Name\n      type: string\n    - jsonPath: .spec.repoUrl\n      name: Repo\n      type: string\n    - jsonPath: .status.phase\n      name: Phase\n      type: string\n    - jsonPath: .metadata.creationTimestamp\n      name: Age\n      type: date\n    name: v1\n    schema:\n      openAPIV3Schema:\n        description: LlamaDeployment is the Schema for the llamadeployments API.\n        properties:\n          apiVersion:\n            description: |-\n              APIVersion defines the versioned schema of this representation of an object.\n              Servers should convert recognized schemas to the latest internal value, and\n              may reject unrecognized values.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\n            type: string\n          kind:\n            description: |-\n              Kind is a string value representing the REST resource this object represents.\n              Servers may infer this from the endpoint the client submits requests to.\n              Cannot be updated.\n              In CamelCase.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\n            type: string\n          metadata:\n            type: object\n          spec:\n            description: LlamaDeploymentSpec defines the desired state of LlamaDeployment.\n            properties:\n              buildGeneration:\n                description: |-\n                  BuildGeneration is a monotonically increasing counter that forces a new\n                  build when incremented, even if all other inputs (gitSha, imageTag, etc.)\n                  are unchanged. This allows retrying a failed build caused by transient\n                  errors (e.g. network failures) without requiring a new git commit.\n                format: int64\n                type: integer\n              deploymentFilePath:\n                default: llama_deployment.yml\n                description: DeploymentFilePath is the path to the deployment file\n                  within the repository\n                type: string\n              displayName:\n                description: DisplayName is the user-facing deployment label\n                type: string\n              gitRef:\n                description: GitRef is the git reference (commit SHA, branch, or tag)\n                  to deploy\n                type: string\n              gitSha:\n                description: A resolved git sha for the git ref\n                type: string\n              image:\n                description: |-\n                  Image is the container image registry and name (e.g., \"llamaindex/llama-agents-appserver\")\n                  If not specified, defaults to environment variable or \"llamaindex/llama-agents-appserver\"\n                type: string\n              imageTag:\n                description: |-\n                  ImageTag is the container image tag\n                  If not specified, defaults to environment variable or \"latest\"\n                type: string\n              name:\n                description: 'Name is the deployment name (DEPRECATED: use DisplayName)'\n                type: string\n              projectId:\n                description: ProjectId is the project ID\n                type: string\n              repoUrl:\n                description: RepoUrl is the URL of the repository to deploy\n                type: string\n              secretName:\n                description: SecretName is the name of the Kubernetes Secret containing\n                  PAT and deployment secrets\n                type: string\n              staticAssetsPath:\n                description: |-\n                  StaticAssetsPath is an optional path (relative to /opt/app) containing\n                  prebuilt UI assets to be served under /deployments/<deployment-id>/ui\n                type: string\n              suspended:\n                description: |-\n                  Suspended scales the underlying Deployment to 0 replicas when true.\n                  Setting suspended to false (or removing the field) restores replicas to 1.\n                type: boolean\n              templateName:\n                description: |-\n                  TemplateName optionally specifies a LlamaDeploymentTemplate to apply.\n                  When empty, the operator will look up a template named \"default\".\n                type: string\n            required:\n            - projectId\n            - repoUrl\n            type: object\n          status:\n            description: LlamaDeploymentStatus defines the observed state of LlamaDeployment.\n            properties:\n              authToken:\n                description: AuthToken is a cryptographically secure token for this\n                  deployment\n                type: string\n              buildId:\n                description: BuildId is the content-addressed identifier for the current\n                  build artifact\n                type: string\n              buildStatus:\n                description: BuildStatus tracks the state of the current build job\n                enum:\n                - Pending\n                - Running\n                - Succeeded\n                - Failed\n                type: string\n              failedRolloutGeneration:\n                description: |-\n                  FailedRolloutGeneration records the LlamaDeployment generation whose rollout\n                  timed out. This prevents the operator from re-attempting the same failing rollout.\n                format: int64\n                type: integer\n              lastBuiltGeneration:\n                description: |-\n                  LastBuiltGeneration is the spec.buildGeneration value that was last\n                  successfully built. When spec.buildGeneration differs from this value,\n                  a new build is triggered even if the deployment is suspended.\n                format: int64\n                type: integer\n              lastReconciledGeneration:\n                description: LastReconciledGeneration tracks the generation that was\n                  last successfully reconciled\n                format: int64\n                type: integer\n              lastUpdated:\n                description: LastUpdated is the timestamp of the last status update\n                format: date-time\n                type: string\n              message:\n                description: Message is a human-readable message indicating details\n                  about the current status\n                type: string\n              phase:\n                description: Phase represents the current phase of the deployment\n                enum:\n                - Pending\n                - Running\n                - Failed\n                - RollingOut\n                - RolloutFailed\n                - Suspended\n                - Building\n                - BuildFailed\n                - AwaitingCode\n                type: string\n              releaseHistory:\n                description: ReleaseHistory keeps the last 20 released git shas with\n                  timestamps\n                items:\n                  description: ReleaseHistoryEntry represents a single released version\n                    entry\n                  properties:\n                    gitSha:\n                      description: GitSha is the released git commit SHA\n                      type: string\n                    imageTag:\n                      description: ImageTag is the appserver image tag used for this\n                        release\n                      type: string\n                    releasedAt:\n                      description: ReleasedAt is the timestamp when this version was\n                        released\n                      format: date-time\n                      type: string\n                  required:\n                  - gitSha\n                  - releasedAt\n                  type: object\n                type: array\n              rolloutStartedAt:\n                description: |-\n                  RolloutStartedAt is the timestamp when the current rollout began.\n                  Set when the phase transitions to Pending or RollingOut, cleared on Running or failure.\n                format: date-time\n                type: string\n              schemaVersion:\n                description: SchemaVersion is the version of the CRD schema used when\n                  this resource was last reconciled\n                type: string\n              secretCheckRetries:\n                description: |-\n                  SecretCheckRetries tracks how many times we've retried finding the Secret.\n                  This handles informer cache lag when the Secret is created just before the CR.\n                format: int32\n                type: integer\n            type: object\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n"
  },
  {
    "path": "operator/config/crd/bases/deploy.llamaindex.ai_llamadeploymenttemplates.yaml",
    "content": "---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  annotations:\n    controller-gen.kubebuilder.io/version: v0.18.0\n  name: llamadeploymenttemplates.deploy.llamaindex.ai\nspec:\n  group: deploy.llamaindex.ai\n  names:\n    kind: LlamaDeploymentTemplate\n    listKind: LlamaDeploymentTemplateList\n    plural: llamadeploymenttemplates\n    singular: llamadeploymenttemplate\n  scope: Namespaced\n  versions:\n  - additionalPrinterColumns:\n    - jsonPath: .metadata.creationTimestamp\n      name: Age\n      type: date\n    name: v1\n    schema:\n      openAPIV3Schema:\n        description: |-\n          LlamaDeploymentTemplate configures default Pod template fields for LlamaDeployments.\n          The resource name is referenced by LlamaDeployment.spec.templateName. A special name\n          \"default\" is used as a fallback when no templateName is provided.\n        properties:\n          apiVersion:\n            description: |-\n              APIVersion defines the versioned schema of this representation of an object.\n              Servers should convert recognized schemas to the latest internal value, and\n              may reject unrecognized values.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\n            type: string\n          kind:\n            description: |-\n              Kind is a string value representing the REST resource this object represents.\n              Servers may infer this from the endpoint the client submits requests to.\n              Cannot be updated.\n              In CamelCase.\n              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\n            type: string\n          metadata:\n            type: object\n          spec:\n            description: |-\n              LlamaDeploymentTemplateSpec defines the desired overlay for a LlamaDeployment's PodTemplate.\n              This is intended to carry scheduling-related fields like node selectors, tolerations, and\n              affinity, but supports any partial PodTemplateSpec. Fields set here will take precedence\n              over the operator-computed defaults when merged.\n            properties:\n              podSpec:\n                description: PodSpec holds a partial PodTemplateSpec to be merged\n                  into the generated PodTemplate.\n                x-kubernetes-preserve-unknown-fields: true\n            type: object\n          status:\n            type: string\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n"
  },
  {
    "path": "operator/config/crd/kustomizeconfig.yaml",
    "content": "# This file is for teaching kustomize how to substitute name and namespace reference in CRD\nnameReference:\n- kind: Service\n  version: v1\n  fieldSpecs:\n  - kind: CustomResourceDefinition\n    version: v1\n    group: apiextensions.k8s.io\n    path: spec/conversion/webhook/clientConfig/service/name\n\nnamespace:\n- kind: CustomResourceDefinition\n  version: v1\n  group: apiextensions.k8s.io\n  path: spec/conversion/webhook/clientConfig/service/namespace\n  create: false\n\nvarReference:\n- path: metadata/annotations\n"
  },
  {
    "path": "operator/config/rbac/role.yaml",
    "content": "---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: manager-role\nrules:\n- apiGroups:\n  - \"\"\n  resources:\n  - configmaps\n  - secrets\n  - serviceaccounts\n  - services\n  verbs:\n  - create\n  - delete\n  - get\n  - list\n  - patch\n  - update\n  - watch\n- apiGroups:\n  - \"\"\n  resources:\n  - events\n  verbs:\n  - create\n  - get\n  - list\n  - patch\n  - watch\n- apiGroups:\n  - \"\"\n  resources:\n  - pods\n  verbs:\n  - get\n  - list\n  - watch\n- apiGroups:\n  - \"\"\n  resources:\n  - pods/log\n  verbs:\n  - get\n- apiGroups:\n  - apps\n  resources:\n  - deployments\n  verbs:\n  - create\n  - delete\n  - get\n  - list\n  - patch\n  - update\n  - watch\n- apiGroups:\n  - apps\n  resources:\n  - replicasets\n  verbs:\n  - get\n  - list\n  - patch\n  - update\n  - watch\n- apiGroups:\n  - batch\n  resources:\n  - jobs\n  verbs:\n  - create\n  - delete\n  - get\n  - list\n  - patch\n  - update\n  - watch\n- apiGroups:\n  - coordination.k8s.io\n  resources:\n  - leases\n  verbs:\n  - create\n  - delete\n  - get\n  - list\n  - patch\n  - update\n  - watch\n- apiGroups:\n  - deploy.llamaindex.ai\n  resources:\n  - llamadeployments\n  verbs:\n  - create\n  - delete\n  - get\n  - list\n  - patch\n  - update\n  - watch\n- apiGroups:\n  - deploy.llamaindex.ai\n  resources:\n  - llamadeployments/finalizers\n  verbs:\n  - update\n- apiGroups:\n  - deploy.llamaindex.ai\n  resources:\n  - llamadeployments/status\n  verbs:\n  - get\n  - patch\n  - update\n- apiGroups:\n  - deploy.llamaindex.ai\n  resources:\n  - llamadeploymenttemplates\n  verbs:\n  - get\n  - list\n  - watch\n- apiGroups:\n  - networking.k8s.io\n  resources:\n  - ingresses\n  - networkpolicies\n  verbs:\n  - create\n  - delete\n  - get\n  - list\n  - patch\n  - update\n  - watch\n"
  },
  {
    "path": "operator/dev.py",
    "content": "#!/usr/bin/env -S uv run --script\n# /// script\n# dependencies = [\"click\"]\n# ///\n\"\"\"\nLocal development environment for cloud_llama_deploy.\n\n  up     — create/ensure cluster and start tilt\n  down   — tear down tilt resources, retaining data\n  down --delete — also delete the kind cluster (kind target only)\n\nTargets:\n  kind            — (default) creates a kind cluster\n  docker-desktop  — uses Docker Desktop's built-in Kubernetes\n\"\"\"\n\nimport os\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom textwrap import dedent\n\nimport click\n\nPROJECT_ROOT = Path(__file__).parent.parent.absolute()\nNAMESPACE = \"llama-agents\"\nAPPS_NAMESPACE = \"llama-agents-apps\"\n\nTARGETS = (\"kind\", \"docker-desktop\")\nK8S_CONTEXTS = {\n    \"kind\": \"kind-kind\",\n    \"docker-desktop\": \"docker-desktop\",\n}\n\n\ndef run(\n    cmd: list[str], check: bool = True, capture: bool = False\n) -> subprocess.CompletedProcess:\n    result = subprocess.run(\n        cmd, check=False, capture_output=capture, text=True, cwd=PROJECT_ROOT\n    )\n    if check and result.returncode != 0:\n        if capture and result.stderr:\n            print(result.stderr, file=sys.stderr)\n        sys.exit(1)\n    return result\n\n\n# ---------------------------------------------------------------------------\n# kind helpers\n# ---------------------------------------------------------------------------\n\n\ndef kind_cluster_exists() -> bool:\n    result = run([\"kind\", \"get\", \"clusters\"], check=False, capture=True)\n    return \"kind\" in result.stdout\n\n\ndef ensure_kind_cluster() -> None:\n    if kind_cluster_exists():\n        result = run(\n            [\"kind\", \"export\", \"kubeconfig\", \"--name\", \"kind\"],\n            check=False,\n            capture=True,\n        )\n        if result.returncode == 0:\n            return\n        print(\"Cluster 'kind' exists but is broken, recreating...\")\n        run([\"kind\", \"delete\", \"cluster\", \"--name\", \"kind\"])\n\n    print(\"Creating kind cluster 'kind'...\")\n\n    kind_config = dedent(\"\"\"\\\n        kind: Cluster\n        apiVersion: kind.x-k8s.io/v1alpha4\n        nodes:\n        - role: control-plane\n          kubeadmConfigPatches:\n          - |\n            kind: InitConfiguration\n            nodeRegistration:\n              kubeletExtraArgs:\n                node-labels: \"ingress-ready=true\"\n          extraPortMappings:\n          - containerPort: 80\n            hostPort: 8090\n            protocol: TCP\n          - containerPort: 443\n            hostPort: 8444\n            protocol: TCP\n    \"\"\")\n\n    import tempfile\n\n    with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yaml\", delete=False) as f:\n        f.write(kind_config)\n        config_path = f.name\n\n    try:\n        run(\n            [\n                \"kind\",\n                \"create\",\n                \"cluster\",\n                \"--name\",\n                \"kind\",\n                \"--config\",\n                config_path,\n            ]\n        )\n    finally:\n        os.unlink(config_path)\n\n    install_ingress_controller(\"kind\")\n\n\n# ---------------------------------------------------------------------------\n# docker-desktop helpers\n# ---------------------------------------------------------------------------\n\n\ndef ensure_docker_desktop_cluster() -> None:\n    context = K8S_CONTEXTS[\"docker-desktop\"]\n    result = run(\n        [\"kubectl\", \"--context\", context, \"cluster-info\"],\n        check=False,\n        capture=True,\n    )\n    if result.returncode != 0:\n        print(\n            f\"Kubernetes context '{context}' is not reachable.\\n\"\n            \"Enable Kubernetes in Docker Desktop → Settings → Kubernetes.\",\n            file=sys.stderr,\n        )\n        sys.exit(1)\n\n    # Switch to the docker-desktop context\n    run([\"kubectl\", \"config\", \"use-context\", context])\n    install_ingress_controller(\"docker-desktop\")\n\n\n# ---------------------------------------------------------------------------\n# shared helpers\n# ---------------------------------------------------------------------------\n\nINGRESS_MANIFESTS = {\n    \"kind\": \"https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml\",\n    \"docker-desktop\": \"https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml\",\n}\n\n\ndef install_ingress_controller(target: str) -> None:\n    result = run(\n        [\"kubectl\", \"get\", \"namespace\", \"ingress-nginx\"], check=False, capture=True\n    )\n    if result.returncode == 0:\n        return\n\n    print(\"Installing nginx ingress controller...\")\n    run([\"kubectl\", \"apply\", \"-f\", INGRESS_MANIFESTS[target]])\n\n    import time\n\n    start = time.time()\n    while time.time() - start < 10:\n        result = run(\n            [\n                \"kubectl\",\n                \"get\",\n                \"pods\",\n                \"--namespace\",\n                \"ingress-nginx\",\n                \"--selector=app.kubernetes.io/component=controller\",\n                \"--no-headers\",\n            ],\n            check=False,\n            capture=True,\n        )\n        if result.returncode == 0 and result.stdout.strip():\n            break\n        time.sleep(1)\n\n    run(\n        [\n            \"kubectl\",\n            \"wait\",\n            \"--namespace\",\n            \"ingress-nginx\",\n            \"--for=condition=ready\",\n            \"pod\",\n            \"--selector=app.kubernetes.io/component=controller\",\n            \"--timeout=300s\",\n        ]\n    )\n\n\ntarget_option = click.option(\n    \"--target\",\n    type=click.Choice(TARGETS),\n    default=None,\n    envvar=\"DEV_TARGET\",\n    help=\"Kubernetes target cluster. [default: kind]\",\n)\n\napps_ns_option = click.option(\n    \"--apps-namespace\",\n    \"apps_namespace\",\n    is_flag=False,\n    flag_value=APPS_NAMESPACE,\n    default=None,\n    envvar=\"DEV_APPS_NAMESPACE\",\n    help=(\n        \"Run in split-namespace mode: put LlamaDeployment CRs and app \"\n        f\"resources in this namespace (default: {APPS_NAMESPACE}). \"\n        \"Unset = single-namespace mode.\"\n    ),\n)\n\n\ndef resolve_target(ctx: click.Context, target: str | None) -> str:\n    return target or ctx.obj.get(\"target\") or \"kind\"\n\n\ndef resolve_apps_namespace(\n    ctx: click.Context, apps_namespace: str | None\n) -> str | None:\n    return apps_namespace or ctx.obj.get(\"apps_namespace\")\n\n\n@click.group()\n@target_option\n@apps_ns_option\n@click.pass_context\ndef cli(ctx: click.Context, target: str | None, apps_namespace: str | None) -> None:\n    \"\"\"Local development environment for cloud_llama_deploy.\"\"\"\n    ctx.ensure_object(dict)\n    ctx.obj[\"target\"] = target\n    ctx.obj[\"apps_namespace\"] = apps_namespace\n\n\n@cli.command()\n@target_option\n@apps_ns_option\n@click.pass_context\ndef up(ctx: click.Context, target: str | None, apps_namespace: str | None) -> None:\n    \"\"\"Create/ensure cluster and start tilt.\"\"\"\n    target = resolve_target(ctx, target)\n    apps_namespace = resolve_apps_namespace(ctx, apps_namespace)\n\n    # Check required tools\n    version_cmds: dict[str, list[str]] = {\n        \"kubectl\": [\"kubectl\", \"version\", \"--client\"],\n        \"docker\": [\"docker\", \"--version\"],\n        \"tilt\": [\"tilt\", \"version\"],\n    }\n    if target == \"kind\":\n        version_cmds[\"kind\"] = [\"kind\", \"--version\"]\n\n    for tool, cmd in version_cmds.items():\n        if run(cmd, check=False, capture=True).returncode != 0:\n            print(f\"Missing required tool: {tool}\", file=sys.stderr)\n            sys.exit(1)\n\n    if target == \"kind\":\n        ensure_kind_cluster()\n    else:\n        ensure_docker_desktop_cluster()\n\n    # Ensure namespaces exist\n    for ns in [NAMESPACE] + ([apps_namespace] if apps_namespace else []):\n        result = run([\"kubectl\", \"get\", \"namespace\", ns], check=False, capture=True)\n        if result.returncode != 0:\n            run([\"kubectl\", \"create\", \"namespace\", ns])\n\n    if not (PROJECT_ROOT / \".env\").exists():\n        print(\n            \"Note: no .env file found. GitHub integration requires GITHUB_APP_PRIVATE_KEY, GITHUB_APP_CLIENT_ID, GITHUB_APP_NAME, GITHUB_APP_SECRET.\"\n        )\n\n    ingress_port = \"8090\" if target == \"kind\" else \"80\"\n    print(\"Starting tilt...\")\n    print(\"  API:     http://localhost:8011\")\n    print(\"  Tilt UI: http://localhost:10350\")\n    print(f\"  Ingress: *.127.0.0.1.nip.io:{ingress_port}\")\n    if apps_namespace:\n        print(f\"  Apps namespace: {apps_namespace}\")\n    tilt_args = [\n        \"tilt\",\n        \"up\",\n        \"-f\",\n        str(PROJECT_ROOT / \"operator\" / \"Tiltfile\"),\n        \"--\",\n        target,\n    ]\n    if apps_namespace:\n        tilt_args += [\"--apps-namespace\", apps_namespace]\n    os.execvp(\"tilt\", tilt_args)\n\n\n@cli.command()\n@click.option(\"--delete\", is_flag=True, help=\"Also delete the kind cluster\")\n@target_option\n@apps_ns_option\n@click.pass_context\ndef down(\n    ctx: click.Context,\n    delete: bool,\n    target: str | None,\n    apps_namespace: str | None,\n) -> None:\n    \"\"\"Tear down tilt resources. Use --delete to also remove the cluster (kind only).\"\"\"\n    target = resolve_target(ctx, target)\n    apps_namespace = resolve_apps_namespace(ctx, apps_namespace)\n\n    tilt_args = [\n        \"tilt\",\n        \"down\",\n        \"-f\",\n        str(PROJECT_ROOT / \"operator\" / \"Tiltfile\"),\n        \"--\",\n        target,\n    ]\n    if apps_namespace:\n        tilt_args += [\"--apps-namespace\", apps_namespace]\n    run(tilt_args, check=False)\n\n    if delete:\n        if target != \"kind\":\n            print(\"--delete only applies to the kind target\", file=sys.stderr)\n            return\n        if kind_cluster_exists():\n            print(\"Deleting kind cluster 'kind'...\")\n            run([\"kind\", \"delete\", \"cluster\", \"--name\", \"kind\"])\n\n\n@cli.command()\n@target_option\n@apps_ns_option\n@click.pass_context\ndef status(ctx: click.Context, target: str | None, apps_namespace: str | None) -> None:\n    \"\"\"Show cluster and deployment status.\"\"\"\n    target = resolve_target(ctx, target)\n    apps_namespace = resolve_apps_namespace(ctx, apps_namespace)\n    context = K8S_CONTEXTS[target]\n\n    if target == \"kind\":\n        if not kind_cluster_exists():\n            print(\"No kind cluster 'kind' found\")\n            return\n        run([\"kind\", \"export\", \"kubeconfig\", \"--name\", \"kind\"])\n    else:\n        result = run(\n            [\"kubectl\", \"--context\", context, \"cluster-info\"],\n            check=False,\n            capture=True,\n        )\n        if result.returncode != 0:\n            print(f\"Kubernetes context '{context}' is not reachable\")\n            return\n        run([\"kubectl\", \"config\", \"use-context\", context])\n\n    run([\"kubectl\", \"get\", \"pods\", \"-n\", NAMESPACE], check=False)\n    if apps_namespace and apps_namespace != NAMESPACE:\n        print()\n        run([\"kubectl\", \"get\", \"pods\", \"-n\", apps_namespace], check=False)\n\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "operator/go.mod",
    "content": "module llama-agents-operator\n\ngo 1.24.0\n\nrequire (\n\tgithub.com/onsi/ginkgo/v2 v2.22.0\n\tgithub.com/onsi/gomega v1.36.1\n\tgithub.com/prometheus/client_golang v1.22.0\n\tgithub.com/prometheus/client_model v0.6.1\n\tk8s.io/api v0.33.0\n\tk8s.io/apimachinery v0.33.0\n\tk8s.io/client-go v0.33.0\n\tsigs.k8s.io/controller-runtime v0.21.0\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/blang/semver/v4 v4.0.0 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.11.0 // indirect\n\tgithub.com/evanphx/json-patch/v5 v5.9.11 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.7.0 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.7.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-logr/zapr v1.3.0 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.2 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-task/slim-sprig/v3 v3.0.0 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/google/cel-go v0.23.2 // indirect\n\tgithub.com/google/gnostic-models v0.6.9 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/prometheus/common v0.62.0 // indirect\n\tgithub.com/prometheus/procfs v0.15.1 // indirect\n\tgithub.com/spf13/cobra v1.8.1 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/stoewer/go-strcase v1.3.0 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect\n\tgo.opentelemetry.io/otel v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.41.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.4.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap v1.27.0 // indirect\n\tgolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect\n\tgolang.org/x/net v0.48.0 // indirect\n\tgolang.org/x/oauth2 v0.34.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.40.0 // indirect\n\tgolang.org/x/term v0.38.0 // indirect\n\tgolang.org/x/text v0.32.0 // indirect\n\tgolang.org/x/time v0.9.0 // indirect\n\tgolang.org/x/tools v0.39.0 // indirect\n\tgomodules.xyz/jsonpatch/v2 v2.4.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/apiextensions-apiserver v0.33.0 // indirect\n\tk8s.io/apiserver v0.33.0 // indirect\n\tk8s.io/component-base v0.33.0 // indirect\n\tk8s.io/klog/v2 v2.130.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect\n\tk8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect\n\tsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect\n\tsigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect\n\tsigs.k8s.io/yaml v1.4.0 // indirect\n)\n"
  },
  {
    "path": "operator/go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ngithub.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=\ngithub.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=\ngithub.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=\ngithub.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=\ngithub.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=\ngithub.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=\ngithub.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=\ngithub.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=\ngithub.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=\ngithub.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=\ngithub.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=\ngithub.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4=\ngithub.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo=\ngithub.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=\ngithub.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=\ngithub.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=\ngithub.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=\ngithub.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=\ngithub.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=\ngithub.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=\ngithub.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=\ngithub.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=\ngithub.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=\ngithub.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=\ngithub.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=\ngithub.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=\ngithub.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=\ngithub.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngo.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=\ngo.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=\ngolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=\ngolang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=\ngolang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=\ngolang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=\ngolang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=\ngolang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=\ngolang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=\ngolang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=\ngolang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=\ngomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=\ngopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nk8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=\nk8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=\nk8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=\nk8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=\nk8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=\nk8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=\nk8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc=\nk8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8=\nk8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=\nk8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=\nk8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk=\nk8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU=\nk8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=\nk8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=\nk8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=\nk8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=\nk8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=\nsigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=\nsigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=\nsigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=\nsigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=\nsigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=\nsigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=\nsigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=\nsigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=\n"
  },
  {
    "path": "operator/internal/controller/apps_namespace_test.go",
    "content": "//go:build integration\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t. \"github.com/onsi/ginkgo/v2\"\n\t. \"github.com/onsi/gomega\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/errors\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\t\"sigs.k8s.io/controller-runtime/pkg/reconcile\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\n// Verifies that the reconciler produces child resources in whatever\n// namespace the LlamaDeployment CR lives in — the \"apps namespace\" mode\n// from the Helm chart. Cross-namespace owner references are illegal in\n// Kubernetes, so co-locating the CR with its children keeps the existing\n// ownership model unchanged. This test proves that assumption holds in\n// practice by exercising a non-default namespace end-to-end.\nvar _ = Describe(\"LlamaDeployment apps namespace\", func() {\n\tconst (\n\t\tappsNamespace = \"llama-agents-apps\"\n\t\tprojectID     = \"apps-ns-project\"\n\t\trepoURL       = \"https://github.com/test/apps-ns.git\"\n\t\tgitRef        = \"abc123\"\n\t)\n\n\tvar (\n\t\tctx          context.Context\n\t\treconciler   *LlamaDeploymentReconciler\n\t\tnsCreatedKey = \"created-by-apps-ns-test\"\n\t)\n\n\tBeforeEach(func() {\n\t\tctx = context.Background()\n\t\treconciler = NewTestReconciler()\n\n\t\tns := &corev1.Namespace{\n\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\tName:   appsNamespace,\n\t\t\t\tLabels: map[string]string{nsCreatedKey: \"true\"},\n\t\t\t},\n\t\t}\n\t\terr := k8sClient.Create(ctx, ns)\n\t\tif err != nil && !errors.IsAlreadyExists(err) {\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t}\n\t})\n\n\tIt(\"reconciles a LlamaDeployment in an alternate namespace\", func() {\n\t\tname := \"ld-in-apps-ns\"\n\n\t\tld := NewLlama(name, appsNamespace, projectID, repoURL, WithGitRef(gitRef))\n\t\tCreateAndReconcile(ctx, reconciler, ld)\n\t\tdefer CleanupLlama(ctx, name, appsNamespace)\n\t\tCompleteBuild(ctx, reconciler, ld)\n\n\t\tBy(\"creating a Deployment in the apps namespace\")\n\t\tdep := GetDeploymentEventually(ctx, name, appsNamespace)\n\t\tExpect(dep.Namespace).To(Equal(appsNamespace))\n\n\t\tBy(\"creating a ServiceAccount in the apps namespace\")\n\t\tsa := &corev1.ServiceAccount{}\n\t\tEventually(func() error {\n\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: name + \"-sa\", Namespace: appsNamespace}, sa)\n\t\t}).Should(Succeed())\n\t\tExpect(sa.Namespace).To(Equal(appsNamespace))\n\n\t\tBy(\"creating a Service in the apps namespace\")\n\t\tExpectServicePort(ctx, name, appsNamespace, 80, 8081)\n\n\t\tBy(\"preserving the ownerReference so GC works on CR deletion\")\n\t\tExpect(dep.OwnerReferences).NotTo(BeEmpty(), \"Deployment should have an ownerReference to the CR\")\n\t\tExpect(dep.OwnerReferences[0].Name).To(Equal(name))\n\n\t\tBy(\"not creating any child resources in the default namespace\")\n\t\tdefaultDep := &appsv1.Deployment{}\n\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: \"default\"}, defaultDep)\n\t\tExpect(errors.IsNotFound(err)).To(BeTrue(), \"no Deployment should leak into the default namespace\")\n\t})\n\n\tIt(\"cleans up child resources when the CR is deleted in the apps namespace\", func() {\n\t\tname := \"ld-cleanup-apps-ns\"\n\n\t\tld := NewLlama(name, appsNamespace, projectID, repoURL, WithGitRef(gitRef))\n\t\tCreateAndReconcile(ctx, reconciler, ld)\n\t\tCompleteBuild(ctx, reconciler, ld)\n\n\t\t_ = GetDeploymentEventually(ctx, name, appsNamespace)\n\n\t\tBy(\"deleting the CR and running one more reconcile to process finalizers\")\n\t\texisting := &llamadeployv1.LlamaDeployment{}\n\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: appsNamespace}, existing)).To(Succeed())\n\t\tif len(existing.Finalizers) > 0 {\n\t\t\texisting.Finalizers = []string{}\n\t\t\tExpect(k8sClient.Update(ctx, existing)).To(Succeed())\n\t\t}\n\t\tExpect(k8sClient.Delete(ctx, existing)).To(Succeed())\n\n\t\t_, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: appsNamespace}})\n\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\tBy(\"confirming the CR is gone\")\n\t\tEventually(func() bool {\n\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: appsNamespace}, existing)\n\t\t\treturn errors.IsNotFound(err)\n\t\t}, \"5s\", \"100ms\").Should(BeTrue(), fmt.Sprintf(\"CR %s/%s should be deleted\", appsNamespace, name))\n\t})\n})\n"
  },
  {
    "path": "operator/internal/controller/classify_pod_test.go",
    "content": "package controller\n\nimport (\n\t\"testing\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n)\n\nfunc TestClassifyPod(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpod  *corev1.Pod\n\t\twant failureType\n\t}{\n\t\t{\n\t\t\tname: \"evicted pod returns failureInfra\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tReason: \"Evicted\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureInfra,\n\t\t},\n\t\t{\n\t\t\tname: \"pending pod with no container statuses returns failureInfra\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tPhase: corev1.PodPending,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureInfra,\n\t\t},\n\t\t{\n\t\t\tname: \"container in CrashLoopBackOff returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &corev1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: \"CrashLoopBackOff\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t\t{\n\t\t\tname: \"container in ImagePullBackOff returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &corev1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: \"ImagePullBackOff\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t\t{\n\t\t\tname: \"container in ErrImagePull returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &corev1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: \"ErrImagePull\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t\t{\n\t\t\tname: \"container in CreateContainerConfigError returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &corev1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: \"CreateContainerConfigError\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t\t{\n\t\t\tname: \"container terminated with non-zero exit code returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &corev1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t\t{\n\t\t\tname: \"container OOMKilled with non-zero exit returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &corev1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 137,\n\t\t\t\t\t\t\t\t\tReason:   \"OOMKilled\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t\t{\n\t\t\tname: \"init container terminated with non-zero exit code returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tInitContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &corev1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t\t{\n\t\t\tname: \"running pod with no issues returns failureUnknown\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tPhase: corev1.PodRunning,\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &corev1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureUnknown,\n\t\t},\n\t\t{\n\t\t\tname: \"container with LastTerminationState non-zero exit returns failureApp\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &corev1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLastTerminationState: corev1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &corev1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: failureApp,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := classifyPod(tt.pod)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"classifyPod() = %v (%d), want %v (%d)\", got, got, tt.want, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "operator/internal/controller/image_tag_unit_test.go",
    "content": "//go:build !integration\n\npackage controller\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nfunc TestGetContainerImageTag_Precedence_SpecOverridesEnv(t *testing.T) {\n\tprev := os.Getenv(EnvImageTag)\n\tt.Cleanup(func() { _ = os.Setenv(EnvImageTag, prev) })\n\n\tconst specTag = \"spec-tag\"\n\n\t// When both spec and env are set, spec should win (per-deployment pinning)\n\t_ = os.Setenv(EnvImageTag, \"env-tag\")\n\tld := &llamadeployv1.LlamaDeployment{ObjectMeta: metav1.ObjectMeta{Name: \"demo\"}}\n\tld.Spec.ImageTag = specTag\n\tif got := getContainerImageTag(ld); got != specTag {\n\t\tt.Fatalf(\"expected spec tag to win, got %q\", got)\n\t}\n\n\t// When spec is set and env is empty, spec should be used\n\t_ = os.Unsetenv(EnvImageTag)\n\tld.Spec.ImageTag = \"spec-only\"\n\tif got := getContainerImageTag(ld); got != \"spec-only\" {\n\t\tt.Fatalf(\"expected spec tag when env unset, got %q\", got)\n\t}\n\n\t// When env is set and spec empty, env should be used (fallback)\n\t_ = os.Setenv(EnvImageTag, \"env-only\")\n\tld.Spec.ImageTag = \"\"\n\tif got := getContainerImageTag(ld); got != \"env-only\" {\n\t\tt.Fatalf(\"expected env tag when spec empty, got %q\", got)\n\t}\n\n\t// When both unset, default should be used\n\t_ = os.Unsetenv(EnvImageTag)\n\tld.Spec.ImageTag = \"\"\n\tif got := getContainerImageTag(ld); got != DefaultImageTag {\n\t\tt.Fatalf(\"expected default tag %q, got %q\", DefaultImageTag, got)\n\t}\n\n\t// Legacy \"appserver-\" prefix should be stripped from spec tag\n\tld.Spec.ImageTag = \"appserver-0.7.2\"\n\tif got := getContainerImageTag(ld); got != \"0.7.2\" {\n\t\tt.Fatalf(\"expected appserver- prefix to be stripped, got %q\", got)\n\t}\n\n\t// Plain version spec tag should pass through unchanged\n\tld.Spec.ImageTag = \"0.9.3\"\n\tif got := getContainerImageTag(ld); got != \"0.9.3\" {\n\t\tt.Fatalf(\"expected plain version tag, got %q\", got)\n\t}\n}\n"
  },
  {
    "path": "operator/internal/controller/lifecycle.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\tmathrand \"math/rand\"\n\t\"strconv\"\n\t\"time\"\n\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/errors\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\tctrl \"sigs.k8s.io/controller-runtime\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/log\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\n// isFailedPhase returns true for phases where failure remediation may be needed.\nfunc isFailedPhase(phase string) bool {\n\treturn phase == PhaseRolloutFailed || phase == PhaseFailed\n}\n\n// isAwaitingCodePush returns true when the deployment has no code source\n// configured and is waiting for an initial git push.\nfunc isAwaitingCodePush(ld *llamadeployv1.LlamaDeployment) bool {\n\treturn ld.Spec.RepoUrl == \"\"\n}\n\n// isRollingPhase returns true for phases that consume a rollout slot.\nfunc isRollingPhase(phase string) bool {\n\treturn phase == PhasePending || phase == PhaseRollingOut || phase == PhaseBuilding\n}\n\n// checkRolloutCapacity returns (true, result, nil) when the reconcile should be\n// requeued because the max concurrent rollout limit has been reached.\nfunc (r *LlamaDeploymentReconciler) checkRolloutCapacity(ctx context.Context, current *llamadeployv1.LlamaDeployment) (bool, ctrl.Result, error) {\n\tif r.MaxConcurrentRollouts <= 0 {\n\t\treturn false, ctrl.Result{}, nil\n\t}\n\n\tlogger := log.FromContext(ctx)\n\n\tvar list llamadeployv1.LlamaDeploymentList\n\tif err := r.List(ctx, &list, &client.ListOptions{Namespace: current.Namespace}); err != nil {\n\t\treturn false, ctrl.Result{}, fmt.Errorf(\"failed to list LlamaDeployments for rollout capacity check: %w\", err)\n\t}\n\n\trolling := 0\n\tfor i := range list.Items {\n\t\tld := &list.Items[i]\n\t\tif ld.Name == current.Name {\n\t\t\tcontinue\n\t\t}\n\t\t// Only count non-suspended deployments toward the rollout limit.\n\t\t// Suspended deployments may transiently remain in Pending/Building\n\t\t// phase until the next reconcile updates them to Suspended.\n\t\tif ld.Spec.Suspended {\n\t\t\tcontinue\n\t\t}\n\t\tif isRollingPhase(ld.Status.Phase) {\n\t\t\trolling++\n\t\t}\n\t}\n\n\tif rolling >= r.MaxConcurrentRollouts {\n\t\t// Jittered requeue: 10-20 seconds\n\t\tjitter := time.Duration(10+mathrand.Intn(11)) * time.Second\n\t\tlogger.Info(\"Max concurrent rollouts reached, requeuing\",\n\t\t\t\"limit\", r.MaxConcurrentRollouts,\n\t\t\t\"inProgress\", rolling,\n\t\t\t\"requeueAfter\", jitter)\n\t\treturn true, ctrl.Result{RequeueAfter: jitter}, nil\n\t}\n\n\treturn false, ctrl.Result{}, nil\n}\n\n// checkDeploymentCapacity returns (true, result, nil) when the reconcile should\n// be requeued because the max deployments limit has been reached.\nfunc (r *LlamaDeploymentReconciler) checkDeploymentCapacity(ctx context.Context, current *llamadeployv1.LlamaDeployment) (bool, ctrl.Result, error) {\n\tif r.MaxDeployments <= 0 {\n\t\treturn false, ctrl.Result{}, nil\n\t}\n\n\tlogger := log.FromContext(ctx)\n\n\tvar list llamadeployv1.LlamaDeploymentList\n\tif err := r.List(ctx, &list, &client.ListOptions{Namespace: current.Namespace}); err != nil {\n\t\treturn false, ctrl.Result{}, fmt.Errorf(\"failed to list LlamaDeployments for deployment capacity check: %w\", err)\n\t}\n\n\tactive := 0\n\tfor i := range list.Items {\n\t\tld := &list.Items[i]\n\t\tif ld.Name == current.Name {\n\t\t\tcontinue\n\t\t}\n\t\tif isActivePhase(ld.Status.Phase) {\n\t\t\tactive++\n\t\t}\n\t}\n\n\tif active >= r.MaxDeployments {\n\t\tlogger.Info(\"Max deployments limit reached, requeuing\",\n\t\t\t\"limit\", r.MaxDeployments,\n\t\t\t\"active\", active,\n\t\t\t\"requeueAfter\", 5*time.Minute)\n\t\treturn true, ctrl.Result{RequeueAfter: 5 * time.Minute}, nil\n\t}\n\n\treturn false, ctrl.Result{}, nil\n}\n\n// checkCapacityGates short-circuits the reconcile when deployment or rollout\n// capacity limits are reached. Returns (*result, nil) to requeue, or (nil, nil)\n// to continue. Must be called before initializeStatus so gated deployments\n// stay in their previous phase.\nfunc (r *LlamaDeploymentReconciler) checkCapacityGates(ctx context.Context, ld *llamadeployv1.LlamaDeployment, needsFullReconcile bool) (*ctrl.Result, error) {\n\tif !needsFullReconcile || ld.Spec.Suspended || ld.Status.Phase == PhaseAwaitingCode {\n\t\treturn nil, nil\n\t}\n\tif requeue, result, err := r.checkDeploymentCapacity(ctx, ld); err != nil {\n\t\treturn nil, err\n\t} else if requeue {\n\t\treturn &result, nil\n\t}\n\tif requeue, result, err := r.checkRolloutCapacity(ctx, ld); err != nil {\n\t\treturn nil, err\n\t} else if requeue {\n\t\treturn &result, nil\n\t}\n\treturn nil, nil\n}\n\n// checkSecretGate verifies that a referenced Secret exists. Returns (*result, nil)\n// to short-circuit the reconcile, or (nil, nil) to continue.\nfunc (r *LlamaDeploymentReconciler) checkSecretGate(ctx context.Context, ld *llamadeployv1.LlamaDeployment) (*ctrl.Result, error) {\n\tif ld.Spec.SecretName == \"\" {\n\t\treturn nil, nil\n\t}\n\tif done, result, err := r.checkSecretExists(ctx, ld); done {\n\t\treturn &result, err\n\t}\n\treturn nil, nil\n}\n\n// checkSecretExists verifies the referenced Secret exists, retrying a few times\n// to handle informer cache lag. Returns (done, result, err) where done=true means\n// the caller should return result/err immediately.\nfunc (r *LlamaDeploymentReconciler) checkSecretExists(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) (bool, ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\n\tsecret := &corev1.Secret{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: llamaDeploy.Spec.SecretName, Namespace: llamaDeploy.Namespace}, secret); err != nil {\n\t\tif errors.IsNotFound(err) {\n\t\t\tsecretRetries := llamaDeploy.Status.SecretCheckRetries\n\t\t\tif secretRetries < 3 {\n\t\t\t\tllamaDeploy.Status.SecretCheckRetries = secretRetries + 1\n\t\t\t\tif statusErr := r.Status().Update(ctx, llamaDeploy); statusErr != nil {\n\t\t\t\t\tlogger.Error(statusErr, \"Failed to update secret check retry count\")\n\t\t\t\t}\n\t\t\t\tlogger.Info(\"Secret not found, will retry\",\n\t\t\t\t\t\"secret\", llamaDeploy.Spec.SecretName,\n\t\t\t\t\t\"retry\", secretRetries+1)\n\t\t\t\treturn true, ctrl.Result{RequeueAfter: 5 * time.Second}, nil\n\t\t\t}\n\t\t\tmessage := fmt.Sprintf(\"Secret %s not found after retries - rollout failed. Create the Secret and update the resource to retry.\", llamaDeploy.Spec.SecretName)\n\t\t\tif llamaDeploy.Status.Phase != PhaseRolloutFailed || llamaDeploy.Status.Message != message {\n\t\t\t\tllamaDeploy.Status.Phase = PhaseRolloutFailed\n\t\t\t\tllamaDeploy.Status.Message = message\n\t\t\t\tnow := metav1.Now()\n\t\t\t\tllamaDeploy.Status.LastUpdated = &now\n\t\t\t\tif statusErr := r.Status().Update(ctx, llamaDeploy); statusErr != nil {\n\t\t\t\t\tlogger.Error(statusErr, \"Failed to update status for missing Secret\")\n\t\t\t\t}\n\t\t\t\tif r.Recorder != nil {\n\t\t\t\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeWarning, PhaseRolloutFailed, message)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true, ctrl.Result{}, nil\n\t\t}\n\t\t// Any other error fetching the Secret is retriable\n\t\treturn true, ctrl.Result{}, err\n\t}\n\t// Reset retry counter on success\n\tif llamaDeploy.Status.SecretCheckRetries > 0 {\n\t\tllamaDeploy.Status.SecretCheckRetries = 0\n\t\tif statusErr := r.Status().Update(ctx, llamaDeploy); statusErr != nil {\n\t\t\tlogger.Error(statusErr, \"Failed to reset secret check retry count\")\n\t\t}\n\t}\n\treturn false, ctrl.Result{}, nil\n}\n\n// assessDeploymentHealth determines the deployment phase, tracks rollout timing,\n// and checks for rollout timeouts. Returns the assessed phase, status message,\n// requeue duration, and whether status fields were mutated (RolloutStartedAt).\nfunc (r *LlamaDeploymentReconciler) assessDeploymentHealth(ctx context.Context, ld *llamadeployv1.LlamaDeployment) (phase string, message string, requeueAfter time.Duration, statusDirty bool, err error) {\n\tlogger := log.FromContext(ctx)\n\n\tphase, message, err = r.determineDeploymentPhase(ctx, ld)\n\tif err != nil {\n\t\tlogger.Error(err, \"Failed to determine deployment phase\")\n\t\treturn \"\", \"\", 0, false, err\n\t}\n\n\t// Track rollout start time\n\tif phase == PhasePending || phase == PhaseRollingOut {\n\t\tif ld.Status.RolloutStartedAt == nil {\n\t\t\tnow := metav1.Now()\n\t\t\tld.Status.RolloutStartedAt = &now\n\t\t\tstatusDirty = true\n\t\t}\n\t} else if ld.Status.RolloutStartedAt != nil {\n\t\tld.Status.RolloutStartedAt = nil\n\t\tstatusDirty = true\n\t}\n\n\t// Check operator-level rollout timeout for in-progress phases\n\tif phase == PhasePending || phase == PhaseRollingOut {\n\t\ttoResult := r.checkRolloutTimeout(ctx, ld)\n\t\tif toResult.TimedOut {\n\t\t\tphase = toResult.Phase\n\t\t\tmessage = toResult.Message\n\t\t} else if toResult.RequeueAfter > 0 {\n\t\t\trequeueAfter = toResult.RequeueAfter\n\t\t}\n\t}\n\n\treturn phase, message, requeueAfter, statusDirty, nil\n}\n\n// determineDeploymentPhase analyzes the deployment status and returns the appropriate phase and message\nfunc (r *LlamaDeploymentReconciler) determineDeploymentPhase(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) (string, string, error) {\n\tif llamaDeploy.Spec.Suspended {\n\t\treturn PhaseSuspended, \"Deployment is suspended (scaled to 0 replicas)\", nil\n\t}\n\n\tdeployment := &appsv1.Deployment{}\n\terr := r.Get(ctx, client.ObjectKey{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}, deployment)\n\tif err != nil {\n\t\tif errors.IsNotFound(err) {\n\t\t\t// Deployment doesn't exist yet\n\t\t\treturn PhasePending, \"Waiting for deployment to be created\", nil\n\t\t}\n\t\treturn \"\", \"\", err\n\t}\n\n\tdesiredReplicas := int32(1)\n\tif deployment.Spec.Replicas != nil {\n\t\tdesiredReplicas = *deployment.Spec.Replicas\n\t}\n\n\tstatus := deployment.Status\n\n\t// Get deployment conditions\n\tvar availableCondition, progressingCondition *appsv1.DeploymentCondition\n\tfor i := range status.Conditions {\n\t\tcondition := &status.Conditions[i]\n\t\tswitch condition.Type {\n\t\tcase appsv1.DeploymentAvailable:\n\t\t\tavailableCondition = condition\n\t\tcase appsv1.DeploymentProgressing:\n\t\t\tprogressingCondition = condition\n\t\t}\n\t}\n\n\t// Determine phase based on Kubernetes deployment conditions\n\tswitch {\n\tcase status.AvailableReplicas == 0:\n\t\t// No pods available - either starting up or completely failed\n\t\tif progressingCondition != nil && progressingCondition.Status == corev1.ConditionFalse {\n\t\t\treturn PhaseFailed, \"Deployment has failed and no pods are available\", nil\n\t\t}\n\t\treturn PhasePending, \"Waiting for deployment pods to become available\", nil\n\n\tcase progressingCondition != nil && progressingCondition.Status == corev1.ConditionFalse:\n\t\t// Progress deadline exceeded - rollout failed\n\t\tif status.AvailableReplicas > 0 {\n\t\t\treturn PhaseRolloutFailed, fmt.Sprintf(\"Deployment rollout failed but %d pods from previous version are still serving traffic\", status.AvailableReplicas), nil\n\t\t}\n\t\treturn PhaseFailed, \"Deployment rollout failed\", nil\n\n\tcase availableCondition != nil && availableCondition.Status == corev1.ConditionTrue &&\n\t\tprogressingCondition != nil && progressingCondition.Status == corev1.ConditionTrue &&\n\t\tstatus.ReadyReplicas == desiredReplicas && status.Replicas == desiredReplicas:\n\t\t// Deployment is available, progressing successfully, and has the right number of ready replicas\n\t\treturn PhaseRunning, \"Deployment is healthy and running\", nil\n\n\tcase progressingCondition != nil && progressingCondition.Status == corev1.ConditionTrue &&\n\t\t(status.Replicas > desiredReplicas || status.ReadyReplicas < desiredReplicas):\n\t\t// Deployment is progressing but not yet complete (rollout in progress)\n\t\tif status.Replicas > desiredReplicas {\n\t\t\treturn PhaseRollingOut, fmt.Sprintf(\"Rolling update in progress (%d/%d pods ready, %d total)\", status.ReadyReplicas, desiredReplicas, status.Replicas), nil\n\t\t}\n\t\treturn PhasePending, fmt.Sprintf(\"Deployment starting up (%d/%d pods ready)\", status.ReadyReplicas, desiredReplicas), nil\n\n\tdefault:\n\t\t// Fallback - deployment exists but conditions are unclear\n\t\tif status.ReadyReplicas > 0 {\n\t\t\treturn PhasePending, fmt.Sprintf(\"Deployment status unclear (%d/%d pods ready)\", status.ReadyReplicas, desiredReplicas), nil\n\t\t}\n\t\treturn PhasePending, \"Waiting for deployment to be ready\", nil\n\t}\n}\n\n// rolloutTimeoutResult holds the outcome of a rollout timeout check.\ntype rolloutTimeoutResult struct {\n\tTimedOut     bool\n\tPhase        string\n\tMessage      string\n\tRequeueAfter time.Duration\n}\n\n// checkRolloutTimeout checks whether the current rollout has exceeded the configured timeout.\n// Side-effect-free — the caller is responsible for remediation and status writes.\nfunc (r *LlamaDeploymentReconciler) checkRolloutTimeout(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) rolloutTimeoutResult {\n\tif llamaDeploy.Status.RolloutStartedAt == nil {\n\t\treturn rolloutTimeoutResult{}\n\t}\n\n\ttimeout := getRolloutTimeout()\n\telapsed := time.Since(llamaDeploy.Status.RolloutStartedAt.Time)\n\tremaining := timeout - elapsed\n\n\tif remaining > 0 {\n\t\treturn rolloutTimeoutResult{RequeueAfter: remaining}\n\t}\n\n\t// Timed out — determine whether this is a full failure (no healthy pods)\n\t// or a rollout failure (old RS still serving traffic).\n\tlogger := log.FromContext(ctx)\n\tlogger.Info(\"Rollout timeout exceeded\", \"elapsed\", elapsed.Truncate(time.Second), \"timeout\", timeout)\n\n\t// Check the k8s Deployment for available replicas to decide the phase\n\tdeployment := &appsv1.Deployment{}\n\tfailurePhase := PhaseRolloutFailed\n\tif err := r.Get(ctx, client.ObjectKey{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}, deployment); err == nil {\n\t\tif deployment.Status.AvailableReplicas == 0 {\n\t\t\tfailurePhase = PhaseFailed\n\t\t}\n\t}\n\n\tmessage := fmt.Sprintf(\"Rollout timed out after %s (limit: %s); failing pods stopped\",\n\t\telapsed.Truncate(time.Second), timeout)\n\n\treturn rolloutTimeoutResult{TimedOut: true, Phase: failurePhase, Message: message}\n}\n\n// remediateFailedRollout handles failure remediation: classifies pod failures,\n// records the failed generation, and scales down the appropriate resources.\n// Returns a non-nil result to short-circuit the reconcile (infra failures).\nfunc (r *LlamaDeploymentReconciler) remediateFailedRollout(ctx context.Context, ld *llamadeployv1.LlamaDeployment, phase string, buildId string) *ctrl.Result {\n\tlogger := log.FromContext(ctx)\n\n\t// Classify pod failures before acting — infrastructure issues should not\n\t// trigger scale-down.\n\tclassification := r.classifyPodFailures(ctx, ld)\n\tif classification == failureInfra {\n\t\tlogger.Info(\"Pod failures are infrastructure-related; not scaling down\",\n\t\t\t\"deployment\", ld.Name)\n\t\tif r.Recorder != nil {\n\t\t\tr.Recorder.Event(ld, corev1.EventTypeWarning, \"InfrastructureIssue\",\n\t\t\t\t\"Pods are being evicted or preempted; not scaling down — check node/resource configuration\")\n\t\t}\n\t\tresult := ctrl.Result{RequeueAfter: 30 * time.Second}\n\t\treturn &result\n\t}\n\n\tld.Status.FailedRolloutGeneration = ld.Generation\n\tld.Status.RolloutStartedAt = nil\n\n\tif phase == PhaseRolloutFailed {\n\t\t// Rollout failed but old RS has healthy traffic — pause and scale\n\t\t// down the newest RS to stop crash-looping while preserving old pods.\n\t\tif scaleErr := r.scaleDownFailingReplicaSet(ctx, ld); scaleErr != nil {\n\t\t\tlogger.Error(scaleErr, \"Failed to scale down failing ReplicaSet\")\n\t\t}\n\t} else {\n\t\t// Fully failed (no healthy pods) — set replicas=0 via the normal\n\t\t// SSA path. Set phase before reconcileDeployment so\n\t\t// createDeploymentForLlama sees PhaseFailed and produces replicas=0.\n\t\tld.Status.Phase = PhaseFailed\n\t\tif reconcileErr := r.reconcileDeployment(ctx, ld, buildId); reconcileErr != nil {\n\t\t\tlogger.Error(reconcileErr, \"Failed to scale down deployment via replicas\")\n\t\t}\n\t\tif r.Recorder != nil {\n\t\t\tr.Recorder.Event(ld, corev1.EventTypeWarning, \"DeploymentScaledDown\",\n\t\t\t\t\"Deployment has no healthy pods and failed to progress; scaled to 0 replicas\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// failureType classifies the kind of pod failure.\ntype failureType int\n\nconst (\n\tfailureUnknown failureType = iota\n\tfailureApp\n\tfailureInfra\n)\n\nfunc (f failureType) String() string {\n\tswitch f {\n\tcase failureApp:\n\t\treturn \"app\"\n\tcase failureInfra:\n\t\treturn \"infra\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// classifyPodFailures inspects pods for a deployment and classifies the failure\n// as app-level (CrashLoopBackOff, ImagePullBackOff, OOMKilled, etc.) or\n// infra-level (eviction, preemption, scheduling). Uses directClient to avoid\n// informer cache bloat.\nfunc (r *LlamaDeploymentReconciler) classifyPodFailures(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) failureType {\n\tlogger := log.FromContext(ctx)\n\n\tpodList := &corev1.PodList{}\n\tif err := r.directClient().List(ctx, podList,\n\t\tclient.InNamespace(llamaDeploy.Namespace),\n\t\tclient.MatchingLabels{\"app\": llamaDeploy.Name},\n\t); err != nil {\n\t\tlogger.Error(err, \"Failed to list pods for failure classification\")\n\t\treturn failureUnknown\n\t}\n\n\tif len(podList.Items) == 0 {\n\t\treturn failureUnknown\n\t}\n\n\thasInfra := false\n\tfor i := range podList.Items {\n\t\tpod := &podList.Items[i]\n\t\tclassification := classifyPod(pod)\n\t\tif classification == failureApp {\n\t\t\treturn failureApp\n\t\t}\n\t\tif classification == failureInfra {\n\t\t\thasInfra = true\n\t\t}\n\t}\n\n\tif hasInfra {\n\t\treturn failureInfra\n\t}\n\treturn failureUnknown\n}\n\n// classifyPod classifies a single pod's failure type.\nfunc classifyPod(pod *corev1.Pod) failureType {\n\t// Evicted pods are infrastructure failures\n\tif pod.Status.Reason == \"Evicted\" {\n\t\treturn failureInfra\n\t}\n\n\t// Pending pods with no container statuses are scheduling issues\n\tif pod.Status.Phase == corev1.PodPending && len(pod.Status.ContainerStatuses) == 0 && len(pod.Status.InitContainerStatuses) == 0 {\n\t\treturn failureInfra\n\t}\n\n\t// Check all container statuses (init + regular)\n\tfor _, statuses := range [][]corev1.ContainerStatus{pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses} {\n\t\tfor _, cs := range statuses {\n\t\t\tif cs.State.Waiting != nil {\n\t\t\t\tswitch cs.State.Waiting.Reason {\n\t\t\t\tcase \"CrashLoopBackOff\", \"ImagePullBackOff\", \"ErrImagePull\", \"CreateContainerConfigError\":\n\t\t\t\t\treturn failureApp\n\t\t\t\t}\n\t\t\t}\n\t\t\tif cs.State.Terminated != nil && cs.State.Terminated.ExitCode != 0 {\n\t\t\t\treturn failureApp\n\t\t\t}\n\t\t\t// Also check LastTerminationState for containers that restarted\n\t\t\tif cs.LastTerminationState.Terminated != nil && cs.LastTerminationState.Terminated.ExitCode != 0 {\n\t\t\t\treturn failureApp\n\t\t\t}\n\t\t}\n\t}\n\n\treturn failureUnknown\n}\n\n// scaleDownFailingReplicaSet finds the newest ReplicaSet for the Deployment,\n// pauses the Deployment to prevent the controller from fighting the scale-down,\n// and scales the newest RS to zero. The older RS (if any) is left intact.\nfunc (r *LlamaDeploymentReconciler) scaleDownFailingReplicaSet(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) error {\n\tlogger := log.FromContext(ctx)\n\n\t// Get the Deployment\n\tdeployment := &appsv1.Deployment{}\n\tif err := r.Get(ctx, client.ObjectKey{\n\t\tName:      llamaDeploy.Name,\n\t\tNamespace: llamaDeploy.Namespace,\n\t}, deployment); err != nil {\n\t\treturn fmt.Errorf(\"failed to get deployment: %w\", err)\n\t}\n\n\t// Pause the Deployment so the Deployment controller does not fight our RS changes\n\tif !deployment.Spec.Paused {\n\t\tpatch := client.MergeFrom(deployment.DeepCopy())\n\t\tdeployment.Spec.Paused = true\n\t\tif err := r.Patch(ctx, deployment, patch); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to pause deployment: %w\", err)\n\t\t}\n\t\tlogger.Info(\"Paused Deployment for rollout timeout remediation\")\n\t}\n\n\t// Use DirectClient for ReplicaSet operations to avoid starting an informer\n\t// that would cache ALL ReplicaSets in the namespace. With hundreds of\n\t// Deployments this can consume gigabytes of memory.\n\trsClient := r.directClient()\n\n\t// List ReplicaSets with matching labels\n\trsList := &appsv1.ReplicaSetList{}\n\tif err := rsClient.List(ctx, rsList,\n\t\tclient.InNamespace(llamaDeploy.Namespace),\n\t\tclient.MatchingLabels(deployment.Spec.Selector.MatchLabels),\n\t); err != nil {\n\t\treturn fmt.Errorf(\"failed to list ReplicaSets: %w\", err)\n\t}\n\n\t// Filter to ReplicaSets owned by this Deployment and find the newest by revision\n\tvar newestRS *appsv1.ReplicaSet\n\tvar maxRevision int64 = -1\n\thasOtherHealthyRS := false\n\n\tfor i := range rsList.Items {\n\t\trs := &rsList.Items[i]\n\n\t\t// Check ownership\n\t\towned := false\n\t\tfor _, ownerRef := range rs.OwnerReferences {\n\t\t\tif ownerRef.UID == deployment.UID {\n\t\t\t\towned = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !owned {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Track whether newest once determined\n\t\trevStr := rs.Annotations[\"deployment.kubernetes.io/revision\"]\n\t\trev, err := strconv.ParseInt(revStr, 10, 64)\n\t\tif err != nil {\n\t\t\tif newestRS == nil || rs.CreationTimestamp.After(newestRS.CreationTimestamp.Time) {\n\t\t\t\t// Track whether previous newest had healthy replicas\n\t\t\t\tif newestRS != nil && newestRS.Status.AvailableReplicas > 0 {\n\t\t\t\t\thasOtherHealthyRS = true\n\t\t\t\t}\n\t\t\t\tnewestRS = rs\n\t\t\t} else if rs.Status.AvailableReplicas > 0 {\n\t\t\t\thasOtherHealthyRS = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif rev > maxRevision {\n\t\t\tif newestRS != nil && newestRS.Status.AvailableReplicas > 0 {\n\t\t\t\thasOtherHealthyRS = true\n\t\t\t}\n\t\t\tmaxRevision = rev\n\t\t\tnewestRS = rs\n\t\t} else if rs.Status.AvailableReplicas > 0 {\n\t\t\thasOtherHealthyRS = true\n\t\t}\n\t}\n\n\tif newestRS == nil {\n\t\tlogger.Info(\"No ReplicaSets found for deployment\")\n\t\treturn nil\n\t}\n\n\t// Safety: skip scale-down when the newest RS is the sole source of healthy\n\t// traffic. This function is now only called for PhaseRolloutFailed where\n\t// an older RS is serving traffic, so this guards against edge cases.\n\tif newestRS.Status.AvailableReplicas > 0 && !hasOtherHealthyRS {\n\t\tlogger.Info(\"Newest ReplicaSet is the only one serving traffic; not scaling down\",\n\t\t\t\"replicaSet\", newestRS.Name)\n\t\treturn nil\n\t}\n\n\t// Scale down to 0 if not already\n\tcurrentReplicas := int32(1)\n\tif newestRS.Spec.Replicas != nil {\n\t\tcurrentReplicas = *newestRS.Spec.Replicas\n\t}\n\tif currentReplicas == 0 {\n\t\tlogger.Info(\"Newest ReplicaSet already scaled to 0\", \"replicaSet\", newestRS.Name)\n\t\treturn nil\n\t}\n\n\tnewestRS.Spec.Replicas = ptr(int32(0))\n\tif err := rsClient.Update(ctx, newestRS); err != nil {\n\t\treturn fmt.Errorf(\"failed to scale down ReplicaSet %s: %w\", newestRS.Name, err)\n\t}\n\n\tlogger.Info(\"Scaled down failing ReplicaSet to 0\",\n\t\t\"replicaSet\", newestRS.Name,\n\t\t\"previousReplicas\", currentReplicas)\n\n\tif r.Recorder != nil {\n\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeWarning, \"ReplicaSetScaledDown\",\n\t\t\tfmt.Sprintf(\"Rollout failed with healthy pods still serving; scaled down failing ReplicaSet %s from %d to 0 replicas\", newestRS.Name, currentReplicas))\n\t}\n\n\treturn nil\n}\n\n// finalizePhase writes the assessed phase to the status subresource and emits\n// events for phase transitions. Only writes when phase changed or statusDirty\n// indicates other status fields (e.g. RolloutStartedAt) were mutated.\nfunc (r *LlamaDeploymentReconciler) finalizePhase(ctx context.Context, ld *llamadeployv1.LlamaDeployment, phase, message string, requeueAfter time.Duration, statusDirty bool) (ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\n\tif ld.Status.Phase != phase || statusDirty {\n\t\toldPhase := ld.Status.Phase\n\t\tld.Status.Phase = phase\n\t\tld.Status.Message = message\n\t\tnow := metav1.Now()\n\t\tld.Status.LastUpdated = &now\n\n\t\tif err := r.Status().Update(ctx, ld); err != nil {\n\t\t\tlogger.Error(err, \"Failed to update LlamaDeployment status\", \"phase\", phase)\n\t\t\treturn ctrl.Result{}, err\n\t\t}\n\t\tlogger.Info(\"Updated deployment status\", \"phase\", phase, \"message\", message)\n\n\t\tif oldPhase != phase {\n\t\t\teventType := corev1.EventTypeNormal\n\t\t\teventMessage := fmt.Sprintf(\"Phase changed from %s to %s: %s\", oldPhase, phase, message)\n\t\t\tif phase == PhaseFailed || phase == PhaseRolloutFailed {\n\t\t\t\teventType = corev1.EventTypeWarning\n\t\t\t}\n\t\t\tif r.Recorder != nil {\n\t\t\t\tr.Recorder.Event(ld, eventType, phase, eventMessage)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ctrl.Result{RequeueAfter: requeueAfter}, nil\n}\n"
  },
  {
    "path": "operator/internal/controller/lifecycle_test.go",
    "content": "//go:build !integration\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client/fake\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\n// testSchemeWithApps returns a scheme with appsv1 registered in addition to\n// the types from newTestScheme.\nfunc testSchemeWithApps() *runtime.Scheme {\n\tscheme := newTestScheme()\n\t_ = appsv1.AddToScheme(scheme)\n\treturn scheme\n}\n\n// ---------------------------------------------------------------------------\n// assessDeploymentHealth tests\n// ---------------------------------------------------------------------------\n\nfunc TestAssessDeploymentHealth_RolloutTimeout(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\t// Seed a Deployment with 0 available replicas so timeout yields PhaseFailed.\n\tdep := &appsv1.Deployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tStatus: appsv1.DeploymentStatus{\n\t\t\tAvailableReplicas: 0,\n\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue},\n\t\t\t},\n\t\t},\n\t}\n\n\texpiredTime := metav1.NewTime(time.Now().Add(-2 * time.Hour))\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:            PhasePending,\n\t\t\tRolloutStartedAt: &expiredTime,\n\t\t\tAuthToken:        \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, dep).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tphase, _, _, _, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif phase != PhaseFailed {\n\t\tt.Errorf(\"expected phase %q on timeout with 0 available replicas, got %q\", PhaseFailed, phase)\n\t}\n}\n\nfunc TestAssessDeploymentHealth_RolloutTimeout_WithAvailableReplicas(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\t// Seed a Deployment with available replicas so timeout yields PhaseRolloutFailed.\n\tdep := &appsv1.Deployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tStatus: appsv1.DeploymentStatus{\n\t\t\tAvailableReplicas: 1,\n\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue},\n\t\t\t},\n\t\t},\n\t}\n\n\texpiredTime := metav1.NewTime(time.Now().Add(-2 * time.Hour))\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:            PhaseRollingOut,\n\t\t\tRolloutStartedAt: &expiredTime,\n\t\t\tAuthToken:        \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, dep).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tphase, _, _, _, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif phase != PhaseRolloutFailed {\n\t\tt.Errorf(\"expected phase %q on timeout with available replicas, got %q\", PhaseRolloutFailed, phase)\n\t}\n}\n\nfunc TestAssessDeploymentHealth_PhaseRunning(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tdep := &appsv1.Deployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       appsv1.DeploymentSpec{Replicas: ptr(int32(1))},\n\t\tStatus: appsv1.DeploymentStatus{\n\t\t\tAvailableReplicas: 1,\n\t\t\tReadyReplicas:     1,\n\t\t\tReplicas:          1,\n\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t{Type: appsv1.DeploymentAvailable, Status: corev1.ConditionTrue},\n\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue},\n\t\t\t},\n\t\t},\n\t}\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhaseRollingOut,\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, dep).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tphase, _, _, _, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif phase != PhaseRunning {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhaseRunning, phase)\n\t}\n}\n\nfunc TestAssessDeploymentHealth_PhasePending_NoDeployment(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhasePending,\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tphase, _, _, statusDirty, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif phase != PhasePending {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhasePending, phase)\n\t}\n\t// RolloutStartedAt should be set since phase is Pending and it was nil\n\tif !statusDirty {\n\t\tt.Error(\"expected statusDirty=true when RolloutStartedAt is first set\")\n\t}\n\tif ld.Status.RolloutStartedAt == nil {\n\t\tt.Error(\"expected RolloutStartedAt to be set\")\n\t}\n}\n\nfunc TestAssessDeploymentHealth_ClearsRolloutStartedAt_OnRunning(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tdep := &appsv1.Deployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       appsv1.DeploymentSpec{Replicas: ptr(int32(1))},\n\t\tStatus: appsv1.DeploymentStatus{\n\t\t\tAvailableReplicas: 1,\n\t\t\tReadyReplicas:     1,\n\t\t\tReplicas:          1,\n\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t{Type: appsv1.DeploymentAvailable, Status: corev1.ConditionTrue},\n\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue},\n\t\t\t},\n\t\t},\n\t}\n\n\tstartTime := metav1.NewTime(time.Now().Add(-1 * time.Minute))\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:            PhaseRollingOut,\n\t\t\tRolloutStartedAt: &startTime,\n\t\t\tAuthToken:        \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, dep).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tphase, _, _, statusDirty, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif phase != PhaseRunning {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhaseRunning, phase)\n\t}\n\tif !statusDirty {\n\t\tt.Error(\"expected statusDirty=true when RolloutStartedAt is cleared\")\n\t}\n\tif ld.Status.RolloutStartedAt != nil {\n\t\tt.Error(\"expected RolloutStartedAt to be cleared on Running phase\")\n\t}\n}\n\nfunc TestAssessDeploymentHealth_PhaseSuspended(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", Suspended: true},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhaseRunning,\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tphase, _, _, _, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif phase != PhaseSuspended {\n\t\tt.Errorf(\"expected phase %q for suspended deployment, got %q\", PhaseSuspended, phase)\n\t}\n}\n\nfunc TestAssessDeploymentHealth_PhaseFailed_ProgressFalse_NoAvailable(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tdep := &appsv1.Deployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tStatus: appsv1.DeploymentStatus{\n\t\t\tAvailableReplicas: 0,\n\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionFalse},\n\t\t\t},\n\t\t},\n\t}\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhasePending,\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, dep).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tphase, _, _, _, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif phase != PhaseFailed {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhaseFailed, phase)\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// checkCapacityGates tests\n// ---------------------------------------------------------------------------\n\nfunc TestCheckCapacityGates_SkipsWhenNotNeedsFullReconcile(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient:         fakeClient,\n\t\tScheme:         scheme,\n\t\tMaxDeployments: 1,\n\t}\n\tctx := context.Background()\n\n\tresult, err := r.checkCapacityGates(ctx, ld, false)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result != nil {\n\t\tt.Errorf(\"expected nil result when needsFullReconcile=false, got %+v\", result)\n\t}\n}\n\nfunc TestCheckCapacityGates_SkipsWhenSuspended(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", Suspended: true},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient:         fakeClient,\n\t\tScheme:         scheme,\n\t\tMaxDeployments: 1,\n\t}\n\tctx := context.Background()\n\n\tresult, err := r.checkCapacityGates(ctx, ld, true)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result != nil {\n\t\tt.Errorf(\"expected nil result when Suspended=true, got %+v\", result)\n\t}\n}\n\nfunc TestCheckCapacityGates_SkipsWhenAwaitingCodePhase(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\t// Deployment already in PhaseAwaitingCode (has a RepoUrl now but phase hasn't transitioned yet)\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"new-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase: PhaseAwaitingCode,\n\t\t},\n\t}\n\n\t// An existing active deployment that fills the capacity\n\texisting := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"existing-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p2\", RepoUrl: \"r2\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase: PhaseRunning,\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, existing).\n\t\tWithStatusSubresource(ld, existing).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient:                fakeClient,\n\t\tScheme:                scheme,\n\t\tMaxDeployments:        1,\n\t\tMaxConcurrentRollouts: 1,\n\t}\n\tctx := context.Background()\n\n\tresult, err := r.checkCapacityGates(ctx, ld, true)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result != nil {\n\t\tt.Errorf(\"expected nil result when phase is AwaitingCode, got %+v\", result)\n\t}\n}\n\nfunc TestCheckCapacityGates_RequeuesWhenMaxDeploymentsExceeded(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\t// Current deployment being reconciled\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"new-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t}\n\n\t// An existing active deployment that fills the capacity\n\texisting := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"existing-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p2\", RepoUrl: \"r2\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase: PhaseRunning,\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, existing).\n\t\tWithStatusSubresource(ld, existing).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient:         fakeClient,\n\t\tScheme:         scheme,\n\t\tMaxDeployments: 1,\n\t}\n\tctx := context.Background()\n\n\tresult, err := r.checkCapacityGates(ctx, ld, true)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result == nil {\n\t\tt.Fatal(\"expected non-nil result when max deployments exceeded\")\n\t}\n\tif result.RequeueAfter == 0 {\n\t\tt.Error(\"expected RequeueAfter > 0\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// checkSecretGate tests\n// ---------------------------------------------------------------------------\n\nfunc TestCheckSecretGate_NilWhenSecretNameEmpty(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", SecretName: \"\"},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tresult, err := r.checkSecretGate(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result != nil {\n\t\tt.Errorf(\"expected nil result when SecretName is empty, got %+v\", result)\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// finalizePhase tests\n// ---------------------------------------------------------------------------\n\nfunc TestFinalizePhase_NoWriteWhenUnchangedAndNotDirty(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhaseRunning,\n\t\t\tMessage:   \"all good\",\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\t// Same phase, statusDirty=false → should not write\n\tresult, err := r.finalizePhase(ctx, ld, PhaseRunning, \"all good\", 0, false)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result.RequeueAfter != 0 {\n\t\tt.Errorf(\"expected no requeue, got %v\", result.RequeueAfter)\n\t}\n\t// LastUpdated should not have been set since we didn't enter the update path\n\tif ld.Status.LastUpdated != nil {\n\t\tt.Error(\"expected LastUpdated to remain nil when no write happens\")\n\t}\n}\n\nfunc TestFinalizePhase_WritesOnPhaseChange(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhasePending,\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\t_, err := r.finalizePhase(ctx, ld, PhaseRunning, \"deployment healthy\", 0, false)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif ld.Status.Phase != PhaseRunning {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhaseRunning, ld.Status.Phase)\n\t}\n\tif ld.Status.LastUpdated == nil {\n\t\tt.Error(\"expected LastUpdated to be set on phase change\")\n\t}\n}\n\nfunc TestFinalizePhase_WritesWhenStatusDirty(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhaseRunning,\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\t// Same phase but statusDirty=true → should still write\n\t_, err := r.finalizePhase(ctx, ld, PhaseRunning, \"deployment healthy\", 0, true)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif ld.Status.LastUpdated == nil {\n\t\tt.Error(\"expected LastUpdated to be set when statusDirty=true\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// isFailedPhase tests\n// ---------------------------------------------------------------------------\n\nfunc TestIsFailedPhase(t *testing.T) {\n\ttests := []struct {\n\t\tphase string\n\t\twant  bool\n\t}{\n\t\t{PhaseRolloutFailed, true},\n\t\t{PhaseFailed, true},\n\t\t{PhaseRunning, false},\n\t\t{PhasePending, false},\n\t\t{PhaseBuilding, false},\n\t\t{PhaseBuildFailed, false},\n\t\t{PhaseSuspended, false},\n\t\t{PhaseAwaitingCode, false},\n\t\t{\"\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.phase, func(t *testing.T) {\n\t\t\tif got := isFailedPhase(tt.phase); got != tt.want {\n\t\t\t\tt.Errorf(\"isFailedPhase(%q) = %v, want %v\", tt.phase, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// checkRolloutTimeout tests\n// ---------------------------------------------------------------------------\n\nfunc TestCheckRolloutTimeout_NilRolloutStartedAt(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tRolloutStartedAt: nil,\n\t\t},\n\t}\n\tfakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ld).Build()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tresult := r.checkRolloutTimeout(ctx, ld)\n\tif result.TimedOut {\n\t\tt.Error(\"expected TimedOut=false when RolloutStartedAt is nil\")\n\t}\n\tif result.RequeueAfter != 0 {\n\t\tt.Errorf(\"expected RequeueAfter=0, got %v\", result.RequeueAfter)\n\t}\n}\n\nfunc TestCheckRolloutTimeout_RecentStart_RequeuesWithRemaining(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\trecentTime := metav1.NewTime(time.Now().Add(-1 * time.Minute))\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tRolloutStartedAt: &recentTime,\n\t\t},\n\t}\n\tfakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ld).Build()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tresult := r.checkRolloutTimeout(ctx, ld)\n\tif result.TimedOut {\n\t\tt.Error(\"expected TimedOut=false for recent rollout start\")\n\t}\n\tif result.RequeueAfter <= 0 {\n\t\tt.Error(\"expected RequeueAfter > 0 for in-progress rollout\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// checkRolloutCapacity tests\n// ---------------------------------------------------------------------------\n\nfunc TestCheckRolloutCapacity_UnlimitedWhenZero(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t}\n\tfakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ld).Build()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme, MaxConcurrentRollouts: 0}\n\tctx := context.Background()\n\n\trequeue, _, err := r.checkRolloutCapacity(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif requeue {\n\t\tt.Error(\"expected no requeue when MaxConcurrentRollouts=0\")\n\t}\n}\n\nfunc TestCheckRolloutCapacity_RequeuesAtLimit(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tcurrent := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"new-app\", Namespace: \"default\"},\n\t}\n\trolling := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"rolling-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{Phase: PhaseRollingOut},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(current, rolling).\n\t\tWithStatusSubresource(current, rolling).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme, MaxConcurrentRollouts: 1}\n\tctx := context.Background()\n\n\trequeue, result, err := r.checkRolloutCapacity(ctx, current)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif !requeue {\n\t\tt.Error(\"expected requeue when at rollout capacity\")\n\t}\n\tif result.RequeueAfter == 0 {\n\t\tt.Error(\"expected RequeueAfter > 0\")\n\t}\n}\n\nfunc TestCheckRolloutCapacity_IgnoresSuspendedDeployments(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tcurrent := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"new-app\", Namespace: \"default\"},\n\t}\n\t// Suspended deployment in Pending phase should NOT count toward limit\n\tsuspended := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"suspended-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", Suspended: true},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{Phase: PhasePending},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(current, suspended).\n\t\tWithStatusSubresource(current, suspended).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme, MaxConcurrentRollouts: 1}\n\tctx := context.Background()\n\n\trequeue, _, err := r.checkRolloutCapacity(ctx, current)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif requeue {\n\t\tt.Error(\"expected no requeue when suspended deployments don't count\")\n\t}\n}\n\nfunc TestCheckRolloutCapacity_IgnoresAwaitingCodeDeployments(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tcurrent := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"new-app\", Namespace: \"default\"},\n\t}\n\t// Deployment in AwaitingCode phase should NOT count toward rollout limit\n\twaitingForPush := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"waiting-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{Phase: PhaseAwaitingCode},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(current, waitingForPush).\n\t\tWithStatusSubresource(current, waitingForPush).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme, MaxConcurrentRollouts: 1}\n\tctx := context.Background()\n\n\trequeue, _, err := r.checkRolloutCapacity(ctx, current)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif requeue {\n\t\tt.Error(\"expected no requeue when AwaitingCode deployments don't count\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// checkDeploymentCapacity tests\n// ---------------------------------------------------------------------------\n\nfunc TestCheckDeploymentCapacity_UnlimitedWhenZero(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t}\n\tfakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ld).Build()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme, MaxDeployments: 0}\n\tctx := context.Background()\n\n\trequeue, _, err := r.checkDeploymentCapacity(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif requeue {\n\t\tt.Error(\"expected no requeue when MaxDeployments=0\")\n\t}\n}\n\nfunc TestCheckDeploymentCapacity_RequeuesAtLimit(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tcurrent := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"new-app\", Namespace: \"default\"},\n\t}\n\texisting := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"existing-app\", Namespace: \"default\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{Phase: PhaseRunning},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(current, existing).\n\t\tWithStatusSubresource(current, existing).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme, MaxDeployments: 1}\n\tctx := context.Background()\n\n\trequeue, result, err := r.checkDeploymentCapacity(ctx, current)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif !requeue {\n\t\tt.Error(\"expected requeue when at deployment capacity\")\n\t}\n\tif result.RequeueAfter == 0 {\n\t\tt.Error(\"expected RequeueAfter > 0\")\n\t}\n}\n\nfunc TestCheckDeploymentCapacity_IgnoresAwaitingCodeDeployments(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tcurrent := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"new-app\", Namespace: \"default\"},\n\t}\n\t// Deployment in AwaitingCode phase should NOT count toward deployment limit\n\twaitingForPush := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"waiting-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{Phase: PhaseAwaitingCode},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(current, waitingForPush).\n\t\tWithStatusSubresource(current, waitingForPush).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme, MaxDeployments: 1}\n\tctx := context.Background()\n\n\trequeue, _, err := r.checkDeploymentCapacity(ctx, current)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif requeue {\n\t\tt.Error(\"expected no requeue when AwaitingCode deployments don't count\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// failureType.String tests\n// ---------------------------------------------------------------------------\n\nfunc TestFailureTypeString(t *testing.T) {\n\ttests := []struct {\n\t\tft   failureType\n\t\twant string\n\t}{\n\t\t{failureApp, \"app\"},\n\t\t{failureInfra, \"infra\"},\n\t\t{failureUnknown, \"unknown\"},\n\t}\n\tfor _, tt := range tests {\n\t\tif got := tt.ft.String(); got != tt.want {\n\t\t\tt.Errorf(\"failureType(%d).String() = %q, want %q\", tt.ft, got, tt.want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "operator/internal/controller/llamadeployment_controller_test.go",
    "content": "//go:build integration\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t. \"github.com/onsi/ginkgo/v2\"\n\t. \"github.com/onsi/gomega\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tnetworkingv1 \"k8s.io/api/networking/v1\"\n\t\"k8s.io/apimachinery/pkg/api/errors\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\t\"k8s.io/client-go/tools/record\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/reconcile\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nconst LLAMA_DEPLOY_REPO_URL = \"LLAMA_DEPLOY_REPO_URL\"\nconst APP = \"app\"\n\nvar _ = Describe(\"LlamaDeployment Controller\", func() {\n\tContext(\"When reconciling a resource\", func() {\n\t\tconst (\n\t\t\ttestNamespace = \"default\"\n\t\t\ttestProjectID = \"test-project\"\n\t\t\ttestRepoURL   = \"https://github.com/test/repo.git\"\n\t\t\ttestGitRef    = \"abc123\"\n\t\t\ttestGitSha    = \"1234567\"\n\t\t\ttestGitSha2   = \"7654321\"\n\t\t)\n\n\t\tvar (\n\t\t\tctx                  context.Context\n\t\t\tcontrollerReconciler *LlamaDeploymentReconciler\n\t\t)\n\n\t\tBeforeEach(func() {\n\t\t\tctx = context.Background()\n\t\t\tcontrollerReconciler = &LlamaDeploymentReconciler{\n\t\t\t\tClient:       k8sClient,\n\t\t\t\tScheme:       k8sClient.Scheme(),\n\t\t\t\tRecorder:     record.NewFakeRecorder(100), // Buffered fake recorder for tests\n\t\t\t\tDirectClient: k8sClient,\n\t\t\t}\n\t\t})\n\t\tDescribe(\"LlamaDeploymentTemplate overlays\", func() {\n\t\t\tIt(\"should not modify pod spec when template does not exist\", func() {\n\t\t\t\ttestName := \"test-template-none\"\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(dep.Spec.Template.Spec.Tolerations).To(BeEmpty())\n\t\t\t\tExpect(dep.Spec.Template.Spec.NodeSelector).To(BeEmpty())\n\t\t\t})\n\n\t\t\tIt(\"should apply default template overlay when present before deployment\", func() {\n\t\t\t\ttestName := \"test-template-default-before\"\n\n\t\t\t\t// Create default template\n\t\t\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"default\", Namespace: testNamespace},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\t\t\tNodeSelector: map[string]string{\"disktype\": \"ssd\"},\n\t\t\t\t\t\t\t\tTolerations:  []corev1.Toleration{{Key: \"dedicated\", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule}},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, tmpl)).To(Succeed())\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = k8sClient.Delete(ctx, tmpl)\n\t\t\t\t}()\n\n\t\t\t\t// Create deployment\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(dep.Spec.Template.Spec.NodeSelector).To(HaveKeyWithValue(\"disktype\", \"ssd\"))\n\t\t\t\tExpect(dep.Spec.Template.Spec.Tolerations).To(ContainElement(\n\t\t\t\t\tSatisfyAll(\n\t\t\t\t\t\tWithTransform(func(t corev1.Toleration) string { return t.Key }, Equal(\"dedicated\")),\n\t\t\t\t\t\tWithTransform(func(t corev1.Toleration) corev1.TaintEffect { return t.Effect }, Equal(corev1.TaintEffectNoSchedule)),\n\t\t\t\t\t)))\n\t\t\t})\n\n\t\t\tIt(\"should apply template overlay when templateName is specified\", func() {\n\t\t\t\ttestName := \"test-template-named\"\n\n\t\t\t\t// Create a named template\n\t\t\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"high-priority\", Namespace: testNamespace},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\t\t\tSpec: corev1.PodSpec{PriorityClassName: \"system-cluster-critical\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, tmpl)).To(Succeed())\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = k8sClient.Delete(ctx, tmpl)\n\t\t\t\t}()\n\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef, TemplateName: \"high-priority\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(dep.Spec.Template.Spec.PriorityClassName).To(Equal(\"system-cluster-critical\"))\n\t\t\t})\n\n\t\t\tIt(\"should apply overlay after deployment exists when template is added\", func() {\n\t\t\t\ttestName := \"test-template-added-after\"\n\n\t\t\t\t// Create deployment first\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t// First reconcile\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\t// Create default template\n\t\t\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"default\", Namespace: testNamespace},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\t\t\tPodSpec: corev1.PodTemplateSpec{Spec: corev1.PodSpec{Tolerations: []corev1.Toleration{{Key: \"gpu\", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule}}}},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, tmpl)).To(Succeed())\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = k8sClient.Delete(ctx, tmpl)\n\t\t\t\t}()\n\n\t\t\t\t// Reconcile again to pick up template\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(dep.Spec.Template.Spec.Tolerations).To(ContainElement(\n\t\t\t\t\tSatisfyAll(\n\t\t\t\t\t\tWithTransform(func(t corev1.Toleration) string { return t.Key }, Equal(\"gpu\")),\n\t\t\t\t\t\tWithTransform(func(t corev1.Toleration) corev1.TaintEffect { return t.Effect }, Equal(corev1.TaintEffectNoSchedule)),\n\t\t\t\t\t)))\n\t\t\t})\n\n\t\t\tIt(\"should override container resources when template specifies them\", func() {\n\t\t\t\ttestName := \"test-template-container-resources\"\n\n\t\t\t\t// Create template with container resource overrides\n\t\t\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"resource-limits\", Namespace: testNamespace},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tName: \"app\",\n\t\t\t\t\t\t\t\t\t\tResources: corev1.ResourceRequirements{\n\t\t\t\t\t\t\t\t\t\t\tRequests: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceCPU:    resource.MustParse(\"4000m\"),\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceMemory: resource.MustParse(\"8192Mi\"),\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\tLimits: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceMemory: resource.MustParse(\"8192Mi\"),\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, tmpl)).To(Succeed())\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = k8sClient.Delete(ctx, tmpl)\n\t\t\t\t}()\n\n\t\t\t\t// Create deployment with template reference\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef, TemplateName: \"resource-limits\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\t// Find the app container\n\t\t\t\tvar appContainer *corev1.Container\n\t\t\t\tfor i := range dep.Spec.Template.Spec.Containers {\n\t\t\t\t\tif dep.Spec.Template.Spec.Containers[i].Name == \"app\" {\n\t\t\t\t\t\tappContainer = &dep.Spec.Template.Spec.Containers[i]\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(appContainer).NotTo(BeNil(), \"app container should exist\")\n\n\t\t\t\t// Verify resource overrides were applied\n\t\t\t\tExpect(appContainer.Resources.Requests.Cpu().String()).To(Equal(\"4\"))\n\t\t\t\t// Memory may be normalized to different units (8192Mi == 8Gi)\n\t\t\t\texpectedMemory := resource.MustParse(\"8192Mi\")\n\t\t\t\tExpect(appContainer.Resources.Requests.Memory().Cmp(expectedMemory)).To(Equal(0), \"Request memory should equal 8192Mi\")\n\t\t\t\tExpect(appContainer.Resources.Limits.Memory().Cmp(expectedMemory)).To(Equal(0), \"Limit memory should equal 8192Mi\")\n\n\t\t\t\t// Verify other containers (build, file-server) are unchanged\n\t\t\t\tExpect(dep.Spec.Template.Spec.InitContainers).NotTo(BeEmpty(), \"build init container should exist\")\n\t\t\t\tExpect(dep.Spec.Template.Spec.Containers).To(HaveLen(2), \"should have app and file-server containers\")\n\t\t\t})\n\n\t\t\tIt(\"should apply app container resources to build job when no build container in template\", func() {\n\t\t\t\ttestName := \"test-template-build-fallback\"\n\n\t\t\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"build-fallback\", Namespace: testNamespace},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tName: \"app\",\n\t\t\t\t\t\t\t\t\t\tResources: corev1.ResourceRequirements{\n\t\t\t\t\t\t\t\t\t\t\tRequests: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceCPU:    resource.MustParse(\"2000m\"),\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceMemory: resource.MustParse(\"4Gi\"),\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\tLimits: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceMemory: resource.MustParse(\"8Gi\"),\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, tmpl)).To(Succeed())\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = k8sClient.Delete(ctx, tmpl)\n\t\t\t\t}()\n\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef, GitSha: testGitSha, TemplateName: \"build-fallback\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t// First reconcile creates the build job\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Find the build job and verify its resources match the app container from template\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, ld)).To(Succeed())\n\t\t\t\tbuildId := computeBuildId(ld)\n\t\t\t\tjobName := fmt.Sprintf(\"%s-build-%s\", testName, buildId)\n\t\t\t\tif len(jobName) > 63 {\n\t\t\t\t\tjobName = jobName[:63]\n\t\t\t\t}\n\t\t\t\tjob := &batchv1.Job{}\n\t\t\t\tExpect(k8sClient.Get(ctx, client.ObjectKey{Name: jobName, Namespace: testNamespace}, job)).To(Succeed())\n\n\t\t\t\tbuildContainer, found := FindContainer(job.Spec.Template.Spec, \"build\")\n\t\t\t\tExpect(found).To(BeTrue(), \"build container should exist in job\")\n\t\t\t\tExpect(buildContainer.Resources.Requests.Cpu().String()).To(Equal(\"2\"), \"build CPU request should match app container from template\")\n\t\t\t\tExpect(buildContainer.Resources.Requests.Memory().Cmp(resource.MustParse(\"4Gi\"))).To(Equal(0), \"build memory request should match app container from template\")\n\t\t\t\tExpect(buildContainer.Resources.Limits.Memory().Cmp(resource.MustParse(\"8Gi\"))).To(Equal(0), \"build memory limit should match app container from template\")\n\t\t\t})\n\n\t\t\tIt(\"should not add build container to runtime deployment when template has one\", func() {\n\t\t\t\ttestName := \"test-template-build-stripped\"\n\n\t\t\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"with-build\", Namespace: testNamespace},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tName: \"build\",\n\t\t\t\t\t\t\t\t\t\tResources: corev1.ResourceRequirements{\n\t\t\t\t\t\t\t\t\t\t\tRequests: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceCPU:    resource.MustParse(\"1000m\"),\n\t\t\t\t\t\t\t\t\t\t\t\tcorev1.ResourceMemory: resource.MustParse(\"3Gi\"),\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, tmpl)).To(Succeed())\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = k8sClient.Delete(ctx, tmpl)\n\t\t\t\t}()\n\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef, GitSha: testGitSha, TemplateName: \"with-build\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\t// Runtime deployment should NOT have a \"build\" container\n\t\t\t\t_, found := FindContainer(dep.Spec.Template.Spec, \"build\")\n\t\t\t\tExpect(found).To(BeFalse(), \"build container should be stripped from runtime deployment\")\n\n\t\t\t\t// Should still have exactly app + file-server\n\t\t\t\tExpect(dep.Spec.Template.Spec.Containers).To(HaveLen(2), \"should have app and file-server containers only\")\n\t\t\t})\n\t\t})\n\n\t\t// Helper function to clean up resources with proper finalizer handling\n\t\tcleanupResource := func(name string) {\n\t\t\tBy(fmt.Sprintf(\"Cleaning up LlamaDeployment %s\", name))\n\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{}\n\t\t\tif err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: testNamespace}, llamaDeploy); err == nil {\n\t\t\t\t// Remove finalizer first to allow deletion\n\t\t\t\tif len(llamaDeploy.Finalizers) > 0 {\n\t\t\t\t\tllamaDeploy.Finalizers = []string{}\n\t\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Delete(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\t// Wait for actual deletion\n\t\t\t\tEventually(func() bool {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\t\treturn errors.IsNotFound(err)\n\t\t\t\t}, \"5s\", \"100ms\").Should(BeTrue())\n\t\t\t}\n\n\t\t\t// Clean up ServiceAccount\n\t\t\tBy(fmt.Sprintf(\"Cleaning up ServiceAccount %s-sa\", name))\n\t\t\tserviceAccount := &corev1.ServiceAccount{}\n\t\t\tif err := k8sClient.Get(ctx, types.NamespacedName{Name: name + \"-sa\", Namespace: testNamespace}, serviceAccount); err == nil {\n\t\t\t\tExpect(k8sClient.Delete(ctx, serviceAccount)).To(Succeed())\n\t\t\t}\n\n\t\t\t// Clean up NetworkPolicy\n\t\t\tBy(fmt.Sprintf(\"Cleaning up NetworkPolicy %s-egress\", name))\n\t\t\tnetworkPolicy := &networkingv1.NetworkPolicy{}\n\t\t\tif err := k8sClient.Get(ctx, types.NamespacedName{Name: name + \"-egress\", Namespace: testNamespace}, networkPolicy); err == nil {\n\t\t\t\tExpect(k8sClient.Delete(ctx, networkPolicy)).To(Succeed())\n\t\t\t}\n\t\t}\n\n\t\tcleanupSecret := func(name string) {\n\t\t\tBy(fmt.Sprintf(\"Cleaning up Secret %s\", name))\n\t\t\tsecret := &corev1.Secret{}\n\t\t\tif err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: testNamespace}, secret); err == nil {\n\t\t\t\tExpect(k8sClient.Delete(ctx, secret)).To(Succeed())\n\t\t\t}\n\t\t}\n\n\t\tDescribe(\"Basic reconciliation\", func() {\n\t\t\tIt(\"should create all required resources for a basic LlamaDeployment\", func() {\n\t\t\t\ttestName := \"test-basic-reconciliation\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying the ServiceAccount was created\")\n\t\t\t\tserviceAccount := &corev1.ServiceAccount{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-sa\", Namespace: testNamespace}, serviceAccount)\n\t\t\t\t}).Should(Succeed())\n\t\t\t\tExpect(serviceAccount.AutomountServiceAccountToken).NotTo(BeNil())\n\t\t\t\tExpect(*serviceAccount.AutomountServiceAccountToken).To(BeFalse())\n\n\t\t\t\tBy(\"Verifying the Deployment was created\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(*deployment.Spec.Replicas).To(Equal(int32(1)))\n\t\t\t\t// Expect two containers: file-server and appserver\n\t\t\t\tExpect(deployment.Spec.Template.Spec.Containers).To(HaveLen(2))\n\t\t\t\t// Verify app container image on the container named APP\n\t\t\t\tvar appContainer corev1.Container\n\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\tappContainer = c\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(appContainer.Image).To(Equal(\"llamaindex/llama-agents-appserver:latest\"))\n\n\t\t\t\t// Verify ServiceAccount is set in deployment\n\t\t\t\tExpect(deployment.Spec.Template.Spec.ServiceAccountName).To(Equal(testName + \"-sa\"))\n\t\t\t\tExpect(deployment.Spec.Template.Spec.AutomountServiceAccountToken).NotTo(BeNil())\n\t\t\t\tExpect(*deployment.Spec.Template.Spec.AutomountServiceAccountToken).To(BeFalse())\n\n\t\t\t\tBy(\"Verifying the Service was created\")\n\t\t\t\tExpectServicePort(ctx, testName, testNamespace, 80, 8081)\n\n\t\t\t\tBy(\"Verifying the status was updated to Pending\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\treturn llamaDeploy.Status.Phase\n\t\t\t\t}).Should(Equal(\"Pending\"))\n\t\t\t\tExpect(llamaDeploy.Status.Message).To(Equal(\"Waiting for deployment pods to become available\"))\n\n\t\t\t\t// Verify init container exists\n\t\t\t\tExpect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(1))\n\t\t\t\tExpect(deployment.Spec.Template.Spec.InitContainers[0].Name).To(Equal(\"bootstrap\"))\n\t\t\t\tExpect(deployment.Spec.Template.Spec.InitContainers[0].Command).To(Equal([]string{\"python\", \"-m\", \"llama_deploy.appserver.bootstrap\"}))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Secret handling\", func() {\n\t\t\tIt(\"should successfully reconcile when secret exists\", func() {\n\t\t\t\ttestName := \"test-secret-exists\"\n\t\t\t\ttestSecretName := \"test-secret-exists\"\n\n\t\t\t\tBy(\"Creating a test secret\")\n\t\t\t\tsecret := &corev1.Secret{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testSecretName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tData: map[string][]byte{\n\t\t\t\t\t\t\"GITHUB_PAT\": []byte(\"test-token\"),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, secret)).To(Succeed())\n\n\t\t\t\tdefer cleanupSecret(testSecretName)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment with secret reference\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId:  testProjectID,\n\t\t\t\t\t\tRepoUrl:    testRepoURL,\n\t\t\t\t\t\tGitRef:     testGitRef,\n\t\t\t\t\t\tSecretName: testSecretName,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying the status was updated to Pending\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\treturn llamaDeploy.Status.Phase\n\t\t\t\t}).Should(Equal(\"Pending\"))\n\t\t\t})\n\n\t\t\tIt(\"should retry and then mark RolloutFailed when secret missing\", func() {\n\t\t\t\ttestName := \"test-secret-missing\"\n\t\t\t\tnn := types.NamespacedName{Name: testName, Namespace: testNamespace}\n\n\t\t\t\tBy(\"Creating a LlamaDeployment with non-existent secret reference\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId:  testProjectID,\n\t\t\t\t\t\tRepoUrl:    testRepoURL,\n\t\t\t\t\t\tGitRef:     testGitRef,\n\t\t\t\t\t\tSecretName: \"non-existent-secret\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling 3 times — should retry, not fail yet\")\n\t\t\t\tfor i := 0; i < 3; i++ {\n\t\t\t\t\tresult, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: nn})\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\">\", 0), \"should requeue for retry\")\n\n\t\t\t\t\tExpect(k8sClient.Get(ctx, nn, llamaDeploy)).To(Succeed())\n\t\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Pending\"), \"should stay Pending during retries\")\n\t\t\t\t\tExpect(llamaDeploy.Status.SecretCheckRetries).To(Equal(int32(i + 1)))\n\t\t\t\t}\n\n\t\t\t\tBy(\"Reconciling a 4th time — retries exhausted, should mark RolloutFailed\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: nn})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tExpect(k8sClient.Get(ctx, nn, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\n\t\t\t\tBy(\"Reconciling again; phase should remain RolloutFailed\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: nn})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tConsistently(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, nn, llamaDeploy)\n\t\t\t\t\treturn llamaDeploy.Status.Phase\n\t\t\t\t}, \"1s\", \"100ms\").Should(Equal(\"RolloutFailed\"))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Status transitions\", func() {\n\t\t\tIt(\"should set status to Pending initially\", func() {\n\t\t\t\ttestName := \"test-status-syncing\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying status was set to Pending first\")\n\t\t\t\terr = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Pending\")) // Could be either due to fast reconciliation\n\t\t\t\tExpect(llamaDeploy.Status.LastUpdated).NotTo(BeNil())\n\t\t\t})\n\n\t\t\tIt(\"should transition to Running when deployment becomes healthy\", func() {\n\t\t\t\ttestName := \"test-status-running\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation - should create deployment and set to Pending\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying status is Pending\")\n\t\t\t\terr = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Pending\"))\n\n\t\t\t\tBy(\"Simulating healthy deployment by updating deployment status\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// Simulate healthy deployment status\n\t\t\t\tdeployment.Status.ReadyReplicas = 1\n\t\t\t\tdeployment.Status.AvailableReplicas = 1\n\t\t\t\tdeployment.Status.UpdatedReplicas = 1\n\t\t\t\tdeployment.Status.Replicas = 1\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:   appsv1.DeploymentAvailable,\n\t\t\t\t\t\tStatus: corev1.ConditionTrue,\n\t\t\t\t\t\tReason: \"MinimumReplicasAvailable\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:   appsv1.DeploymentProgressing,\n\t\t\t\t\t\tStatus: corev1.ConditionTrue,\n\t\t\t\t\t\tReason: \"NewReplicaSetAvailable\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Second reconciliation - should detect healthy deployment and set to Running\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying status is now Running\")\n\t\t\t\terr = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Running\"))\n\t\t\t\tExpect(llamaDeploy.Status.Message).To(Equal(\"Deployment is healthy and running\"))\n\t\t\t})\n\n\t\t\tIt(\"should transition to RollingOut during rolling update\", func() {\n\t\t\t\ttestName := \"test-status-rollingout\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Simulating rolling update in progress\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// Simulate rolling update: old pods still available, but not all updated\n\t\t\t\tdeployment.Status.ReadyReplicas = 1     // Old pods are ready\n\t\t\t\tdeployment.Status.AvailableReplicas = 1 // Old pods are available\n\t\t\t\tdeployment.Status.UpdatedReplicas = 1   // New pod created but not ready\n\t\t\t\tdeployment.Status.Replicas = 2          // Total: 1 old + 1 new\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:   appsv1.DeploymentAvailable,\n\t\t\t\t\t\tStatus: corev1.ConditionTrue,\n\t\t\t\t\t\tReason: \"MinimumReplicasAvailable\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:   appsv1.DeploymentProgressing,\n\t\t\t\t\t\tStatus: corev1.ConditionTrue,\n\t\t\t\t\t\tReason: \"ReplicaSetUpdated\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling during rolling update\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying status is RollingOut\")\n\t\t\t\terr = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RollingOut\"))\n\t\t\t\tExpect(llamaDeploy.Status.Message).To(ContainSubstring(\"Rolling update in progress (1/1 pods ready, 2 total)\"))\n\t\t\t})\n\n\t\t\tIt(\"should transition to RolloutFailed when new deployment fails but old pods are available\", func() {\n\t\t\t\ttestName := \"test-status-rolloutfailed\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Simulating rollout failure with old pods still available\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// Simulate failed rollout: old pods available, progress failed\n\t\t\t\tdeployment.Status.ReadyReplicas = 1     // Old pods ready\n\t\t\t\tdeployment.Status.AvailableReplicas = 1 // Old pods still available\n\t\t\t\tdeployment.Status.UpdatedReplicas = 0   // New pods failed to update\n\t\t\t\tdeployment.Status.Replicas = 1\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:   appsv1.DeploymentProgressing,\n\t\t\t\t\t\tStatus: corev1.ConditionFalse, // Progress failed\n\t\t\t\t\t\tReason: \"ProgressDeadlineExceeded\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling during rollout failure\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying status is RolloutFailed\")\n\t\t\t\terr = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\t\t\t\tExpect(llamaDeploy.Status.Message).To(ContainSubstring(\"Deployment rollout failed but 1 pods from previous version are still serving traffic\"))\n\t\t\t})\n\n\t\t\tIt(\"should transition to Failed when deployment fails with no available pods\", func() {\n\t\t\t\ttestName := \"test-status-failed-no-pods\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Simulating complete deployment failure\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// Simulate complete failure: no pods available\n\t\t\t\tdeployment.Status.ReadyReplicas = 0\n\t\t\t\tdeployment.Status.AvailableReplicas = 0 // No pods available\n\t\t\t\tdeployment.Status.UpdatedReplicas = 0\n\t\t\t\tdeployment.Status.Replicas = 0\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:   appsv1.DeploymentProgressing,\n\t\t\t\t\t\tStatus: corev1.ConditionFalse,\n\t\t\t\t\t\tReason: \"ProgressDeadlineExceeded\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling during complete failure\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying status is Failed\")\n\t\t\t\terr = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Failed\"))\n\t\t\t\tExpect(llamaDeploy.Status.Message).To(Equal(\"Deployment has failed and no pods are available\"))\n\t\t\t})\n\n\t\t\tIt(\"should handle RolloutFailed when some pods are ready but progress failed\", func() {\n\t\t\t\ttestName := \"test-rollout-failed-partial\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Simulating partial rollout failure\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// Simulate scenario: some pods ready but not all, and progress failed\n\t\t\t\tdeployment.Status.ReadyReplicas = 2     // Old pods ready\n\t\t\t\tdeployment.Status.AvailableReplicas = 2 // Some old pods still available\n\t\t\t\tdeployment.Status.UpdatedReplicas = 0   // No updated replicas\n\t\t\t\tdeployment.Status.Replicas = 2\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:   appsv1.DeploymentProgressing,\n\t\t\t\t\t\tStatus: corev1.ConditionFalse,\n\t\t\t\t\t\tReason: \"ProgressDeadlineExceeded\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling during partial rollout failure\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying status is RolloutFailed with correct message\")\n\t\t\t\terr = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\t\t\t\tExpect(llamaDeploy.Status.Message).To(ContainSubstring(\"Deployment rollout failed but 2 pods from previous version are still serving traffic\"))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Update detection\", func() {\n\t\t\tIt(\"should detect and handle deployment updates\", func() {\n\t\t\t\ttestName := \"test-update-detection\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Modifying the deployment pod template annotation directly\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\tif deployment.Spec.Template.Annotations == nil {\n\t\t\t\t\tdeployment.Spec.Template.Annotations = map[string]string{}\n\t\t\t\t}\n\t\t\t\t// Add an unmanaged annotation that the operator does not own\n\t\t\t\tdeployment.Spec.Template.Annotations[\"test.example/extra\"] = \"present\"\n\t\t\t\tExpect(k8sClient.Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Second reconciliation should succeed without conflicts and preserve unmanaged annotation\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying operator-owned and unmanaged annotations are correct\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tif deployment.Spec.Template.Annotations == nil {\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\treturn deployment.Spec.Template.Annotations[\"deploy.llamaindex.ai/git-source\"]\n\t\t\t\t}).Should(ContainSubstring(testRepoURL))\n\n\t\t\t\tEventually(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tif deployment.Spec.Template.Annotations == nil {\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\treturn deployment.Spec.Template.Annotations[\"test.example/extra\"]\n\t\t\t\t}).Should(Equal(\"present\"))\n\t\t\t})\n\n\t\t\tIt(\"should force ownership on schema migration and override replicas\", func() {\n\t\t\t\ttestName := \"test-update-detection-migration\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\toriginalReplicas := *deployment.Spec.Replicas\n\n\t\t\t\tBy(\"Modifying the deployment replicas and lowering schema version to trigger migration\")\n\t\t\t\tnewReplicas := originalReplicas + 1\n\t\t\t\tdeployment.Spec.Replicas = &newReplicas\n\t\t\t\tExpect(k8sClient.Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\t// Lower the status schema version to simulate a controller schema bump\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Status.SchemaVersion = \"0\"\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Second reconciliation should force ownership and restore replicas\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying deployment was restored to original replica count\")\n\t\t\t\tEventually(func() int32 {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\treturn *deployment.Spec.Replicas\n\t\t\t\t}).Should(Equal(originalReplicas))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Owner references\", func() {\n\t\t\tIt(\"should set owner references on created resources\", func() {\n\t\t\t\ttestName := \"test-owner-references\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying ServiceAccount has correct owner reference\")\n\t\t\t\tserviceAccount := &corev1.ServiceAccount{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-sa\", Namespace: testNamespace}, serviceAccount)\n\t\t\t\t}).Should(Succeed())\n\t\t\t\tExpect(serviceAccount.OwnerReferences).To(HaveLen(1))\n\t\t\t\tExpect(serviceAccount.OwnerReferences[0].Name).To(Equal(testName))\n\n\t\t\t\tBy(\"Verifying Deployment has correct owner reference\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\t\t\t\tExpect(deployment.OwnerReferences).To(HaveLen(1))\n\t\t\t\tExpect(deployment.OwnerReferences[0].Name).To(Equal(testName))\n\n\t\t\t\tBy(\"Verifying Service has correct owner reference\")\n\t\t\t\tservice := &corev1.Service{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, service)\n\t\t\t\t}).Should(Succeed())\n\t\t\t\tExpect(service.OwnerReferences).To(HaveLen(1))\n\t\t\t\tExpect(service.OwnerReferences[0].Name).To(Equal(testName))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Resource deletion\", func() {\n\t\t\tIt(\"should handle graceful deletion\", func() {\n\t\t\t\ttestName := \"test-deletion\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Deleting the LlamaDeployment\")\n\t\t\t\tExpect(k8sClient.Delete(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after deletion should not error\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t})\n\n\t\t\tIt(\"should handle non-existent resource gracefully\", func() {\n\t\t\t\tBy(\"Reconciling a non-existent resource\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: \"non-existent\", Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Environment variables\", func() {\n\t\t\tIt(\"should set correct environment variables in deployment\", func() {\n\t\t\t\ttestName := \"test-env-vars\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource with custom deployment file path\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId:          testProjectID,\n\t\t\t\t\t\tRepoUrl:            testRepoURL,\n\t\t\t\t\t\tGitRef:             testGitRef,\n\t\t\t\t\t\tGitSha:             testGitSha,\n\t\t\t\t\t\tDeploymentFilePath: \"custom_deployment.yml\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Completing the build\")\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying environment variables in deployment\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// Read envs from the app container (named APP)\n\t\t\t\tcontainer, ok := FindContainer(deployment.Spec.Template.Spec, APP)\n\t\t\t\tExpect(ok).To(BeTrue())\n\t\t\t\tenvVars := make(map[string]string)\n\t\t\t\tfor _, env := range container.Env {\n\t\t\t\t\tif env.Value != \"\" {\n\t\t\t\t\t\tenvVars[env.Name] = env.Value\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// LLAMA_DEPLOY_REPO_URL should now point to the build API endpoint with embedded auth token\n\t\t\t\texpectedRepoURLPattern := fmt.Sprintf(\"http://llama-agents-build.llama-agents.svc.cluster.local:8001/deployments/%s\", testName)\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_REPO_URL\"]).To(MatchRegexp(expectedRepoURLPattern))\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_GIT_REF\"]).To(Equal(testGitRef))\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_GIT_SHA\"]).To(Equal(testGitSha))\n\t\t\t\t// LLAMA_DEPLOY_BUILD_API_HOST should be set to the build API host\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_BUILD_API_HOST\"]).To(Equal(\"llama-agents-build.llama-agents.svc.cluster.local:8001\"))\n\t\t\t\t// LLAMA_DEPLOY_AUTH_TOKEN should be set\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_AUTH_TOKEN\"]).NotTo(BeEmpty())\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_DEPLOYMENT_FILE_PATH\"]).To(Equal(\"custom_deployment.yml\"))\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_DEPLOYMENT_NAME\"]).To(Equal(testName))\n\t\t\t})\n\n\t\t\tIt(\"should set envFrom when secret is specified\", func() {\n\t\t\t\ttestName := \"test-envfrom\"\n\t\t\t\ttestSecretName := \"test-multi-secret\"\n\n\t\t\t\tBy(\"Creating a test secret with multiple keys\")\n\t\t\t\tsecret := &corev1.Secret{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testSecretName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tData: map[string][]byte{\n\t\t\t\t\t\t\"GITHUB_PAT\": []byte(\"test-token\"),\n\t\t\t\t\t\t\"OPENAI_KEY\": []byte(\"openai-key\"),\n\t\t\t\t\t\t\"CUSTOM_VAR\": []byte(\"custom-value\"),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, secret)).To(Succeed())\n\n\t\t\t\tdefer cleanupSecret(testSecretName)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment with secret reference\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId:  testProjectID,\n\t\t\t\t\t\tRepoUrl:    testRepoURL,\n\t\t\t\t\t\tGitRef:     testGitRef,\n\t\t\t\t\t\tSecretName: testSecretName,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying envFrom is set in deployment\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// App container envFrom should reference the secret\n\t\t\t\tcontainer, ok := FindContainer(deployment.Spec.Template.Spec, APP)\n\t\t\t\tExpect(ok).To(BeTrue())\n\t\t\t\tExpect(container.EnvFrom).To(HaveLen(1))\n\t\t\t\tExpect(container.EnvFrom[0].SecretRef).NotTo(BeNil())\n\t\t\t\tExpect(container.EnvFrom[0].SecretRef.Name).To(Equal(testSecretName))\n\n\t\t\t\t// Verify the new environment variables are also set\n\t\t\t\tenvVars := make(map[string]string)\n\t\t\t\tfor _, env := range container.Env {\n\t\t\t\t\tif env.Value != \"\" {\n\t\t\t\t\t\tenvVars[env.Name] = env.Value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_BUILD_API_HOST\"]).To(Equal(\"llama-agents-build.llama-agents.svc.cluster.local:8001\"))\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_AUTH_TOKEN\"]).NotTo(BeEmpty())\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Container image configuration\", func() {\n\t\t\tIt(\"should resolve image and tag from spec and defaults\", func() {\n\t\t\t\tcases := []struct {\n\t\t\t\t\tname     string\n\t\t\t\t\topts     []LlamaSpecOption\n\t\t\t\t\texpected string\n\t\t\t\t}{\n\t\t\t\t\t{\"default-image\", []LlamaSpecOption{}, \"llamaindex/llama-agents-appserver:latest\"},\n\t\t\t\t\t{\"custom-image\", []LlamaSpecOption{WithImage(\"custom/llama-deploy\"), WithImageTag(\"v2.0.0\")}, \"custom/llama-deploy:v2.0.0\"},\n\t\t\t\t\t{\"custom-image-only\", []LlamaSpecOption{WithImage(\"custom/llama-deploy\")}, \"custom/llama-deploy:latest\"},\n\t\t\t\t\t{\"custom-tag-only\", []LlamaSpecOption{WithImageTag(\"v3.0.0\")}, \"llamaindex/llama-agents-appserver:v3.0.0\"},\n\t\t\t\t}\n\n\t\t\t\tfor _, tc := range cases {\n\t\t\t\t\ttestName := \"test-\" + tc.name\n\t\t\t\t\tld := NewLlama(testName, testNamespace, testProjectID, testRepoURL, append(tc.opts, WithGitRef(testGitRef))...)\n\t\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\t\tExpect(deployment.Spec.Template.Spec.Containers).To(HaveLen(2))\n\t\t\t\t\tappContainer, ok := FindContainer(deployment.Spec.Template.Spec, APP)\n\t\t\t\t\tExpect(ok).To(BeTrue())\n\t\t\t\t\tExpect(appContainer.Image).To(Equal(tc.expected))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should detect image changes and trigger deployment updates\", func() {\n\t\t\t\ttestName := \"test-image-update\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource with initial image\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t\tImage:     \"initial/image\",\n\t\t\t\t\t\tImageTag:  \"v1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Initial reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying initial deployment image\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\t\t\t\t// Verify initial app container image\n\t\t\t\tvar appContainer corev1.Container\n\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\tappContainer = c\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(appContainer.Image).To(Equal(\"initial/image:v1.0.0\"))\n\t\t\t\tinitialResourceVersion := deployment.ResourceVersion\n\n\t\t\t\tBy(\"Updating the image in spec\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Spec.Image = \"updated/image\"\n\t\t\t\tllamaDeploy.Spec.ImageTag = \"v2.0.0\"\n\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after image change\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying deployment was updated with new image\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\t\treturn c.Image\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t}).Should(Equal(\"updated/image:v2.0.0\"))\n\n\t\t\t\tBy(\"Verifying deployment resource version changed\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.ResourceVersion).NotTo(Equal(initialResourceVersion))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Git reference functionality\", func() {\n\t\t\tIt(\"should trigger deployment update when git ref changes\", func() {\n\t\t\t\ttestName := \"test-git-ref-change\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource with initial git ref\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    \"main\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying initial deployment was created with main branch\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\t// Store initial resource version to verify update\n\t\t\t\tinitialResourceVersion := deployment.ResourceVersion\n\n\t\t\t\tvar container corev1.Container\n\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\tcontainer = c\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tenvVars := make(map[string]string)\n\t\t\t\tfor _, env := range container.Env {\n\t\t\t\t\tif env.Value != \"\" {\n\t\t\t\t\t\tenvVars[env.Name] = env.Value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\texpectedRepoURLPattern := fmt.Sprintf(\"http://llama-agents-build.llama-agents.svc.cluster.local:8001/deployments/%s\", testName)\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_REPO_URL\"]).To(MatchRegexp(expectedRepoURLPattern))\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_GIT_REF\"]).To(Equal(\"main\"))\n\t\t\t\tExpect(envVars[\"LLAMA_DEPLOY_GIT_SHA\"]).To(BeEmpty())\n\n\t\t\t\tBy(\"Updating the git ref to a different branch\")\n\t\t\t\t// Fetch the latest version of llamaDeploy\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Spec.GitRef = \"feature-branch\"\n\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after git ref change\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying deployment was updated with new git ref\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\t\tfor _, env := range c.Env {\n\t\t\t\t\t\t\t\tif env.Name == LLAMA_DEPLOY_REPO_URL {\n\t\t\t\t\t\t\t\t\treturn env.Value\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t}, \"5s\", \"100ms\").Should(MatchRegexp(fmt.Sprintf(\"http://llama-agents-build.llama-agents.svc.cluster.local:8001/deployments/%s\", testName)))\n\n\t\t\t\tBy(\"Verifying deployment resource version changed (indicating update occurred)\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.ResourceVersion).NotTo(Equal(initialResourceVersion))\n\t\t\t})\n\n\t\t\tIt(\"should use build API URL for LLAMA_DEPLOY_REPO_URL regardless of spec changes\", func() {\n\t\t\t\ttestName := \"test-build-api-url\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   \"https://github.com/original/repo.git\",\n\t\t\t\t\t\tGitRef:    \"v1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Initial reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying deployment uses build API URL for LLAMA_DEPLOY_REPO_URL\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\texpectedRepoURLPattern := fmt.Sprintf(\"http://llama-agents-build.llama-agents.svc.cluster.local:8001/deployments/%s\", testName)\n\t\t\t\tEventually(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\t\tfor _, env := range c.Env {\n\t\t\t\t\t\t\t\tif env.Name == LLAMA_DEPLOY_REPO_URL {\n\t\t\t\t\t\t\t\t\treturn env.Value\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t}, \"5s\", \"100ms\").Should(MatchRegexp(expectedRepoURLPattern))\n\t\t\t\tEventually(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\t\tfor _, env := range c.Env {\n\t\t\t\t\t\t\t\tif env.Name == \"LLAMA_DEPLOY_BUILD_API_HOST\" {\n\t\t\t\t\t\t\t\t\treturn env.Value\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t}, \"5s\", \"100ms\").Should(Equal(\"llama-agents-build.llama-agents.svc.cluster.local:8001\"))\n\t\t\t\tEventually(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\t\tfor _, env := range c.Env {\n\t\t\t\t\t\t\t\tif env.Name == \"LLAMA_DEPLOY_AUTH_TOKEN\" {\n\t\t\t\t\t\t\t\t\treturn env.Value\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t}, \"5s\", \"100ms\").ShouldNot(BeEmpty())\n\n\t\t\t\tBy(\"Updating repo URL and git ref in spec\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Spec.RepoUrl = \"https://github.com/updated/repo.git\"\n\t\t\t\tllamaDeploy.Spec.GitRef = \"v2.0.0\"\n\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after spec change\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying LLAMA_DEPLOY_REPO_URL still uses build API (spec changes don't affect LLAMA_DEPLOY_REPO_URL)\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\t\tfor _, env := range c.Env {\n\t\t\t\t\t\t\t\tif env.Name == LLAMA_DEPLOY_REPO_URL {\n\t\t\t\t\t\t\t\t\treturn env.Value\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t}, \"5s\", \"100ms\").Should(MatchRegexp(fmt.Sprintf(\"http://llama-agents-build.llama-agents.svc.cluster.local:8001/deployments/%s\", testName)))\n\t\t\t})\n\n\t\t\tIt(\"should detect deployment file path changes and trigger updates\", func() {\n\t\t\t\ttestName := \"test-deployment-file-path-change\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource with initial deployment file path\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId:          testProjectID,\n\t\t\t\t\t\tRepoUrl:            testRepoURL,\n\t\t\t\t\t\tGitRef:             testGitRef,\n\t\t\t\t\t\tDeploymentFilePath: \"deploy.yml\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Initial reconciliation\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Getting initial deployment state\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\tinitialResourceVersion := deployment.ResourceVersion\n\n\t\t\t\tBy(\"Updating deployment file path\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Spec.DeploymentFilePath = \"production_deploy.yml\"\n\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after deployment file path change\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying deployment was updated due to critical environment variable change\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\treturn deployment.ResourceVersion\n\t\t\t\t}).ShouldNot(Equal(initialResourceVersion))\n\n\t\t\t\tBy(\"Verifying LLAMA_DEPLOY_DEPLOYMENT_FILE_PATH environment variable\")\n\t\t\t\tEventually(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\t\tif c.Name == APP {\n\t\t\t\t\t\t\tfor _, env := range c.Env {\n\t\t\t\t\t\t\t\tif env.Name == \"LLAMA_DEPLOY_DEPLOYMENT_FILE_PATH\" {\n\t\t\t\t\t\t\t\t\treturn env.Value\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t}, \"5s\", \"100ms\").Should(Equal(\"production_deploy.yml\"))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Rollout timeout\", func() {\n\t\t\tIt(\"should set rolloutStartedAt when phase is Pending\", func() {\n\t\t\t\ttestName := \"test-rollout-started-at\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\tresult, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tBy(\"Verifying RequeueAfter is set for timeout check\")\n\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\">\", 0))\n\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying rolloutStartedAt is set\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Pending\"))\n\t\t\t\tExpect(llamaDeploy.Status.RolloutStartedAt).NotTo(BeNil())\n\n\t\t\t})\n\n\t\t\tIt(\"should clear rolloutStartedAt when deployment becomes Running\", func() {\n\t\t\t\ttestName := \"test-rollout-started-cleared\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to Pending\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Simulating healthy deployment\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)\n\t\t\t\t}).Should(Succeed())\n\t\t\t\tdeployment.Status.ReadyReplicas = 1\n\t\t\t\tdeployment.Status.AvailableReplicas = 1\n\t\t\t\tdeployment.Status.UpdatedReplicas = 1\n\t\t\t\tdeployment.Status.Replicas = 1\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{Type: appsv1.DeploymentAvailable, Status: corev1.ConditionTrue, Reason: \"MinimumReplicasAvailable\"},\n\t\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue, Reason: \"NewReplicaSetAvailable\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling again — should transition to Running\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying rolloutStartedAt is cleared\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Running\"))\n\t\t\t\tExpect(llamaDeploy.Status.RolloutStartedAt).To(BeNil())\n\t\t\t})\n\n\t\t\tIt(\"should mark RolloutFailed on timeout and set FailedRolloutGeneration\", func() {\n\t\t\t\ttestName := \"test-rollout-timeout\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"30\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment with short rollout timeout\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"First reconciliation — should set rolloutStartedAt\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.RolloutStartedAt).NotTo(BeNil())\n\n\t\t\t\tBy(\"Setting available replicas so timeout picks RolloutFailed (not Failed)\")\n\t\t\t\tSetDeploymentAvailableReplicas(ctx, testName, testNamespace, 1)\n\n\t\t\t\tBy(\"Backdating rolloutStartedAt to simulate timeout\")\n\t\t\t\tpast := metav1.NewTime(time.Now().Add(-60 * time.Second))\n\t\t\t\tllamaDeploy.Status.RolloutStartedAt = &past\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after timeout — should transition to RolloutFailed\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying phase and failedRolloutGeneration\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\t\t\t\tExpect(llamaDeploy.Status.Message).To(ContainSubstring(\"Rollout timed out\"))\n\t\t\t\tExpect(llamaDeploy.Status.RolloutStartedAt).To(BeNil())\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(llamaDeploy.Generation))\n\t\t\t})\n\n\t\t\tIt(\"should not re-trigger reconciliation for the same failed generation\", func() {\n\t\t\t\ttestName := \"test-rollout-no-retrigger\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"30\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment and timing it out\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t// Reconcile to set rolloutStartedAt\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\t// Set available replicas so timeout picks RolloutFailed\n\t\t\t\tSetDeploymentAvailableReplicas(ctx, testName, testNamespace, 1)\n\n\t\t\t\t// Backdate and timeout\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tpast := metav1.NewTime(time.Now().Add(-60 * time.Second))\n\t\t\t\tllamaDeploy.Status.RolloutStartedAt = &past\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying phase is RolloutFailed\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\n\t\t\t\tBy(\"Reconciling again; phase should remain RolloutFailed and not re-trigger\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tConsistently(func() string {\n\t\t\t\t\t_ = k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)\n\t\t\t\t\treturn llamaDeploy.Status.Phase\n\t\t\t\t}, \"1s\", \"100ms\").Should(Equal(\"RolloutFailed\"))\n\t\t\t})\n\n\t\t\tIt(\"should retry rollout when spec is updated after timeout failure\", func() {\n\t\t\t\ttestName := \"test-rollout-retry-after-timeout\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"30\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment and timing it out\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t\tGitSha:    testGitSha,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t// Reconcile to create build, then complete it\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\t// Set available replicas so timeout picks RolloutFailed\n\t\t\t\tSetDeploymentAvailableReplicas(ctx, testName, testNamespace, 1)\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tpast := metav1.NewTime(time.Now().Add(-60 * time.Second))\n\t\t\t\tllamaDeploy.Status.RolloutStartedAt = &past\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\t\t\t\tfailedGen := llamaDeploy.Generation\n\n\t\t\t\tBy(\"Updating the spec to trigger a new generation\")\n\t\t\t\tllamaDeploy.Spec.GitSha = testGitSha2\n\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\t// After update, generation should change\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Generation).To(BeNumerically(\">\", failedGen))\n\n\t\t\t\tBy(\"Reconciling with new spec — should start a new build\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Pending\"))\n\t\t\t})\n\n\t\t\tIt(\"should set progressDeadlineSeconds on the Kubernetes Deployment\", func() {\n\t\t\t\ttestName := \"test-progress-deadline\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"120\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment with custom rollout timeout\")\n\t\t\t\tld := NewLlama(testName, testNamespace, testProjectID, testRepoURL,\n\t\t\t\t\tWithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tBy(\"Verifying progressDeadlineSeconds on the Deployment\")\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(dep.Spec.ProgressDeadlineSeconds).NotTo(BeNil())\n\t\t\t\tExpect(*dep.Spec.ProgressDeadlineSeconds).To(Equal(int32(120)))\n\t\t\t})\n\n\t\t\tIt(\"should set default progressDeadlineSeconds when rolloutTimeoutSeconds is not specified\", func() {\n\t\t\t\ttestName := \"test-progress-deadline-default\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment without rollout timeout\")\n\t\t\t\tld := NewLlama(testName, testNamespace, testProjectID, testRepoURL,\n\t\t\t\t\tWithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tBy(\"Verifying progressDeadlineSeconds is set to default\")\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(dep.Spec.ProgressDeadlineSeconds).NotTo(BeNil())\n\t\t\t\tExpect(*dep.Spec.ProgressDeadlineSeconds).To(Equal(DefaultRolloutTimeoutSeconds))\n\t\t\t})\n\n\t\t\tIt(\"should scale down failing ReplicaSet on ProgressDeadlineExceeded\", func() {\n\t\t\t\ttestName := \"test-rs-scaledown-progress\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Getting the Deployment\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\tBy(\"Creating an old ReplicaSet (healthy, serving traffic)\")\n\t\t\t\toldRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-old\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, oldRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, oldRS) }()\n\t\t\t\toldRS.Status.Replicas = 1\n\t\t\t\toldRS.Status.AvailableReplicas = 1\n\t\t\t\toldRS.Status.ReadyReplicas = 1\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, oldRS)).To(Succeed())\n\n\t\t\t\tBy(\"Creating a new ReplicaSet (failing, crash-looping)\")\n\t\t\t\tnewRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-new\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, newRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, newRS) }()\n\t\t\t\tnewRS.Status.Replicas = 1\n\t\t\t\tnewRS.Status.AvailableReplicas = 0 // not available (crash-looping)\n\t\t\t\tnewRS.Status.ReadyReplicas = 0\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, newRS)).To(Succeed())\n\n\t\t\t\tBy(\"Simulating ProgressDeadlineExceeded on the Deployment\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tdeployment.Status.ReadyReplicas = 1\n\t\t\t\tdeployment.Status.AvailableReplicas = 1\n\t\t\t\tdeployment.Status.Replicas = 2\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionFalse, Reason: \"ProgressDeadlineExceeded\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should scale down the new RS and mark RolloutFailed\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying phase is RolloutFailed\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(llamaDeploy.Generation))\n\n\t\t\t\tBy(\"Verifying the Deployment is paused\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeTrue())\n\n\t\t\t\tBy(\"Verifying the new ReplicaSet was scaled to 0\")\n\t\t\t\tEventually(func() int32 {\n\t\t\t\t\trs := &appsv1.ReplicaSet{}\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-new\", Namespace: testNamespace}, rs)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn -1\n\t\t\t\t\t}\n\t\t\t\t\tif rs.Spec.Replicas == nil {\n\t\t\t\t\t\treturn 1\n\t\t\t\t\t}\n\t\t\t\t\treturn *rs.Spec.Replicas\n\t\t\t\t}).Should(Equal(int32(0)))\n\n\t\t\t\tBy(\"Verifying the old ReplicaSet is untouched\")\n\t\t\t\tupdatedOldRS := &appsv1.ReplicaSet{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-old\", Namespace: testNamespace}, updatedOldRS)).To(Succeed())\n\t\t\t\tExpect(*updatedOldRS.Spec.Replicas).To(Equal(int32(1)))\n\t\t\t})\n\n\t\t\tIt(\"should scale down the only RS when it is unhealthy\", func() {\n\t\t\t\ttestName := \"test-rs-scaledown-all-unhealthy\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Getting the Deployment\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\tBy(\"Creating a single ReplicaSet that is completely unhealthy (crash-looping)\")\n\t\t\t\tunhealthyRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-only\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, unhealthyRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, unhealthyRS) }()\n\t\t\t\t// Zero available replicas — everything is crash-looping\n\t\t\t\tunhealthyRS.Status.Replicas = 1\n\t\t\t\tunhealthyRS.Status.AvailableReplicas = 0\n\t\t\t\tunhealthyRS.Status.ReadyReplicas = 0\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, unhealthyRS)).To(Succeed())\n\n\t\t\t\tBy(\"Simulating ProgressDeadlineExceeded on the Deployment\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tdeployment.Status.ReadyReplicas = 0\n\t\t\t\tdeployment.Status.AvailableReplicas = 0\n\t\t\t\tdeployment.Status.Replicas = 1\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionFalse, Reason: \"ProgressDeadlineExceeded\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should scale down even the only RS since it is unhealthy\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying phase is Failed (no available replicas)\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Failed\"))\n\n\t\t\t\tBy(\"Verifying the Deployment was scaled to 0 replicas (PhaseFailed uses replicas, not RS manipulation)\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(*deployment.Spec.Replicas).To(Equal(int32(0)))\n\t\t\t})\n\n\t\t\tIt(\"should scale down the only RS when phase is Failed even if AvailableReplicas > 0\", func() {\n\t\t\t\ttestName := \"test-rs-scaledown-failed-avail\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Getting the Deployment\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\tBy(\"Creating a single ReplicaSet with AvailableReplicas > 0 (crash-looping but briefly available)\")\n\t\t\t\trs := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-only\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, rs)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, rs) }()\n\t\t\t\t// AvailableReplicas > 0 but deployment-level status says Failed\n\t\t\t\trs.Status.Replicas = 1\n\t\t\t\trs.Status.AvailableReplicas = 1\n\t\t\t\trs.Status.ReadyReplicas = 1\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, rs)).To(Succeed())\n\n\t\t\t\tBy(\"Simulating ProgressDeadlineExceeded with zero available at deployment level\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tdeployment.Status.ReadyReplicas = 0\n\t\t\t\tdeployment.Status.AvailableReplicas = 0\n\t\t\t\tdeployment.Status.Replicas = 1\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionFalse, Reason: \"ProgressDeadlineExceeded\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should scale down the RS despite AvailableReplicas > 0 because phase is Failed\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying phase is Failed\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"Failed\"))\n\n\t\t\t\tBy(\"Verifying the Deployment was scaled to 0 replicas (PhaseFailed uses replicas, not RS manipulation)\")\n\t\t\t\tdeployment2 := &appsv1.Deployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment2)).To(Succeed())\n\t\t\t\tExpect(*deployment2.Spec.Replicas).To(Equal(int32(0)))\n\t\t\t})\n\n\t\t\tIt(\"should skip reconciliation for PhaseFailed with matching FailedRolloutGeneration\", func() {\n\t\t\t\ttestName := \"test-skip-failed-gen\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Setting the LlamaDeployment to PhaseFailed with FailedRolloutGeneration == Generation\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Status.Phase = PhaseFailed\n\t\t\t\tllamaDeploy.Status.FailedRolloutGeneration = llamaDeploy.Generation\n\t\t\t\tllamaDeploy.Status.Message = \"Deployment has failed\"\n\t\t\t\tnow := metav1.Now()\n\t\t\t\tllamaDeploy.Status.LastUpdated = &now\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Re-reconciling — should skip reconciliation entirely\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying the Deployment is not paused (PhaseFailed uses replicas, not pause)\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeFalse())\n\t\t\t})\n\n\t\t\tIt(\"should scale deployment to 0 replicas when PhaseFailed is detected\", func() {\n\t\t\t\ttestName := \"test-failed-replicas-zero\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Simulating a failed deployment (Progressing=False, 0 available replicas)\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    appsv1.DeploymentProgressing,\n\t\t\t\t\t\tStatus:  corev1.ConditionFalse,\n\t\t\t\t\t\tReason:  \"ProgressDeadlineExceeded\",\n\t\t\t\t\t\tMessage: \"deadline exceeded\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tdeployment.Status.AvailableReplicas = 0\n\t\t\t\tdeployment.Status.ReadyReplicas = 0\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should detect PhaseFailed and set replicas=0\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying the Deployment has replicas=0 and is NOT paused\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(*deployment.Spec.Replicas).To(Equal(int32(0)))\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeFalse())\n\n\t\t\t\tBy(\"Verifying FailedRolloutGeneration is set\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(llamaDeploy.Generation))\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(PhaseFailed))\n\t\t\t})\n\n\t\t\tIt(\"should restore replicas to 1 when spec is updated after PhaseFailed\", func() {\n\t\t\t\ttestName := \"test-failed-recovery\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment and setting it to PhaseFailed\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t\tGitSha:    testGitSha,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the build and completing it\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Manually setting PhaseFailed with FailedRolloutGeneration and replicas=0\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Status.Phase = PhaseFailed\n\t\t\t\tllamaDeploy.Status.FailedRolloutGeneration = llamaDeploy.Generation\n\t\t\t\tnow := metav1.Now()\n\t\t\t\tllamaDeploy.Status.LastUpdated = &now\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tpatch := client.MergeFrom(deployment.DeepCopy())\n\t\t\t\tdeployment.Spec.Replicas = ptr(int32(0))\n\t\t\t\tExpect(k8sClient.Patch(ctx, deployment, patch)).To(Succeed())\n\n\t\t\t\tBy(\"Updating the spec to trigger a new generation\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Spec.GitSha = testGitSha2\n\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after spec update — should start build, then restore replicas to 1\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying the Deployment has replicas=1\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(*deployment.Spec.Replicas).To(Equal(int32(1)))\n\t\t\t})\n\n\t\t\tIt(\"should unpause the Deployment when spec is updated after timeout\", func() {\n\t\t\t\ttestName := \"test-unpause-after-timeout\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"30\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating and timing out a LlamaDeployment\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t\tGitSha:    testGitSha,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t// Reconcile to create build, complete it, then proceed\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\t// Set available replicas so timeout picks RolloutFailed (pauses deployment)\n\t\t\t\tSetDeploymentAvailableReplicas(ctx, testName, testNamespace, 1)\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tpast := metav1.NewTime(time.Now().Add(-60 * time.Second))\n\t\t\t\tllamaDeploy.Status.RolloutStartedAt = &past\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying the Deployment is paused\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeTrue())\n\n\t\t\t\tBy(\"Updating the spec to trigger a new generation\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Spec.GitSha = testGitSha2\n\t\t\t\tExpect(k8sClient.Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after spec update — should start build, then unpause and proceed\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying the Deployment is no longer paused\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeFalse())\n\t\t\t})\n\n\t\t\tIt(\"should NOT unpause the Deployment when re-reconciling the same failed generation\", func() {\n\t\t\t\ttestName := \"test-no-unpause-same-gen\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"30\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment and getting it to RolloutFailed state\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t\tGitSha:    testGitSha,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t// Initial reconcile to create build, then complete it\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\t// Set available replicas so timeout picks RolloutFailed\n\t\t\t\tSetDeploymentAvailableReplicas(ctx, testName, testNamespace, 1)\n\n\t\t\t\t// Backdate rollout start to trigger timeout\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tpast := metav1.NewTime(time.Now().Add(-60 * time.Second))\n\t\t\t\tllamaDeploy.Status.RolloutStartedAt = &past\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\t// Reconcile to trigger timeout — this pauses the deployment\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying the Deployment is paused and phase is RolloutFailed\")\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeTrue())\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(PhaseRolloutFailed))\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(llamaDeploy.Generation))\n\n\t\t\t\tBy(\"Re-reconciling the SAME generation (simulates a racing reconcile)\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying the Deployment is STILL paused — must not unpause for same generation\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeTrue(), \"Deployment should remain paused when re-reconciling a failed generation\")\n\t\t\t})\n\n\t\t\tIt(\"should NOT unpause the Deployment when reconciling with stale cache (FailedRolloutGeneration not yet visible)\", func() {\n\t\t\t\t// This reproduces the race condition observed in production:\n\t\t\t\t// 1. Reconcile A: timeout fires, pauses deployment, scales down RS, sets FailedRolloutGeneration\n\t\t\t\t// 2. Reconcile B: triggered by the deployment pause event, reads stale LlamaDeployment\n\t\t\t\t//    where FailedRolloutGeneration is NOT set yet — unpauses the deployment, undoing the rollback\n\t\t\t\ttestName := \"test-no-unpause-stale\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"30\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t\tGitSha:    testGitSha,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t// Initial reconcile to create build, then complete it\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Manually setting up the stale-cache state: deployment paused, but FailedRolloutGeneration=0\")\n\t\t\t\t// Pause the Deployment (as the timeout handler would)\n\t\t\t\tdeployment := &appsv1.Deployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tpatch := client.MergeFrom(deployment.DeepCopy())\n\t\t\t\tdeployment.Spec.Paused = true\n\t\t\t\tExpect(k8sClient.Patch(ctx, deployment, patch)).To(Succeed())\n\n\t\t\t\t// Set phase to RollingOut (the phase the stale reconcile would see\n\t\t\t\t// since it reads the object before the timeout handler's status update lands)\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Status.Phase = PhaseRollingOut\n\t\t\t\tllamaDeploy.Status.FailedRolloutGeneration = 0 // stale: not yet updated\n\t\t\t\tnow := metav1.Now()\n\t\t\t\tllamaDeploy.Status.RolloutStartedAt = &now\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling with this stale state — must NOT unpause the deployment\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying the Deployment is STILL paused\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeTrue(), \"Deployment must remain paused — reconcileDeployment should not unpause when deployment is paused by timeout handler\")\n\t\t\t})\n\n\t\t\tIt(\"should NOT scale down when pods are evicted (infrastructure failure)\", func() {\n\t\t\t\ttestName := \"test-infra-evicted\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Getting the Deployment\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\tBy(\"Creating an old ReplicaSet (healthy, serving traffic)\")\n\t\t\t\toldRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-old\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, oldRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, oldRS) }()\n\t\t\t\toldRS.Status.Replicas = 1\n\t\t\t\toldRS.Status.AvailableReplicas = 1\n\t\t\t\toldRS.Status.ReadyReplicas = 1\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, oldRS)).To(Succeed())\n\n\t\t\t\tBy(\"Creating a new ReplicaSet (failing)\")\n\t\t\t\tnewRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-new\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, newRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, newRS) }()\n\t\t\t\tnewRS.Status.Replicas = 1\n\t\t\t\tnewRS.Status.AvailableReplicas = 0\n\t\t\t\tnewRS.Status.ReadyReplicas = 0\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, newRS)).To(Succeed())\n\n\t\t\t\tBy(\"Creating an evicted Pod\")\n\t\t\t\tpod := &corev1.Pod{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-evicted\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t{Name: \"app\", Image: \"busybox\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, pod)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, pod) }()\n\t\t\t\tpod.Status.Phase = corev1.PodFailed\n\t\t\t\tpod.Status.Reason = \"Evicted\"\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, pod)).To(Succeed())\n\n\t\t\t\tBy(\"Simulating ProgressDeadlineExceeded on the Deployment\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tdeployment.Status.ReadyReplicas = 1\n\t\t\t\tdeployment.Status.AvailableReplicas = 1\n\t\t\t\tdeployment.Status.Replicas = 2\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionFalse, Reason: \"ProgressDeadlineExceeded\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should NOT scale down due to infrastructure failure\")\n\t\t\t\tresult, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying RequeueAfter is 30 seconds (infrastructure requeue)\")\n\t\t\t\tExpect(result.RequeueAfter).To(Equal(30 * time.Second))\n\n\t\t\t\tBy(\"Verifying FailedRolloutGeneration is NOT set\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(int64(0)))\n\n\t\t\t\tBy(\"Verifying the Deployment is NOT paused\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeFalse())\n\n\t\t\t\tBy(\"Verifying an InfrastructureIssue event was recorded\")\n\t\t\t\trecorder := controllerReconciler.Recorder.(*record.FakeRecorder)\n\t\t\t\tvar found bool\n\t\t\t\tfor len(recorder.Events) > 0 {\n\t\t\t\t\tevent := <-recorder.Events\n\t\t\t\t\tif strings.Contains(event, \"InfrastructureIssue\") {\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(found).To(BeTrue())\n\t\t\t})\n\n\t\t\tIt(\"should scale down when pods have CrashLoopBackOff (app failure)\", func() {\n\t\t\t\ttestName := \"test-infra-crashloop\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Getting the Deployment\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\tBy(\"Creating an old ReplicaSet (healthy, serving traffic)\")\n\t\t\t\toldRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-old\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, oldRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, oldRS) }()\n\t\t\t\toldRS.Status.Replicas = 1\n\t\t\t\toldRS.Status.AvailableReplicas = 1\n\t\t\t\toldRS.Status.ReadyReplicas = 1\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, oldRS)).To(Succeed())\n\n\t\t\t\tBy(\"Creating a new ReplicaSet (failing)\")\n\t\t\t\tnewRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-new\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, newRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, newRS) }()\n\t\t\t\tnewRS.Status.Replicas = 1\n\t\t\t\tnewRS.Status.AvailableReplicas = 0\n\t\t\t\tnewRS.Status.ReadyReplicas = 0\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, newRS)).To(Succeed())\n\n\t\t\t\tBy(\"Creating a Pod with CrashLoopBackOff\")\n\t\t\t\tpod := &corev1.Pod{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-crash\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t{Name: \"app\", Image: \"busybox\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, pod)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, pod) }()\n\t\t\t\tpod.Status.Phase = corev1.PodRunning\n\t\t\t\tpod.Status.ContainerStatuses = []corev1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"app\",\n\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\tWaiting: &corev1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\tReason: \"CrashLoopBackOff\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, pod)).To(Succeed())\n\n\t\t\t\tBy(\"Simulating ProgressDeadlineExceeded on the Deployment\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tdeployment.Status.ReadyReplicas = 1\n\t\t\t\tdeployment.Status.AvailableReplicas = 1\n\t\t\t\tdeployment.Status.Replicas = 2\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionFalse, Reason: \"ProgressDeadlineExceeded\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should proceed with scale-down for app failure\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying FailedRolloutGeneration is set\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(llamaDeploy.Generation))\n\n\t\t\t\tBy(\"Verifying the Deployment is paused\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeTrue())\n\n\t\t\t\tBy(\"Verifying the new ReplicaSet was scaled to 0\")\n\t\t\t\tEventually(func() int32 {\n\t\t\t\t\trs := &appsv1.ReplicaSet{}\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-new\", Namespace: testNamespace}, rs)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn -1\n\t\t\t\t\t}\n\t\t\t\t\tif rs.Spec.Replicas == nil {\n\t\t\t\t\t\treturn 1\n\t\t\t\t\t}\n\t\t\t\t\treturn *rs.Spec.Replicas\n\t\t\t\t}).Should(Equal(int32(0)))\n\t\t\t})\n\n\t\t\tIt(\"should scale down with mixed evicted and CrashLoopBackOff pods\", func() {\n\t\t\t\ttestName := \"test-infra-mixed\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment resource\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling to create the Deployment\")\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Getting the Deployment\")\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\n\t\t\t\tBy(\"Creating an old ReplicaSet (healthy, serving traffic)\")\n\t\t\t\toldRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-old\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, oldRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, oldRS) }()\n\t\t\t\toldRS.Status.Replicas = 1\n\t\t\t\toldRS.Status.AvailableReplicas = 1\n\t\t\t\toldRS.Status.ReadyReplicas = 1\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, oldRS)).To(Succeed())\n\n\t\t\t\tBy(\"Creating a new ReplicaSet (failing)\")\n\t\t\t\tnewRS := &appsv1.ReplicaSet{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-new\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t\tAnnotations: map[string]string{\n\t\t\t\t\t\t\t\"deployment.kubernetes.io/revision\": \"2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAPIVersion: \"apps/v1\",\n\t\t\t\t\t\t\t\tKind:       \"Deployment\",\n\t\t\t\t\t\t\t\tName:       deployment.Name,\n\t\t\t\t\t\t\t\tUID:        deployment.UID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: appsv1.ReplicaSetSpec{\n\t\t\t\t\t\tReplicas: ptr(int32(1)),\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\t\t\t\tMatchLabels: map[string]string{\"app\": testName},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTemplate: deployment.Spec.Template,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, newRS)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, newRS) }()\n\t\t\t\tnewRS.Status.Replicas = 1\n\t\t\t\tnewRS.Status.AvailableReplicas = 0\n\t\t\t\tnewRS.Status.ReadyReplicas = 0\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, newRS)).To(Succeed())\n\n\t\t\t\tBy(\"Creating an evicted Pod\")\n\t\t\t\tevictedPod := &corev1.Pod{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-evicted\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t{Name: \"app\", Image: \"busybox\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, evictedPod)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, evictedPod) }()\n\t\t\t\tevictedPod.Status.Phase = corev1.PodFailed\n\t\t\t\tevictedPod.Status.Reason = \"Evicted\"\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, evictedPod)).To(Succeed())\n\n\t\t\t\tBy(\"Creating a Pod with CrashLoopBackOff\")\n\t\t\t\tcrashPod := &corev1.Pod{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName + \"-crash\",\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t\tLabels:    map[string]string{\"app\": testName},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t{Name: \"app\", Image: \"busybox\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, crashPod)).To(Succeed())\n\t\t\t\tdefer func() { _ = k8sClient.Delete(ctx, crashPod) }()\n\t\t\t\tcrashPod.Status.Phase = corev1.PodRunning\n\t\t\t\tcrashPod.Status.ContainerStatuses = []corev1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"app\",\n\t\t\t\t\t\tState: corev1.ContainerState{\n\t\t\t\t\t\t\tWaiting: &corev1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\tReason: \"CrashLoopBackOff\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, crashPod)).To(Succeed())\n\n\t\t\t\tBy(\"Simulating ProgressDeadlineExceeded on the Deployment\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tdeployment.Status.ReadyReplicas = 1\n\t\t\t\tdeployment.Status.AvailableReplicas = 1\n\t\t\t\tdeployment.Status.Replicas = 2\n\t\t\t\tdeployment.Status.Conditions = []appsv1.DeploymentCondition{\n\t\t\t\t\t{Type: appsv1.DeploymentProgressing, Status: corev1.ConditionFalse, Reason: \"ProgressDeadlineExceeded\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should proceed with scale-down (app failure wins over infra)\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying FailedRolloutGeneration is set\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(llamaDeploy.Generation))\n\n\t\t\t\tBy(\"Verifying the Deployment is paused\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, deployment)).To(Succeed())\n\t\t\t\tExpect(deployment.Spec.Paused).To(BeTrue())\n\n\t\t\t\tBy(\"Verifying the new ReplicaSet was scaled to 0\")\n\t\t\t\tEventually(func() int32 {\n\t\t\t\t\trs := &appsv1.ReplicaSet{}\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-new\", Namespace: testNamespace}, rs)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn -1\n\t\t\t\t\t}\n\t\t\t\t\tif rs.Spec.Replicas == nil {\n\t\t\t\t\t\treturn 1\n\t\t\t\t\t}\n\t\t\t\t\treturn *rs.Spec.Replicas\n\t\t\t\t}).Should(Equal(int32(0)))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"DNS-1035 name validation\", func() {\n\t\t\tIt(\"should mark deployment as Failed and tear down resources for non-compliant name\", func() {\n\t\t\t\t// \"123-invalid-dns\" is valid RFC-1123 (k8s metadata.name) but\n\t\t\t\t// NOT valid DNS-1035 (starts with digit), which Kubernetes\n\t\t\t\t// requires for Service names.\n\t\t\t\ttestName := \"123-invalid-dns\"\n\n\t\t\t\tBy(\"Pre-creating resources that simulate prior operator reconciliation\")\n\t\t\t\tsa := &corev1.ServiceAccount{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName + \"-sa\", Namespace: testNamespace},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, sa)).To(Succeed())\n\n\t\t\t\tcm := &corev1.ConfigMap{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName + \"-nginx-config\", Namespace: testNamespace},\n\t\t\t\t\tData:       map[string]string{\"nginx.conf\": \"fake\"},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, cm)).To(Succeed())\n\n\t\t\t\treplicas := int32(1)\n\t\t\t\tdep := &appsv1.Deployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec: appsv1.DeploymentSpec{\n\t\t\t\t\t\tReplicas: &replicas,\n\t\t\t\t\t\tSelector: &metav1.LabelSelector{MatchLabels: map[string]string{\"app\": testName}},\n\t\t\t\t\t\tTemplate: corev1.PodTemplateSpec{\n\t\t\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Labels: map[string]string{\"app\": testName}},\n\t\t\t\t\t\t\tSpec:       corev1.PodSpec{Containers: []corev1.Container{{Name: \"app\", Image: \"busybox\"}}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, dep)).To(Succeed())\n\n\t\t\t\tBy(\"Creating LlamaDeployment with non-compliant DNS-1035 name\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\tBy(\"Reconciling the LlamaDeployment\")\n\t\t\t\tresult, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Requeue).To(BeFalse())\n\n\t\t\t\tBy(\"Verifying status is Failed with DNS-1035 message\")\n\t\t\t\tupdatedDeploy := &llamadeployv1.LlamaDeployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, updatedDeploy)).To(Succeed())\n\t\t\t\tExpect(updatedDeploy.Status.Phase).To(Equal(PhaseFailed))\n\t\t\t\tExpect(updatedDeploy.Status.Message).To(ContainSubstring(\"not a valid DNS-1035 label\"))\n\n\t\t\t\tBy(\"Verifying Deployment was torn down\")\n\t\t\t\tEventually(func() bool {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, &appsv1.Deployment{})\n\t\t\t\t\treturn errors.IsNotFound(err)\n\t\t\t\t}).Should(BeTrue())\n\n\t\t\t\tBy(\"Verifying ConfigMap was torn down\")\n\t\t\t\tEventually(func() bool {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-nginx-config\", Namespace: testNamespace}, &corev1.ConfigMap{})\n\t\t\t\t\treturn errors.IsNotFound(err)\n\t\t\t\t}).Should(BeTrue())\n\n\t\t\t\tBy(\"Verifying ServiceAccount was torn down\")\n\t\t\t\tEventually(func() bool {\n\t\t\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-sa\", Namespace: testNamespace}, &corev1.ServiceAccount{})\n\t\t\t\t\treturn errors.IsNotFound(err)\n\t\t\t\t}).Should(BeTrue())\n\n\t\t\t\tBy(\"Verifying reconciliation is idempotent on second run\")\n\t\t\t\tresult, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.Requeue).To(BeFalse())\n\t\t\t})\n\n\t\t\tIt(\"should not tear down resources for a valid DNS-1035 name\", func() {\n\t\t\t\ttestName := \"valid-dns-name\"\n\n\t\t\t\tBy(\"Creating and reconciling a valid LlamaDeployment\")\n\t\t\t\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Verifying resources were created (not torn down)\")\n\t\t\t\tGetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tGetConfigMapEventually(ctx, testName+\"-nginx-config\", testNamespace)\n\t\t\t\tsaObj := &corev1.ServiceAccount{}\n\t\t\t\tEventually(func() error {\n\t\t\t\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: testName + \"-sa\", Namespace: testNamespace}, saObj)\n\t\t\t\t}).Should(Succeed())\n\n\t\t\t\tBy(\"Verifying status is NOT Failed\")\n\t\t\t\tupdatedDeploy := &llamadeployv1.LlamaDeployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, updatedDeploy)).To(Succeed())\n\t\t\t\tExpect(updatedDeploy.Status.Phase).NotTo(Equal(PhaseFailed))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Nginx container configuration\", func() {\n\t\t\tIt(\"should serve static assets when StaticAssetsPath is set\", func() {\n\t\t\t\ttestName := \"test-nginx-static-assets\"\n\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId:        testProjectID,\n\t\t\t\t\t\tRepoUrl:          testRepoURL,\n\t\t\t\t\t\tGitRef:           testGitRef,\n\t\t\t\t\t\tStaticAssetsPath: \"frontend/dist\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\t// Verify ConfigMap was created with nginx configuration\n\t\t\t\tconfigMap := GetConfigMapEventually(ctx, testName+\"-nginx-config\", testNamespace)\n\t\t\t\tExpect(configMap.Data).To(HaveKey(\"nginx.conf\"))\n\t\t\t\tnginxConf := configMap.Data[\"nginx.conf\"]\n\t\t\t\t// Should alias static assets path and include try_files fallback to @python_upstream\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"location /deployments/\" + testName + \"/ui {\"))\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"alias /opt/app/frontend/dist/;\"))\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"try_files $uri $uri/ /index.html @python_upstream;\"))\n\t\t\t\t// Everything else proxies to python app\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"location / { proxy_pass http://127.0.0.1:8080;\"))\n\t\t\t\t// Named upstream for fallback proxies to 8081\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"location @python_upstream { proxy_pass http://127.0.0.1:8081;\"))\n\n\t\t\t\t// Verify deployment uses ConfigMap mount\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tvar nginx corev1.Container\n\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\tif c.Name == \"file-server\" {\n\t\t\t\t\t\tnginx = c\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(nginx.Name).To(Equal(\"file-server\"))\n\t\t\t\tExpect(nginx.Command).To(Equal([]string{\"nginx\", \"-g\", \"daemon off;\"}))\n\t\t\t\tExpect(nginx.VolumeMounts).To(ContainElement(corev1.VolumeMount{\n\t\t\t\t\tName:      \"nginx-config\",\n\t\t\t\t\tMountPath: \"/etc/nginx/nginx.conf\",\n\t\t\t\t\tSubPath:   \"nginx.conf\",\n\t\t\t\t}))\n\n\t\t\t\t// Verify nginx-config volume is mounted\n\t\t\t\tvar nginxConfigVolumeFound bool\n\t\t\t\tfor _, vol := range deployment.Spec.Template.Spec.Volumes {\n\t\t\t\t\tif vol.Name == \"nginx-config\" && vol.ConfigMap != nil &&\n\t\t\t\t\t\tvol.ConfigMap.Name == testName+\"-nginx-config\" {\n\t\t\t\t\t\tnginxConfigVolumeFound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(nginxConfigVolumeFound).To(BeTrue(), \"nginx-config volume should be present\")\n\t\t\t})\n\n\t\t\tIt(\"should proxy UI base when StaticAssetsPath is not set\", func() {\n\t\t\t\ttestName := \"test-nginx-proxy-ui\"\n\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\t\tName:      testName,\n\t\t\t\t\t\tNamespace: testNamespace,\n\t\t\t\t\t},\n\t\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\t\tProjectId: testProjectID,\n\t\t\t\t\t\tRepoUrl:   testRepoURL,\n\t\t\t\t\t\tGitRef:    testGitRef,\n\t\t\t\t\t\t// StaticAssetsPath unset\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer cleanupResource(testName)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\t// Verify ConfigMap was created with nginx configuration\n\t\t\t\tconfigMap := GetConfigMapEventually(ctx, testName+\"-nginx-config\", testNamespace)\n\t\t\t\tExpect(configMap.Data).To(HaveKey(\"nginx.conf\"))\n\t\t\t\tnginxConf := configMap.Data[\"nginx.conf\"]\n\t\t\t\t// Should not include an alias when assets path is unset\n\t\t\t\tExpect(strings.Contains(nginxConf, \"alias /opt/app/\")).To(BeFalse())\n\t\t\t\t// UI base should proxy directly to python app\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"location /deployments/\" + testName + \"/ui { proxy_pass http://127.0.0.1:8080;\"))\n\t\t\t\t// Everything else proxies to python app, and named upstream to 8081 exists\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"location / { proxy_pass http://127.0.0.1:8080;\"))\n\t\t\t\tExpect(nginxConf).To(ContainSubstring(\"location @python_upstream { proxy_pass http://127.0.0.1:8081;\"))\n\n\t\t\t\t// Verify deployment uses ConfigMap mount\n\t\t\t\tdeployment := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tvar nginx corev1.Container\n\t\t\t\tfor _, c := range deployment.Spec.Template.Spec.Containers {\n\t\t\t\t\tif c.Name == \"file-server\" {\n\t\t\t\t\t\tnginx = c\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tExpect(nginx.Name).To(Equal(\"file-server\"))\n\t\t\t\tExpect(nginx.Command).To(Equal([]string{\"nginx\", \"-g\", \"daemon off;\"}))\n\t\t\t\tExpect(nginx.VolumeMounts).To(ContainElement(corev1.VolumeMount{\n\t\t\t\t\tName:      \"nginx-config\",\n\t\t\t\t\tMountPath: \"/etc/nginx/nginx.conf\",\n\t\t\t\t\tSubPath:   \"nginx.conf\",\n\t\t\t\t}))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"OOM regression tests\", func() {\n\t\t\tIt(\"should not regenerate auth token on schema version migration\", func() {\n\t\t\t\ttestName := \"test-no-token-regen\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment and reconciling\")\n\t\t\t\tllamaDeploy := NewLlama(testName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\tBy(\"Recording the auth token after initial reconciliation\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.AuthToken).NotTo(BeEmpty())\n\t\t\t\toriginalToken := llamaDeploy.Status.AuthToken\n\n\t\t\t\tBy(\"Lowering the schema version to simulate an operator upgrade\")\n\t\t\t\tllamaDeploy.Status.SchemaVersion = \"0\"\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling after schema version change\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying the auth token was NOT regenerated\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.AuthToken).To(Equal(originalToken))\n\t\t\t\tExpect(llamaDeploy.Status.SchemaVersion).To(Equal(CurrentSchemaVersion))\n\t\t\t})\n\n\t\t\tIt(\"should advance schema version for terminal (RolloutFailed) deployments\", func() {\n\t\t\t\ttestName := \"test-terminal-schema-advance\"\n\t\t\t\tos.Setenv(EnvRolloutTimeoutSeconds, \"30\")\n\t\t\t\tDeferCleanup(os.Unsetenv, EnvRolloutTimeoutSeconds)\n\n\t\t\t\tBy(\"Creating a LlamaDeployment, timing it out to reach RolloutFailed\")\n\t\t\t\tllamaDeploy := NewLlama(testName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, llamaDeploy)\n\n\t\t\t\t// Set available replicas so timeout picks RolloutFailed\n\t\t\t\tSetDeploymentAvailableReplicas(ctx, testName, testNamespace, 1)\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tpast := metav1.NewTime(time.Now().Add(-60 * time.Second))\n\t\t\t\tllamaDeploy.Status.RolloutStartedAt = &past\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying we're in RolloutFailed state\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\t\t\t\tExpect(llamaDeploy.Status.FailedRolloutGeneration).To(Equal(llamaDeploy.Generation))\n\n\t\t\t\tBy(\"Lowering schema version to simulate an operator upgrade\")\n\t\t\t\tllamaDeploy.Status.SchemaVersion = \"0\"\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should advance schema version even though deployment is terminal\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying schema version was advanced to current\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.SchemaVersion).To(Equal(CurrentSchemaVersion))\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(\"RolloutFailed\"))\n\n\t\t\t\tBy(\"Reconciling again — should be a no-op, not an infinite loop\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\ttokenBefore := llamaDeploy.Status.AuthToken\n\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.AuthToken).To(Equal(tokenBefore))\n\t\t\t\tExpect(llamaDeploy.Status.SchemaVersion).To(Equal(CurrentSchemaVersion))\n\t\t\t})\n\n\t\t\tIt(\"should advance schema version for terminal (BuildFailed) deployments\", func() {\n\t\t\t\ttestName := \"test-buildfailed-schema-advance\"\n\n\t\t\t\tBy(\"Creating a LlamaDeployment and reconciling to get initial status\")\n\t\t\t\tllamaDeploy := NewLlama(testName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, llamaDeploy)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Manually setting status to BuildFailed to simulate a failed build\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Status.Phase = PhaseBuildFailed\n\t\t\t\tllamaDeploy.Status.FailedRolloutGeneration = llamaDeploy.Generation\n\t\t\t\tllamaDeploy.Status.BuildStatus = \"Failed\"\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Lowering schema version to simulate an operator upgrade\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tllamaDeploy.Status.SchemaVersion = \"0\"\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, llamaDeploy)).To(Succeed())\n\n\t\t\t\tBy(\"Reconciling — should advance schema version even though deployment build failed\")\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tBy(\"Verifying schema version was advanced and phase remains BuildFailed\")\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.SchemaVersion).To(Equal(CurrentSchemaVersion))\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(PhaseBuildFailed))\n\n\t\t\t\tBy(\"Reconciling again — should be a no-op, not an infinite loop\")\n\t\t\t\ttokenBefore := llamaDeploy.Status.AuthToken\n\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{\n\t\t\t\t\tNamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace},\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, llamaDeploy)).To(Succeed())\n\t\t\t\tExpect(llamaDeploy.Status.AuthToken).To(Equal(tokenBefore))\n\t\t\t\tExpect(llamaDeploy.Status.SchemaVersion).To(Equal(CurrentSchemaVersion))\n\t\t\t\tExpect(llamaDeploy.Status.Phase).To(Equal(PhaseBuildFailed))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Suspended mode\", func() {\n\t\t\tIt(\"should set replicas to 0 and phase to Suspended when spec.suspended is true\", func() {\n\t\t\t\ttestName := \"test-suspended-set-zero\"\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef, Suspended: true},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t// Suspended deployments skip builds entirely, so a single\n\t\t\t\t// reconcile creates the Deployment (with 0 replicas) directly.\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(*dep.Spec.Replicas).To(Equal(int32(0)))\n\n\t\t\t\tld = &llamadeployv1.LlamaDeployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, ld)).To(Succeed())\n\t\t\t\tExpect(ld.Status.Phase).To(Equal(PhaseSuspended))\n\t\t\t})\n\n\t\t\tIt(\"should restore replicas to 1 when suspended is set to false\", func() {\n\t\t\t\ttestName := \"test-suspended-restore\"\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef, Suspended: true},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t// Suspended deployments skip builds — single reconcile is enough.\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(*dep.Spec.Replicas).To(Equal(int32(0)))\n\n\t\t\t\t// Unsuspend — this triggers a build since the deployment has never built.\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, ld)).To(Succeed())\n\t\t\t\tld.Spec.Suspended = false\n\t\t\t\tExpect(k8sClient.Update(ctx, ld)).To(Succeed())\n\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tdep = GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(*dep.Spec.Replicas).To(Equal(int32(1)))\n\n\t\t\t\tld = &llamadeployv1.LlamaDeployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, ld)).To(Succeed())\n\t\t\t\tExpect(ld.Status.Phase).To(Equal(PhasePending))\n\t\t\t})\n\n\t\t\tIt(\"should not set rolloutStartedAt when suspended\", func() {\n\t\t\t\ttestName := \"test-suspended-no-rollout\"\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef, Suspended: true},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t// Suspended deployments skip builds — single reconcile is enough.\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tld = &llamadeployv1.LlamaDeployment{}\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, ld)).To(Succeed())\n\t\t\t\tExpect(ld.Status.RolloutStartedAt).To(BeNil())\n\t\t\t})\n\n\t\t\tIt(\"should default to 1 replica when suspended field is not set\", func() {\n\t\t\t\ttestName := \"test-suspended-default\"\n\t\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace},\n\t\t\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: testProjectID, RepoUrl: testRepoURL, GitRef: testGitRef},\n\t\t\t\t}\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\tdep := GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t\tExpect(*dep.Spec.Replicas).To(Equal(int32(1)))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Max concurrent rollouts gate\", func() {\n\t\t\tIt(\"should requeue when rollout limit is reached\", func() {\n\t\t\t\t// Create a deployment that is already rolling out\n\t\t\t\trollingName := \"test-rolling-dep\"\n\t\t\t\tld1 := NewLlama(rollingName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, rollingName, testNamespace)\n\n\t\t\t\t// Reconcile it to create resources\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: rollingName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set its phase to RollingOut\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: rollingName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseRollingOut\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Now create a new deployment that needs a full reconcile, with limit=1\n\t\t\t\tgatedName := \"test-gated-dep\"\n\t\t\t\tld2 := NewLlama(gatedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, gatedName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxConcurrentRollouts(1))\n\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: gatedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\">=\", 10*time.Second))\n\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\"<=\", 20*time.Second))\n\n\t\t\t\t// Verify the gated deployment did NOT get PhasePending\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: gatedName, Namespace: testNamespace}, ld2)).To(Succeed())\n\t\t\t\tExpect(ld2.Status.Phase).NotTo(Equal(PhasePending))\n\t\t\t})\n\n\t\t\tIt(\"should requeue when another deployment is in Pending phase\", func() {\n\t\t\t\t// PhasePending also indicates an active rollout and should count toward the limit\n\t\t\t\tpendingName := \"test-pending-dep\"\n\t\t\t\tld1 := NewLlama(pendingName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, pendingName, testNamespace)\n\n\t\t\t\t// Reconcile to create resources\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: pendingName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set its phase to Pending (active rollout, pods not ready yet)\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: pendingName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhasePending\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Try to reconcile a new deployment with limit=1\n\t\t\t\tgatedName := \"test-gated-by-pending\"\n\t\t\t\tld2 := NewLlama(gatedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, gatedName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxConcurrentRollouts(1))\n\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: gatedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\">=\", 10*time.Second))\n\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\"<=\", 20*time.Second))\n\t\t\t})\n\n\t\t\tIt(\"should requeue when another deployment is in Building phase\", func() {\n\t\t\t\t// PhaseBuilding also indicates an active rollout and should count toward the limit\n\t\t\t\tbuildingName := \"test-building-dep\"\n\t\t\t\tld1 := NewLlama(buildingName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, buildingName, testNamespace)\n\n\t\t\t\t// Reconcile to create resources\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: buildingName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set its phase to Building (build job in progress)\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: buildingName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseBuilding\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Try to reconcile a new deployment with limit=1\n\t\t\t\tgatedName := \"test-gated-by-building\"\n\t\t\t\tld2 := NewLlama(gatedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, gatedName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxConcurrentRollouts(1))\n\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: gatedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\">=\", 10*time.Second))\n\t\t\t\tExpect(result.RequeueAfter).To(BeNumerically(\"<=\", 20*time.Second))\n\t\t\t})\n\n\t\t\tIt(\"should allow rollout when under limit\", func() {\n\t\t\t\ttestName := \"test-under-limit\"\n\t\t\t\tld := NewLlama(testName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxConcurrentRollouts(5))\n\n\t\t\t\t_, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, gatedReconciler, ld)\n\n\t\t\t\t// Should have proceeded to create resources (deployment exists)\n\t\t\t\t_ = GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t})\n\n\t\t\tIt(\"should bypass gate when limit is 0\", func() {\n\t\t\t\ttestName := \"test-limit-zero\"\n\t\t\t\tld := NewLlama(testName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t// limit=0 means unlimited\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\n\t\t\t\t// Should have proceeded to create resources (deployment exists)\n\t\t\t\t_ = GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t})\n\n\t\t\tIt(\"should not gate status-only reconciles\", func() {\n\t\t\t\t// Create a deployment already in RollingOut to fill the limit\n\t\t\t\trollingName := \"test-rolling-status\"\n\t\t\t\tld1 := NewLlama(rollingName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, rollingName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: rollingName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set phase to RollingOut\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: rollingName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseRollingOut\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Create another deployment that is already reconciled (not needing full reconcile)\n\t\t\t\tstatusName := \"test-status-only\"\n\t\t\t\tld2 := NewLlama(statusName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, statusName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxConcurrentRollouts(1))\n\n\t\t\t\t// First reconcile to initialize (will be gated since ld1 is rolling)\n\t\t\t\t// But we want to test status-only, so reconcile with unlimited first\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: statusName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld2)\n\n\t\t\t\t// Now reconcile again - this time it's a status-only reconcile (generation hasn't changed)\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: statusName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t// Status-only reconcile should NOT be gated (requeue would be 10-20s if gated)\n\t\t\t\t// It may have a rollout timeout requeue (~1800s) which is fine - just not the gate jitter range\n\t\t\t\tif result.RequeueAfter > 0 {\n\t\t\t\t\tisGateRequeue := result.RequeueAfter >= 10*time.Second && result.RequeueAfter <= 20*time.Second\n\t\t\t\t\tExpect(isGateRequeue).To(BeFalse(), \"status-only reconcile should not be gated\")\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"Max deployments gate\", func() {\n\t\t\tIt(\"should requeue when deployment limit is reached\", func() {\n\t\t\t\t// Create a deployment that is already running\n\t\t\t\trunningName := \"test-maxdep-running\"\n\t\t\t\tld1 := NewLlama(runningName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, runningName, testNamespace)\n\n\t\t\t\t// Reconcile it to create resources\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: runningName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set its phase to Running\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: runningName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseRunning\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Now create a new deployment that needs a full reconcile, with limit=1\n\t\t\t\tgatedName := \"test-maxdep-gated\"\n\t\t\t\tld2 := NewLlama(gatedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, gatedName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxDeployments(1))\n\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: gatedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.RequeueAfter).To(Equal(5 * time.Minute))\n\n\t\t\t\t// Verify the gated deployment did NOT get PhasePending\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: gatedName, Namespace: testNamespace}, ld2)).To(Succeed())\n\t\t\t\tExpect(ld2.Status.Phase).NotTo(Equal(PhasePending))\n\t\t\t})\n\n\t\t\tIt(\"should allow deployment when under limit\", func() {\n\t\t\t\ttestName := \"test-maxdep-under-limit\"\n\t\t\t\tld := NewLlama(testName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxDeployments(5))\n\n\t\t\t\t_, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tCompleteBuild(ctx, gatedReconciler, ld)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Should have proceeded to create resources (deployment exists)\n\t\t\t\t_ = GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t})\n\n\t\t\tIt(\"should bypass gate when limit is 0\", func() {\n\t\t\t\ttestName := \"test-maxdep-limit-zero\"\n\t\t\t\tld := NewLlama(testName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, testName, testNamespace)\n\n\t\t\t\t// limit=0 means unlimited (controllerReconciler has MaxDeployments=0)\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: testName, Namespace: testNamespace}})\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Should have proceeded to create resources (deployment exists)\n\t\t\t\t_ = GetDeploymentEventually(ctx, testName, testNamespace)\n\t\t\t})\n\n\t\t\tIt(\"should not gate status-only reconciles\", func() {\n\t\t\t\t// Create a deployment already in Running to fill the limit\n\t\t\t\trunningName := \"test-maxdep-running-status\"\n\t\t\t\tld1 := NewLlama(runningName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, runningName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: runningName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set phase to Running\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: runningName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseRunning\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Create another deployment that is already reconciled (not needing full reconcile)\n\t\t\t\tstatusName := \"test-maxdep-status-only\"\n\t\t\t\tld2 := NewLlama(statusName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, statusName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxDeployments(1))\n\n\t\t\t\t// First reconcile to initialize with unlimited reconciler\n\t\t\t\t_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: statusName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tCompleteBuild(ctx, controllerReconciler, ld2)\n\n\t\t\t\t// Now reconcile again - this time it's a status-only reconcile (generation hasn't changed)\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: statusName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t// Status-only reconcile should NOT be gated (requeue would be 5m if gated)\n\t\t\t\tif result.RequeueAfter > 0 {\n\t\t\t\t\tExpect(result.RequeueAfter).NotTo(Equal(5*time.Minute), \"status-only reconcile should not be gated\")\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tIt(\"should not count Suspended deployments toward limit\", func() {\n\t\t\t\t// Create a deployment and set it to Suspended\n\t\t\t\tsuspendedName := \"test-maxdep-suspended\"\n\t\t\t\tld1 := NewLlama(suspendedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, suspendedName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: suspendedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set phase to Suspended\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: suspendedName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseSuspended\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Try to reconcile a new deployment with limit=1\n\t\t\t\tnewName := \"test-maxdep-after-suspended\"\n\t\t\t\tld2 := NewLlama(newName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, newName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxDeployments(1))\n\n\t\t\t\t_, err = gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: newName, Namespace: testNamespace}})\n\t\t\t\tCompleteBuild(ctx, gatedReconciler, ld2)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Should have proceeded to create resources (Suspended doesn't count)\n\t\t\t\t_ = GetDeploymentEventually(ctx, newName, testNamespace)\n\t\t\t})\n\n\t\t\tIt(\"should not count Failed deployments toward limit\", func() {\n\t\t\t\t// Create a deployment and set it to Failed\n\t\t\t\tfailedName := \"test-maxdep-failed\"\n\t\t\t\tld1 := NewLlama(failedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, failedName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: failedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set phase to Failed\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: failedName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseFailed\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Try to reconcile a new deployment with limit=1\n\t\t\t\tnewName := \"test-maxdep-after-failed\"\n\t\t\t\tld2 := NewLlama(newName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, newName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxDeployments(1))\n\n\t\t\t\t_, err = gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: newName, Namespace: testNamespace}})\n\t\t\t\tCompleteBuild(ctx, gatedReconciler, ld2)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Should have proceeded to create resources (Failed doesn't count)\n\t\t\t\t_ = GetDeploymentEventually(ctx, newName, testNamespace)\n\t\t\t})\n\n\t\t\tIt(\"should count RolloutFailed deployments toward limit\", func() {\n\t\t\t\t// Create a deployment and set it to RolloutFailed\n\t\t\t\trolloutFailedName := \"test-maxdep-rolloutfailed\"\n\t\t\t\tld1 := NewLlama(rolloutFailedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, rolloutFailedName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: rolloutFailedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set phase to RolloutFailed\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: rolloutFailedName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseRolloutFailed\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Try to reconcile a new deployment with limit=1\n\t\t\t\tgatedName := \"test-maxdep-gated-by-rf\"\n\t\t\t\tld2 := NewLlama(gatedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, gatedName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxDeployments(1))\n\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: gatedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.RequeueAfter).To(Equal(5 * time.Minute))\n\t\t\t})\n\n\t\t\tIt(\"safety-net requeue is approximately 5 minutes\", func() {\n\t\t\t\t// Create a deployment that is already running to fill the limit\n\t\t\t\trunningName := \"test-maxdep-safety-running\"\n\t\t\t\tld1 := NewLlama(runningName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld1)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, runningName, testNamespace)\n\n\t\t\t\t_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: runningName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\t// Set phase to Running\n\t\t\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: runningName, Namespace: testNamespace}, ld1)).To(Succeed())\n\t\t\t\tld1.Status.Phase = PhaseRunning\n\t\t\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\t\t\t// Try to reconcile a new deployment with limit=1\n\t\t\t\tgatedName := \"test-maxdep-safety-gated\"\n\t\t\t\tld2 := NewLlama(gatedName, testNamespace, testProjectID, testRepoURL, WithGitRef(testGitRef))\n\t\t\t\tExpect(k8sClient.Create(ctx, ld2)).To(Succeed())\n\t\t\t\tdefer CleanupLlama(ctx, gatedName, testNamespace)\n\n\t\t\t\tgatedReconciler := NewTestReconciler(WithMaxDeployments(1))\n\n\t\t\t\tresult, err := gatedReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: gatedName, Namespace: testNamespace}})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(result.RequeueAfter).To(Equal(5 * time.Minute))\n\t\t\t})\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "operator/internal/controller/mapping_unit_test.go",
    "content": "//go:build !integration\n\npackage controller\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nconst (\n\tcontainerNameApp        = \"app\"\n\tcontainerNameFileServer = \"file-server\"\n)\n\n// Test that nginx config string generation maps inputs correctly without envtest\nfunc TestGenerateNginxConfig_StaticAndProxy(t *testing.T) {\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{StaticAssetsPath: \"frontend/dist\"},\n\t}\n\n\tconf := (&LlamaDeploymentReconciler{}).generateNginxConfig(ld)\n\tif !strings.Contains(conf, \"location /deployments/demo/ui {\") {\n\t\tt.Fatalf(\"expected UI base location, got: %s\", conf)\n\t}\n\tif !strings.Contains(conf, \"alias /opt/app/frontend/dist/\") {\n\t\tt.Fatalf(\"expected alias for static assets, got: %s\", conf)\n\t}\n\tif !strings.Contains(conf, \"location @python_upstream { proxy_pass http://127.0.0.1:8081;\") {\n\t\tt.Fatalf(\"expected named upstream for fallback, got: %s\", conf)\n\t}\n\tif !strings.Contains(conf, \"access_log    off;\") {\n\t\tt.Fatalf(\"expected access_log off, got: %s\", conf)\n\t}\n}\n\nfunc TestCreateDeploymentForLlama_ShapesPodSpec(t *testing.T) {\n\treconciler := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", GitRef: \"main\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{AuthToken: \"tok\"},\n\t}\n\n\tdep := reconciler.createDeploymentForLlama(ld, \"\")\n\tif dep.Spec.Template.Spec.ServiceAccountName != \"demo-sa\" {\n\t\tt.Fatalf(\"expected SA name demo-sa, got %s\", dep.Spec.Template.Spec.ServiceAccountName)\n\t}\n\tif len(dep.Spec.Template.Spec.Containers) != 2 {\n\t\tt.Fatalf(\"expected 2 containers, got %d\", len(dep.Spec.Template.Spec.Containers))\n\t}\n\tvar appC, nginxC corev1.Container\n\tfor _, c := range dep.Spec.Template.Spec.Containers {\n\t\tif c.Name == containerNameApp {\n\t\t\tappC = c\n\t\t}\n\t\tif c.Name == containerNameFileServer {\n\t\t\tnginxC = c\n\t\t}\n\t}\n\tif appC.Name != containerNameApp || nginxC.Name != containerNameFileServer {\n\t\tt.Fatalf(\"expected app and file-server containers\")\n\t}\n\tif appC.Ports[0].ContainerPort != 8080 || nginxC.Ports[0].ContainerPort != 8081 {\n\t\tt.Fatalf(\"expected app:8080 and file-server:8081\")\n\t}\n}\n\nfunc TestGetRolloutTimeout_Default(t *testing.T) {\n\tt.Setenv(EnvRolloutTimeoutSeconds, \"\")\n\tgot := getRolloutTimeout()\n\texpected := time.Duration(DefaultRolloutTimeoutSeconds) * time.Second\n\tif got != expected {\n\t\tt.Fatalf(\"expected %v for default, got %v\", expected, got)\n\t}\n}\n\nfunc TestGetRolloutTimeout_Custom(t *testing.T) {\n\tt.Setenv(EnvRolloutTimeoutSeconds, \"120\")\n\tgot := getRolloutTimeout()\n\tif got != 120*time.Second {\n\t\tt.Fatalf(\"expected 120s, got %v\", got)\n\t}\n}\n\nfunc TestGetRolloutTimeoutSeconds_Default(t *testing.T) {\n\tt.Setenv(EnvRolloutTimeoutSeconds, \"\")\n\tgot := getRolloutTimeoutSeconds()\n\tif got == nil || *got != DefaultRolloutTimeoutSeconds {\n\t\tt.Fatalf(\"expected %d, got %v\", DefaultRolloutTimeoutSeconds, got)\n\t}\n}\n\nfunc TestGetRolloutTimeoutSeconds_Custom(t *testing.T) {\n\tt.Setenv(EnvRolloutTimeoutSeconds, \"600\")\n\tgot := getRolloutTimeoutSeconds()\n\tif got == nil || *got != 600 {\n\t\tt.Fatalf(\"expected 600, got %v\", got)\n\t}\n}\n\nfunc TestCreateDeploymentForLlama_ProgressDeadlineSeconds_Default(t *testing.T) {\n\treconciler := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", GitRef: \"main\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{AuthToken: \"tok\"},\n\t}\n\tdep := reconciler.createDeploymentForLlama(ld, \"\")\n\tif dep.Spec.ProgressDeadlineSeconds == nil {\n\t\tt.Fatalf(\"expected progressDeadlineSeconds to be set\")\n\t}\n\tif *dep.Spec.ProgressDeadlineSeconds != DefaultRolloutTimeoutSeconds {\n\t\tt.Fatalf(\"expected %d, got %d\", DefaultRolloutTimeoutSeconds, *dep.Spec.ProgressDeadlineSeconds)\n\t}\n}\n\nfunc TestCreateDeploymentForLlama_ProgressDeadlineSeconds_Custom(t *testing.T) {\n\tt.Setenv(EnvRolloutTimeoutSeconds, \"120\")\n\treconciler := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"p\",\n\t\t\tRepoUrl:   \"r\",\n\t\t\tGitRef:    \"main\",\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{AuthToken: \"tok\"},\n\t}\n\tdep := reconciler.createDeploymentForLlama(ld, \"\")\n\tif dep.Spec.ProgressDeadlineSeconds == nil || *dep.Spec.ProgressDeadlineSeconds != 120 {\n\t\tt.Fatalf(\"expected 120, got %v\", dep.Spec.ProgressDeadlineSeconds)\n\t}\n}\n\nfunc TestCreateDeploymentForLlama_BuildIdEnvVar(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", GitRef: \"main\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{AuthToken: \"tok\"},\n\t}\n\n\tdep := r.createDeploymentForLlama(ld, \"build-abc123\")\n\n\t// Check init container env vars for LLAMA_DEPLOY_BUILD_ID\n\tfound := false\n\tfor _, env := range dep.Spec.Template.Spec.InitContainers[0].Env {\n\t\tif env.Name == envBuildID {\n\t\t\tfound = true\n\t\t\tif env.Value != \"build-abc123\" {\n\t\t\t\tt.Errorf(\"expected LLAMA_DEPLOY_BUILD_ID=build-abc123, got %q\", env.Value)\n\t\t\t}\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"expected LLAMA_DEPLOY_BUILD_ID env var to be present when buildId is non-empty\")\n\t}\n\n\t// Also check app container\n\tfound = false\n\tfor _, c := range dep.Spec.Template.Spec.Containers {\n\t\tif c.Name == containerNameApp {\n\t\t\tfor _, env := range c.Env {\n\t\t\t\tif env.Name == envBuildID {\n\t\t\t\t\tfound = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"expected LLAMA_DEPLOY_BUILD_ID env var on app container when buildId is non-empty\")\n\t}\n}\n\nfunc TestCreateDeploymentForLlama_NoBuildIdEnvVar(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", GitRef: \"main\"},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{AuthToken: \"tok\"},\n\t}\n\n\tdep := r.createDeploymentForLlama(ld, \"\")\n\n\t// Check init container: LLAMA_DEPLOY_BUILD_ID should NOT be present\n\tfor _, env := range dep.Spec.Template.Spec.InitContainers[0].Env {\n\t\tif env.Name == envBuildID {\n\t\t\tt.Error(\"expected LLAMA_DEPLOY_BUILD_ID env var to NOT be present when buildId is empty\")\n\t\t}\n\t}\n\n\t// Check app container\n\tfor _, c := range dep.Spec.Template.Spec.Containers {\n\t\tif c.Name == containerNameApp {\n\t\t\tfor _, env := range c.Env {\n\t\t\t\tif env.Name == envBuildID {\n\t\t\t\t\tt.Error(\"expected LLAMA_DEPLOY_BUILD_ID env var to NOT be present on app container when buildId is empty\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestCreateDeploymentForLlama_ReplicasZeroForFailedPhase(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\", Generation: 5},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", GitRef: \"main\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tAuthToken:               \"tok\",\n\t\t\tPhase:                   PhaseFailed,\n\t\t\tFailedRolloutGeneration: 5, // matches Generation\n\t\t},\n\t}\n\n\tdep := r.createDeploymentForLlama(ld, \"\")\n\tif dep.Spec.Replicas == nil || *dep.Spec.Replicas != 0 {\n\t\tt.Errorf(\"expected replicas=0 for failed phase with matching generation, got %v\", dep.Spec.Replicas)\n\t}\n}\n\nfunc TestCreateDeploymentForLlama_ReplicasZeroForSuspended(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\", GitRef: \"main\", Suspended: true},\n\t\tStatus:     llamadeployv1.LlamaDeploymentStatus{AuthToken: \"tok\"},\n\t}\n\n\tdep := r.createDeploymentForLlama(ld, \"\")\n\tif dep.Spec.Replicas == nil || *dep.Spec.Replicas != 0 {\n\t\tt.Errorf(\"expected replicas=0 for suspended deployment, got %v\", dep.Spec.Replicas)\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// looksLikeFilePath tests\n// ---------------------------------------------------------------------------\n\nfunc TestLooksLikeFilePath(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\twant  bool\n\t}{\n\t\t{\"deployment.yml\", true},\n\t\t{\"path/to/file.yaml\", true},\n\t\t{\"main.py\", true},\n\t\t{\"src/app.js\", true},\n\t\t{\"mydir\", false},\n\t\t{\"path/to/dir\", false},\n\t\t{\".\", false},\n\t\t{\"/\", false},\n\t\t{\".hidden\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tif got := looksLikeFilePath(tt.input); got != tt.want {\n\t\t\t\tt.Errorf(\"looksLikeFilePath(%q) = %v, want %v\", tt.input, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// getContainerImage tests\n// ---------------------------------------------------------------------------\n\nfunc TestGetContainerImage(t *testing.T) {\n\tt.Run(\"spec override wins\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageName, \"env-image\")\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{Image: \"spec-image\"},\n\t\t}\n\t\tif got := getContainerImage(ld); got != \"spec-image\" {\n\t\t\tt.Errorf(\"expected spec-image, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"env fallback\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageName, \"env-image\")\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t}\n\t\tif got := getContainerImage(ld); got != \"env-image\" {\n\t\t\tt.Errorf(\"expected env-image, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageName, \"\")\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t}\n\t\tif got := getContainerImage(ld); got != DefaultImage {\n\t\t\tt.Errorf(\"expected %q, got %q\", DefaultImage, got)\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// getContainerImageTag tests\n// ---------------------------------------------------------------------------\n\nfunc TestGetContainerImageTag(t *testing.T) {\n\tt.Run(\"spec override wins\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageTag, \"env-tag\")\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ImageTag: \"spec-tag\"},\n\t\t}\n\t\tif got := getContainerImageTag(ld); got != \"spec-tag\" {\n\t\t\tt.Errorf(\"expected spec-tag, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"env fallback\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageTag, \"env-tag\")\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t}\n\t\tif got := getContainerImageTag(ld); got != \"env-tag\" {\n\t\t\tt.Errorf(\"expected env-tag, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageTag, \"\")\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t}\n\t\tif got := getContainerImageTag(ld); got != DefaultImageTag {\n\t\t\tt.Errorf(\"expected %q, got %q\", DefaultImageTag, got)\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// shouldForceOwnership tests\n// ---------------------------------------------------------------------------\n\nfunc TestShouldForceOwnership(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\n\tt.Run(\"generation mismatch forces ownership\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Generation: 2},\n\t\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\t\tLastReconciledGeneration: 1,\n\t\t\t\tSchemaVersion:            CurrentSchemaVersion,\n\t\t\t},\n\t\t}\n\t\tif !r.shouldForceOwnership(ld) {\n\t\t\tt.Error(\"expected true when generation mismatches\")\n\t\t}\n\t})\n\n\tt.Run(\"schema version mismatch forces ownership\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Generation: 1},\n\t\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\t\tLastReconciledGeneration: 1,\n\t\t\t\tSchemaVersion:            \"0\",\n\t\t\t},\n\t\t}\n\t\tif !r.shouldForceOwnership(ld) {\n\t\t\tt.Error(\"expected true when schema version is old\")\n\t\t}\n\t})\n\n\tt.Run(\"annotation override forces ownership\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\tName:        \"demo\",\n\t\t\t\tGeneration:  1,\n\t\t\t\tAnnotations: map[string]string{\"deploy.llamaindex.ai/force-ownership\": \"true\"},\n\t\t\t},\n\t\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\t\tLastReconciledGeneration: 1,\n\t\t\t\tSchemaVersion:            CurrentSchemaVersion,\n\t\t\t},\n\t\t}\n\t\tif !r.shouldForceOwnership(ld) {\n\t\t\tt.Error(\"expected true with force-ownership annotation\")\n\t\t}\n\t})\n\n\tt.Run(\"all matching does not force ownership\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Generation: 1},\n\t\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\t\tLastReconciledGeneration: 1,\n\t\t\t\tSchemaVersion:            CurrentSchemaVersion,\n\t\t\t},\n\t\t}\n\t\tif r.shouldForceOwnership(ld) {\n\t\t\tt.Error(\"expected false when everything matches\")\n\t\t}\n\t})\n\n\tt.Run(\"empty schema version forces ownership\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Generation: 1},\n\t\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\t\tLastReconciledGeneration: 1,\n\t\t\t\tSchemaVersion:            \"\",\n\t\t\t},\n\t\t}\n\t\tif !r.shouldForceOwnership(ld) {\n\t\t\tt.Error(\"expected true when schema version is empty\")\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// getContainerImagePullPolicy tests\n// ---------------------------------------------------------------------------\n\nfunc TestGetContainerImagePullPolicy(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tenvVal string\n\t\twant   corev1.PullPolicy\n\t}{\n\t\t{\"default\", \"\", corev1.PullIfNotPresent},\n\t\t{\"always\", \"Always\", corev1.PullAlways},\n\t\t{\"never\", \"Never\", corev1.PullNever},\n\t\t{\"if not present\", \"IfNotPresent\", corev1.PullIfNotPresent},\n\t\t{\"invalid falls back to default\", \"BadValue\", corev1.PullIfNotPresent},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Setenv(EnvImagePullPolicy, tt.envVal)\n\t\t\tif got := getContainerImagePullPolicy(); got != tt.want {\n\t\t\t\tt.Errorf(\"getContainerImagePullPolicy() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// getDefaultImage / getDefaultImageTag tests\n// ---------------------------------------------------------------------------\n\nfunc TestGetDefaultImage(t *testing.T) {\n\tt.Run(\"env override\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageName, \"custom-image\")\n\t\tif got := getDefaultImage(); got != \"custom-image\" {\n\t\t\tt.Errorf(\"expected custom-image, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageName, \"\")\n\t\tif got := getDefaultImage(); got != DefaultImage {\n\t\t\tt.Errorf(\"expected %q, got %q\", DefaultImage, got)\n\t\t}\n\t})\n}\n\nfunc TestGetDefaultImageTag(t *testing.T) {\n\tt.Run(\"env override\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageTag, \"custom-tag\")\n\t\tif got := getDefaultImageTag(); got != \"custom-tag\" {\n\t\t\tt.Errorf(\"expected custom-tag, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(EnvImageTag, \"\")\n\t\tif got := getDefaultImageTag(); got != DefaultImageTag {\n\t\t\tt.Errorf(\"expected %q, got %q\", DefaultImageTag, got)\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// getNginxImage / getNginxImageTag tests\n// ---------------------------------------------------------------------------\n\nfunc TestGetNginxImage(t *testing.T) {\n\tt.Run(\"env override\", func(t *testing.T) {\n\t\tt.Setenv(EnvNginxImageName, \"custom-nginx\")\n\t\tif got := getNginxImage(); got != \"custom-nginx\" {\n\t\t\tt.Errorf(\"expected custom-nginx, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(EnvNginxImageName, \"\")\n\t\tif got := getNginxImage(); got != DefaultNginxImage {\n\t\t\tt.Errorf(\"expected %q, got %q\", DefaultNginxImage, got)\n\t\t}\n\t})\n}\n\nfunc TestGetNginxImageTag(t *testing.T) {\n\tt.Run(\"env override\", func(t *testing.T) {\n\t\tt.Setenv(EnvNginxImageTag, \"custom-tag\")\n\t\tif got := getNginxImageTag(); got != \"custom-tag\" {\n\t\t\tt.Errorf(\"expected custom-tag, got %q\", got)\n\t\t}\n\t})\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(EnvNginxImageTag, \"\")\n\t\tif got := getNginxImageTag(); got != DefaultNginxImageTag {\n\t\t\tt.Errorf(\"expected %q, got %q\", DefaultNginxImageTag, got)\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// Working directory logic tests\n// ---------------------------------------------------------------------------\n\nfunc TestCreateDeploymentForLlama_WorkingDir(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfilePath string\n\t\twantDir  string\n\t}{\n\t\t{\"default path\", \"\", \"/opt/app\"},\n\t\t{\"dot path\", \".\", \"/opt/app\"},\n\t\t{\"file in subdir\", \"subdir/deploy.yml\", \"/opt/app/subdir\"},\n\t\t{\"directory path\", \"subdir\", \"/opt/app/subdir\"},\n\t\t{\"nested file\", \"a/b/c/deploy.yml\", \"/opt/app/a/b/c\"},\n\t\t{\"root file\", \"deploy.yml\", \"/opt/app\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := &LlamaDeploymentReconciler{}\n\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\", Namespace: \"default\"},\n\t\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\t\tProjectId:          \"p\",\n\t\t\t\t\tRepoUrl:            \"r\",\n\t\t\t\t\tDeploymentFilePath: tt.filePath,\n\t\t\t\t},\n\t\t\t\tStatus: llamadeployv1.LlamaDeploymentStatus{AuthToken: \"tok\"},\n\t\t\t}\n\t\t\tdep := r.createDeploymentForLlama(ld, \"\")\n\t\t\tvar appC corev1.Container\n\t\t\tfor _, c := range dep.Spec.Template.Spec.Containers {\n\t\t\t\tif c.Name == containerNameApp {\n\t\t\t\t\tappC = c\n\t\t\t\t}\n\t\t\t}\n\t\t\tif appC.WorkingDir != tt.wantDir {\n\t\t\t\tt.Errorf(\"expected WorkingDir %q, got %q\", tt.wantDir, appC.WorkingDir)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// commonEnvVars tests\n// ---------------------------------------------------------------------------\n\nfunc TestCommonEnvVars_AppserverVersion(t *testing.T) {\n\tconst envAppserverVersion = \"LLAMA_DEPLOY_APPSERVER_VERSION\"\n\tr := &LlamaDeploymentReconciler{}\n\n\tt.Run(\"appserver tag sets version env\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ImageTag: \"appserver-0.4.15\"},\n\t\t}\n\t\tenvs := r.commonEnvVars(ld)\n\t\tfound := false\n\t\tfor _, e := range envs {\n\t\t\tif e.Name == envAppserverVersion {\n\t\t\t\tfound = true\n\t\t\t\tif e.Value != \"0.4.15\" {\n\t\t\t\t\tt.Errorf(\"expected 0.4.15, got %q\", e.Value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tt.Error(\"expected LLAMA_DEPLOY_APPSERVER_VERSION env var\")\n\t\t}\n\t})\n\n\tt.Run(\"plain version tag sets version env\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ImageTag: \"0.8.1\"},\n\t\t}\n\t\tenvs := r.commonEnvVars(ld)\n\t\tfound := false\n\t\tfor _, e := range envs {\n\t\t\tif e.Name == envAppserverVersion {\n\t\t\t\tfound = true\n\t\t\t\tif e.Value != \"0.8.1\" {\n\t\t\t\t\tt.Errorf(\"expected 0.8.1, got %q\", e.Value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tt.Error(\"expected LLAMA_DEPLOY_APPSERVER_VERSION env var for plain version tag\")\n\t\t}\n\t})\n\n\tt.Run(\"empty tag does not set version env\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ImageTag: \"\"},\n\t\t}\n\t\tenvs := r.commonEnvVars(ld)\n\t\tfor _, e := range envs {\n\t\t\tif e.Name == envAppserverVersion {\n\t\t\t\tt.Error(\"did not expect LLAMA_DEPLOY_APPSERVER_VERSION for empty tag\")\n\t\t\t}\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// commonEnvFrom tests\n// ---------------------------------------------------------------------------\n\nfunc TestCommonEnvFrom(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\n\tt.Run(\"with secret\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{SecretName: \"my-secret\"},\n\t\t}\n\t\tenvFrom := r.commonEnvFrom(ld)\n\t\tif len(envFrom) != 1 {\n\t\t\tt.Fatalf(\"expected 1 envFrom, got %d\", len(envFrom))\n\t\t}\n\t\tif envFrom[0].SecretRef.Name != \"my-secret\" {\n\t\t\tt.Errorf(\"expected secret name my-secret, got %q\", envFrom[0].SecretRef.Name)\n\t\t}\n\t})\n\n\tt.Run(\"without secret\", func(t *testing.T) {\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"demo\"},\n\t\t}\n\t\tenvFrom := r.commonEnvFrom(ld)\n\t\tif envFrom != nil {\n\t\t\tt.Errorf(\"expected nil envFrom, got %v\", envFrom)\n\t\t}\n\t})\n}\n\nfunc TestIsValidDNS1035Label(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput string\n\t\twant  bool\n\t}{\n\t\t{\"valid simple\", \"my-service\", true},\n\t\t{\"valid single char\", \"a\", true},\n\t\t{\"valid alphanumeric end\", \"abc-123\", true},\n\t\t{\"valid long\", strings.Repeat(\"a\", 63), true},\n\t\t{\"starts with digit\", \"10101010\", false},\n\t\t{\"starts with digit then letters\", \"123-service\", false},\n\t\t{\"starts with dash\", \"-service\", false},\n\t\t{\"ends with dash\", \"service-\", false},\n\t\t{\"uppercase\", \"MyService\", false},\n\t\t{\"empty\", \"\", false},\n\t\t{\"too long\", strings.Repeat(\"a\", 64), false},\n\t\t{\"contains underscore\", \"my_service\", false},\n\t\t{\"contains dot\", \"my.service\", false},\n\t\t{\"single digit\", \"1\", false},\n\t\t{\"only dashes\", \"---\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := isValidDNS1035Label(tt.input)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"isValidDNS1035Label(%q) = %v, want %v\", tt.input, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "operator/internal/controller/metrics.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nvar descPhaseTotal = prometheus.NewDesc(\n\t\"llamadeployments_by_phase\",\n\t\"Number of LlamaDeployments per status phase\",\n\t[]string{\"phase\"}, nil,\n)\n\n// PhaseCollector implements prometheus.Collector and reports deployment counts\n// by phase, reading from the informer cache at scrape time.\ntype PhaseCollector struct {\n\treader client.Reader\n}\n\n// NewPhaseCollector creates a new collector that uses the given reader\n// (typically the controller-runtime manager's cached client) to list LlamaDeployments.\nfunc NewPhaseCollector(reader client.Reader) *PhaseCollector {\n\treturn &PhaseCollector{reader: reader}\n}\n\nfunc (c *PhaseCollector) Describe(ch chan<- *prometheus.Desc) {\n\tch <- descPhaseTotal\n}\n\nfunc (c *PhaseCollector) Collect(ch chan<- prometheus.Metric) {\n\tvar list llamadeployv1.LlamaDeploymentList\n\tif err := c.reader.List(context.Background(), &list); err != nil {\n\t\treturn\n\t}\n\n\tcounts := make(map[string]float64)\n\tfor i := range list.Items {\n\t\tphase := list.Items[i].Status.Phase\n\t\tif phase == \"\" {\n\t\t\tphase = \"Unknown\"\n\t\t}\n\t\tcounts[phase]++\n\t}\n\n\tfor phase, count := range counts {\n\t\tch <- prometheus.MustNewConstMetric(descPhaseTotal, prometheus.GaugeValue, count, phase)\n\t}\n}\n\nvar descActiveTotal = prometheus.NewDesc(\n\t\"llamadeployments_active_total\",\n\t\"Number of active (non-suspended, non-failed) LlamaDeployments\",\n\t[]string{\"namespace\"}, nil,\n)\n\nvar descMaxCapacity = prometheus.NewDesc(\n\t\"llamadeployments_max_capacity\",\n\t\"Configured maximum deployments limit (0 = unlimited)\",\n\t[]string{\"namespace\"}, nil,\n)\n\n// CapacityCollector implements prometheus.Collector and reports active deployment\n// counts and the configured maximum capacity per namespace.\ntype CapacityCollector struct {\n\treader         client.Reader\n\tmaxDeployments int\n}\n\n// NewCapacityCollector creates a new collector that reports active deployment\n// counts and the configured max deployments limit.\nfunc NewCapacityCollector(reader client.Reader, maxDeployments int) *CapacityCollector {\n\treturn &CapacityCollector{reader: reader, maxDeployments: maxDeployments}\n}\n\nfunc (c *CapacityCollector) Describe(ch chan<- *prometheus.Desc) {\n\tch <- descActiveTotal\n\tch <- descMaxCapacity\n}\n\nfunc (c *CapacityCollector) Collect(ch chan<- prometheus.Metric) {\n\tvar list llamadeployv1.LlamaDeploymentList\n\tif err := c.reader.List(context.Background(), &list); err != nil {\n\t\treturn\n\t}\n\n\tcounts := make(map[string]float64)\n\tnamespaces := make(map[string]bool)\n\tfor i := range list.Items {\n\t\tns := list.Items[i].Namespace\n\t\tnamespaces[ns] = true\n\t\tphase := list.Items[i].Status.Phase\n\t\tif isActivePhase(phase) {\n\t\t\tcounts[ns]++\n\t\t}\n\t}\n\n\tfor ns := range namespaces {\n\t\tch <- prometheus.MustNewConstMetric(descActiveTotal, prometheus.GaugeValue, counts[ns], ns)\n\t\tch <- prometheus.MustNewConstMetric(descMaxCapacity, prometheus.GaugeValue, float64(c.maxDeployments), ns)\n\t}\n}\n"
  },
  {
    "path": "operator/internal/controller/metrics_test.go",
    "content": "//go:build integration\n\npackage controller\n\nimport (\n\t\"context\"\n\n\t. \"github.com/onsi/ginkgo/v2\"\n\t. \"github.com/onsi/gomega\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tdto \"github.com/prometheus/client_model/go\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nvar _ = Describe(\"PhaseCollector\", func() {\n\tconst testNamespace = \"default\"\n\n\tIt(\"should report counts by phase\", func() {\n\t\tctx := context.Background()\n\n\t\t// Create test LlamaDeployments with different phases.\n\t\tld1 := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"test-metrics-phase-1\", Namespace: testNamespace},\n\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\tProjectId: \"metrics-test\",\n\t\t\t\tRepoUrl:   \"https://github.com/test/repo.git\",\n\t\t\t\tGitRef:    \"main\",\n\t\t\t},\n\t\t}\n\t\tld2 := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"test-metrics-phase-2\", Namespace: testNamespace},\n\t\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\t\tProjectId: \"metrics-test\",\n\t\t\t\tRepoUrl:   \"https://github.com/test/repo.git\",\n\t\t\t\tGitRef:    \"main\",\n\t\t\t\tSuspended: true,\n\t\t\t},\n\t\t}\n\n\t\tfor _, ld := range []*llamadeployv1.LlamaDeployment{ld1, ld2} {\n\t\t\tExpect(k8sClient.Create(ctx, ld)).To(Succeed())\n\t\t}\n\t\tdefer func() {\n\t\t\tfor _, ld := range []*llamadeployv1.LlamaDeployment{ld1, ld2} {\n\t\t\t\t_ = k8sClient.Delete(ctx, ld)\n\t\t\t}\n\t\t}()\n\n\t\t// Set phases via status subresource\n\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: ld1.Name, Namespace: ld1.Namespace}, ld1)).To(Succeed())\n\t\tld1.Status.Phase = PhaseRunning\n\t\tExpect(k8sClient.Status().Update(ctx, ld1)).To(Succeed())\n\n\t\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: ld2.Name, Namespace: ld2.Namespace}, ld2)).To(Succeed())\n\t\tld2.Status.Phase = PhaseSuspended\n\t\tExpect(k8sClient.Status().Update(ctx, ld2)).To(Succeed())\n\n\t\tcollector := NewPhaseCollector(k8sClient)\n\n\t\t// Collect metrics\n\t\tch := make(chan prometheus.Metric, 100)\n\t\tcollector.Collect(ch)\n\t\tclose(ch)\n\n\t\tcounts := make(map[string]float64)\n\t\tfor m := range ch {\n\t\t\td := &dto.Metric{}\n\t\t\tExpect(m.Write(d)).To(Succeed())\n\t\t\tphase := d.Label[0].GetValue()\n\t\t\tcounts[phase] += d.Gauge.GetValue()\n\t\t}\n\n\t\tExpect(counts[PhaseRunning]).To(BeNumerically(\">=\", 1))\n\t\tExpect(counts[PhaseSuspended]).To(BeNumerically(\">=\", 1))\n\t})\n})\n"
  },
  {
    "path": "operator/internal/controller/reconcile.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/errors\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/tools/record\"\n\tctrl \"sigs.k8s.io/controller-runtime\"\n\t\"sigs.k8s.io/controller-runtime/pkg/builder\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil\"\n\t\"sigs.k8s.io/controller-runtime/pkg/event\"\n\t\"sigs.k8s.io/controller-runtime/pkg/handler\"\n\t\"sigs.k8s.io/controller-runtime/pkg/log\"\n\t\"sigs.k8s.io/controller-runtime/pkg/predicate\"\n\t\"sigs.k8s.io/controller-runtime/pkg/reconcile\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nconst (\n\tllamaDeploymentFinalizer = \"deploy.llamaindex.ai/finalizer\"\n\n\t// LlamaDeployment status phases\n\tPhasePending       = \"Pending\"\n\tPhaseRunning       = \"Running\"\n\tPhaseFailed        = \"Failed\"\n\tPhaseRollingOut    = \"RollingOut\"\n\tPhaseRolloutFailed = \"RolloutFailed\"\n\tPhaseSuspended     = \"Suspended\"\n\tPhaseBuilding      = \"Building\"\n\tPhaseBuildFailed   = \"BuildFailed\"\n\tPhaseAwaitingCode  = \"AwaitingCode\"\n\n\t// Build status values for status.buildStatus\n\tBuildStatusPending   = \"Pending\"\n\tBuildStatusRunning   = \"Running\"\n\tBuildStatusSucceeded = \"Succeeded\"\n\tBuildStatusFailed    = \"Failed\"\n\n\t// Schema version for tracking CRD changes and forcing reconciliation\n\t// Increment this version when making schema changes that require full reconciliation\n\t// ONLY INCREMENT THIS WHEN MAKING SCHEMA CHANGES THAT REQUIRE FULL RECONCILIATION\n\tCurrentSchemaVersion = \"7\"\n\n\t// Environment variable for max concurrent rollouts configuration\n\tEnvMaxConcurrentRollouts = \"LLAMA_DEPLOY_MAX_CONCURRENT_ROLLOUTS\"\n\tEnvMaxDeployments        = \"LLAMA_DEPLOY_MAX_DEPLOYMENTS\"\n)\n\n// ptr returns a pointer to the given value\nfunc ptr[T any](v T) *T { return &v }\n\n// generateAuthToken generates a cryptographically secure random token using only alphanumeric characters\nfunc generateAuthToken() (string, error) {\n\tconst charset = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n\tconst tokenLength = 43 // Similar length to base64 encoding of 32 bytes\n\n\tbytes := make([]byte, tokenLength)\n\tif _, err := rand.Read(bytes); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate random token: %w\", err)\n\t}\n\n\tfor i := range bytes {\n\t\tbytes[i] = charset[bytes[i]%byte(len(charset))]\n\t}\n\n\treturn string(bytes), nil\n}\n\n// needsFullReconciliation determines if a full reconciliation is required\n// This happens when:\n// 1. Schema version doesn't match the current version\n// 2. The resource generation has changed since last reconciliation\n// 3. The resource has never been reconciled (initial creation)\nfunc (r *LlamaDeploymentReconciler) needsFullReconciliation(llamaDeploy *llamadeployv1.LlamaDeployment) bool {\n\t// Always reconcile if schema version doesn't match\n\tif llamaDeploy.Status.SchemaVersion != CurrentSchemaVersion {\n\t\treturn true\n\t}\n\n\t// Always reconcile if generation has changed (spec was updated)\n\tif llamaDeploy.Status.LastReconciledGeneration != llamaDeploy.Generation {\n\t\treturn true\n\t}\n\n\t// Always reconcile if this is the first reconciliation\n\tif llamaDeploy.Status.LastReconciledGeneration == 0 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// isTerminalFailure returns true if the phase is a terminal failure state\n// where no further reconciliation should occur for this generation.\nfunc isTerminalFailure(phase string) bool {\n\treturn phase == PhaseRolloutFailed || phase == PhaseFailed || phase == PhaseBuildFailed\n}\n\n// isActivePhase returns true if the phase represents an active deployment\n// (not suspended, not failed, and not empty/unset).\nfunc isActivePhase(phase string) bool {\n\treturn phase != PhaseSuspended && phase != PhaseFailed && phase != PhaseBuilding && phase != PhaseBuildFailed && phase != PhaseAwaitingCode && phase != \"\"\n}\n\n// LlamaDeploymentReconciler reconciles a LlamaDeployment object\ntype LlamaDeploymentReconciler struct {\n\tclient.Client\n\tScheme   *runtime.Scheme\n\tRecorder record.EventRecorder\n\t// DirectClient bypasses the informer cache. Use this for types that should\n\t// NOT be cached (e.g. ReplicaSets) to avoid loading potentially thousands\n\t// of objects into memory.\n\tDirectClient client.Client\n\t// MaxConcurrentRollouts limits how many LlamaDeployments can roll out\n\t// simultaneously. 0 means unlimited (default).\n\tMaxConcurrentRollouts int\n\t// MaxDeployments limits the total number of active (non-suspended, non-failed)\n\t// LlamaDeployments per namespace. 0 means unlimited (default).\n\tMaxDeployments int\n}\n\n// directClient returns a client that bypasses the informer cache.\nfunc (r *LlamaDeploymentReconciler) directClient() client.Client {\n\treturn r.DirectClient\n}\n\n// fetchDeployment retrieves the LlamaDeployment for the given request.\n// Returns (nil, nil) when the object has been deleted.\nfunc (r *LlamaDeploymentReconciler) fetchDeployment(ctx context.Context, req ctrl.Request) (*llamadeployv1.LlamaDeployment, error) {\n\tlogger := log.FromContext(ctx)\n\tld := &llamadeployv1.LlamaDeployment{}\n\tif err := r.Get(ctx, req.NamespacedName, ld); err != nil {\n\t\tif errors.IsNotFound(err) {\n\t\t\tlogger.Info(\"LlamaDeployment resource not found. Ignoring since object must be deleted\")\n\t\t\treturn nil, nil\n\t\t}\n\t\tlogger.Error(err, \"Failed to get LlamaDeployment\")\n\t\treturn nil, err\n\t}\n\treturn ld, nil\n}\n\n// migrateDisplayName moves spec.name to spec.displayName for existing CRDs.\n// Returns true if the object was patched and the reconcile should requeue.\nfunc (r *LlamaDeploymentReconciler) migrateDisplayName(ctx context.Context, ld *llamadeployv1.LlamaDeployment) (bool, error) {\n\tif ld.Spec.Name == \"\" || ld.Spec.DisplayName != \"\" {\n\t\t// No migration needed: either no old name or displayName already set\n\t\treturn false, nil\n\t}\n\n\tlogger := log.FromContext(ctx)\n\tlogger.Info(\"Migrating spec.name to spec.displayName\", \"name\", ld.Name)\n\n\tpatch := client.MergeFrom(ld.DeepCopy())\n\tld.Spec.DisplayName = ld.Spec.Name\n\tld.Spec.Name = \"\"\n\tif err := r.Patch(ctx, ld, patch); err != nil {\n\t\tlogger.Error(err, \"Failed to migrate displayName\")\n\t\treturn false, err\n\t}\n\n\t// Re-fetch to get the updated generation\n\tif err := r.Get(ctx, client.ObjectKeyFromObject(ld), ld); err != nil {\n\t\treturn false, err\n\t}\n\n\t// Update lastReconciledGeneration to the new generation so the spec change\n\t// doesn't trigger a full reconciliation (no rebuild, no pod restart).\n\tstatusPatch := client.MergeFrom(ld.DeepCopy())\n\tld.Status.LastReconciledGeneration = ld.Generation\n\tif err := r.Status().Patch(ctx, ld, statusPatch); err != nil {\n\t\tlogger.Error(err, \"Failed to update lastReconciledGeneration after displayName migration\")\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\n// ensureFinalizer adds the cleanup finalizer if not already present.\n// After updating, re-reads the object to pick up the fresh resourceVersion.\nfunc (r *LlamaDeploymentReconciler) ensureFinalizer(ctx context.Context, ld *llamadeployv1.LlamaDeployment) error {\n\tif controllerutil.ContainsFinalizer(ld, llamaDeploymentFinalizer) {\n\t\treturn nil\n\t}\n\tlogger := log.FromContext(ctx)\n\tcontrollerutil.AddFinalizer(ld, llamaDeploymentFinalizer)\n\tif err := r.Update(ctx, ld); err != nil {\n\t\tlogger.Error(err, \"Failed to add finalizer\")\n\t\treturn err\n\t}\n\tlogger.Info(\"Added finalizer to LlamaDeployment\")\n\t// Re-read to pick up the new resourceVersion after the Update\n\treturn r.Get(ctx, client.ObjectKeyFromObject(ld), ld)\n}\n\n// handleAlreadyFailed handles deployments whose current generation has already\n// been marked as a terminal failure. Still updates version tracking during\n// schema migration to prevent infinite loops.\nfunc (r *LlamaDeploymentReconciler) handleAlreadyFailed(ctx context.Context, ld *llamadeployv1.LlamaDeployment, needsFullReconcile bool) (ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\tif needsFullReconcile {\n\t\tif err := r.updateVersionTracking(ctx, ld); err != nil {\n\t\t\tlogger.Error(err, \"Failed to update version tracking for terminal deployment\")\n\t\t\treturn ctrl.Result{}, err\n\t\t}\n\t}\n\tlogger.Info(\"Skipping reconciliation for already-failed rollout generation\",\n\t\t\"generation\", ld.Generation)\n\treturn ctrl.Result{}, nil\n}\n\n// +kubebuilder:rbac:groups=deploy.llamaindex.ai,resources=llamadeployments,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=deploy.llamaindex.ai,resources=llamadeployments/status,verbs=get;update;patch\n// +kubebuilder:rbac:groups=deploy.llamaindex.ai,resources=llamadeployments/finalizers,verbs=update\n// +kubebuilder:rbac:groups=deploy.llamaindex.ai,resources=llamadeploymenttemplates,verbs=get;list;watch\n\n// +kubebuilder:rbac:groups=\"\",resources=secrets,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=\"\",resources=services,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=\"\",resources=configmaps,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=\"\",resources=events,verbs=get;list;watch;create;patch\n// Grant access for control plane log streaming and pod discovery\n// +kubebuilder:rbac:groups=\"\",resources=pods,verbs=get;list;watch\n// +kubebuilder:rbac:groups=\"\",resources=pods/log,verbs=get\n// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete\n// Needed to discover and scale down ReplicaSets during rollout timeout remediation\n// +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch;update;patch\n// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=\"\",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete\n// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete\n\n// Reconcile is part of the main kubernetes reconciliation loop which aims to\n// move the current state of the cluster closer to the desired state.\nfunc (r *LlamaDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\n\tld, err := r.fetchDeployment(ctx, req)\n\tif ld == nil || err != nil {\n\t\treturn ctrl.Result{}, err\n\t}\n\n\tif ld.DeletionTimestamp != nil {\n\t\tlogger.Info(\"LlamaDeployment is being deleted\")\n\t\treturn r.handleDeletion(ctx, ld)\n\t}\n\n\tlogger.Info(\"Reconciling LlamaDeployment\", \"name\", ld.Name, \"namespace\", ld.Namespace, \"generation\", ld.Generation, \"schemaVersion\", ld.Status.SchemaVersion)\n\n\t// Migrate spec.name → spec.displayName for existing CRDs\n\tif requeue, err := r.migrateDisplayName(ctx, ld); err != nil {\n\t\treturn ctrl.Result{}, err\n\t} else if requeue {\n\t\treturn ctrl.Result{Requeue: true}, nil\n\t}\n\n\tneedsFullReconcile := r.needsFullReconciliation(ld)\n\tif needsFullReconcile {\n\t\tlogger.Info(\"Full reconciliation required\",\n\t\t\t\"currentSchemaVersion\", CurrentSchemaVersion,\n\t\t\t\"resourceSchemaVersion\", ld.Status.SchemaVersion,\n\t\t\t\"generation\", ld.Generation,\n\t\t\t\"lastReconciledGeneration\", ld.Status.LastReconciledGeneration)\n\t}\n\n\t// Capacity gates — must come before initializeStatus so gated deployments\n\t// stay in their previous phase.\n\tif result, err := r.checkCapacityGates(ctx, ld, needsFullReconcile); result != nil || err != nil {\n\t\tif result != nil {\n\t\t\treturn *result, err\n\t\t}\n\t\treturn ctrl.Result{}, err\n\t}\n\n\tif err := r.ensureFinalizer(ctx, ld); err != nil {\n\t\treturn ctrl.Result{}, err\n\t}\n\n\tif err := r.initializeStatus(ctx, ld, needsFullReconcile); err != nil {\n\t\tlogger.Error(err, \"Failed to initialize status\")\n\t\treturn ctrl.Result{}, err\n\t}\n\n\t// Validation gates — each may short-circuit the reconcile\n\tif result, err := r.checkSecretGate(ctx, ld); result != nil || err != nil {\n\t\tif result != nil {\n\t\t\treturn *result, err\n\t\t}\n\t\treturn ctrl.Result{}, err\n\t}\n\n\tif !isValidDNS1035Label(ld.Name) {\n\t\treturn r.handleInvalidDNSName(ctx, ld)\n\t}\n\n\tif isTerminalFailure(ld.Status.Phase) && ld.Status.FailedRolloutGeneration == ld.Generation {\n\t\treturn r.handleAlreadyFailed(ctx, ld, needsFullReconcile)\n\t}\n\n\t// No code source configured — skip build and resources, stay Pending.\n\tif isAwaitingCodePush(ld) {\n\t\tlogger.Info(\"RepoUrl is empty, waiting for code push\", \"name\", ld.Name)\n\t\tif needsFullReconcile {\n\t\t\tif err := r.updateVersionTracking(ctx, ld); err != nil {\n\t\t\t\tlogger.Error(err, \"Failed to update version tracking\")\n\t\t\t\treturn ctrl.Result{}, err\n\t\t\t}\n\t\t}\n\t\treturn r.finalizePhase(ctx, ld, PhaseAwaitingCode, \"Waiting for code push\", 0, false)\n\t}\n\n\t// Build phase — may short-circuit if build is in progress or failed\n\tbuildId, buildResult, err := r.reconcileBuild(ctx, ld)\n\tif err != nil {\n\t\treturn ctrl.Result{}, err\n\t}\n\tif buildResult != nil {\n\t\treturn *buildResult, nil\n\t}\n\n\t// Apply runtime resources (ServiceAccount, ConfigMap, Deployment, Service)\n\tif err := r.reconcileResources(ctx, ld, buildId); err != nil {\n\t\treturn r.handleReconcileFailure(ctx, ld, err)\n\t}\n\n\t// Track schema version and release history\n\tif needsFullReconcile {\n\t\tif err := r.updateVersionTracking(ctx, ld); err != nil {\n\t\t\tlogger.Error(err, \"Failed to update version tracking\")\n\t\t\treturn ctrl.Result{}, err\n\t\t}\n\t}\n\n\t// Determine deployment health and act on failures\n\tphase, message, requeueAfter, statusDirty, err := r.assessDeploymentHealth(ctx, ld)\n\tif err != nil {\n\t\treturn ctrl.Result{}, err\n\t}\n\tif isFailedPhase(phase) && ld.Status.FailedRolloutGeneration != ld.Generation {\n\t\tif result := r.remediateFailedRollout(ctx, ld, phase, buildId); result != nil {\n\t\t\treturn *result, nil\n\t\t}\n\t\tstatusDirty = true\n\t}\n\n\treturn r.finalizePhase(ctx, ld, phase, message, requeueAfter, statusDirty)\n}\n\n// initializeStatus handles status initialization for new deployments or when full reconciliation is needed\nfunc (r *LlamaDeploymentReconciler) initializeStatus(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment, needsFullReconcile bool) error {\n\tlogger := log.FromContext(ctx)\n\n\t// Update status to Pending if not set or if full reconciliation is needed\n\tif llamaDeploy.Status.Phase == \"\" || needsFullReconcile {\n\t\tstatusUpdated := false\n\t\toldSchemaVersion := llamaDeploy.Status.SchemaVersion\n\t\t// Do not reset terminal phases, in-progress build phases, or suspended\n\t\t// phases during full reconciliation to avoid loops. PhaseBuilding is not\n\t\t// a failure state but should be preserved — resetting to Pending causes\n\t\t// status flip-flopping while a build Job is running. PhaseSuspended\n\t\t// should be preserved so suspended deployments don't get reset to\n\t\t// Pending and then consume rollout capacity or trigger builds.\n\t\tisTerminal := isTerminalFailure(llamaDeploy.Status.Phase) ||\n\t\t\tllamaDeploy.Status.Phase == PhaseBuilding ||\n\t\t\tllamaDeploy.Status.Phase == PhaseSuspended ||\n\t\t\tllamaDeploy.Status.Phase == PhaseAwaitingCode\n\n\t\t// Generate auth token for new deployments only.\n\t\t// We intentionally do NOT regenerate on schema version changes: rotating\n\t\t// the token changes the LLAMA_DEPLOY_AUTH_TOKEN env var in the pod\n\t\t// template, which triggers a rolling update for every single deployment.\n\t\t// With hundreds of deployments this creates a thundering-herd of\n\t\t// simultaneous rollouts that can OOM the operator.\n\t\tif llamaDeploy.Status.AuthToken == \"\" {\n\t\t\tauthToken, err := generateAuthToken()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to generate auth token: %w\", err)\n\t\t\t}\n\t\t\tllamaDeploy.Status.AuthToken = authToken\n\t\t\tstatusUpdated = true\n\t\t}\n\n\t\t// Update phase to Pending but do NOT update version tracking yet\n\t\t// Version tracking will be updated only after successful reconciliation\n\t\tif (llamaDeploy.Status.Phase == \"\" || needsFullReconcile) && !isTerminal {\n\t\t\tllamaDeploy.Status.Phase = PhasePending\n\t\t\tif needsFullReconcile {\n\t\t\t\tif llamaDeploy.Status.SchemaVersion != CurrentSchemaVersion {\n\t\t\t\t\tllamaDeploy.Status.Message = fmt.Sprintf(\"Schema version migration required (from %s to %s)\", llamaDeploy.Status.SchemaVersion, CurrentSchemaVersion)\n\t\t\t\t} else {\n\t\t\t\t\tllamaDeploy.Status.Message = \"Full reconciliation required due to spec changes\"\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tllamaDeploy.Status.Message = \"Starting reconciliation\"\n\t\t\t}\n\t\t\tnow := metav1.Now()\n\t\t\tllamaDeploy.Status.LastUpdated = &now\n\t\t\tstatusUpdated = true\n\t\t}\n\n\t\tif statusUpdated {\n\t\t\tif err := r.Status().Update(ctx, llamaDeploy); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to update status: %w\", err)\n\t\t\t}\n\t\t\tlogger.Info(\"Updated status to Pending\", \"generation\", llamaDeploy.Generation)\n\t\t\tif r.Recorder != nil {\n\t\t\t\tif needsFullReconcile && oldSchemaVersion != CurrentSchemaVersion {\n\t\t\t\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeNormal, \"SchemaVersionMigration\", fmt.Sprintf(\"Migrating from schema version %s to %s\", oldSchemaVersion, CurrentSchemaVersion))\n\t\t\t\t} else {\n\t\t\t\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeNormal, PhasePending, \"Started reconciliation of LlamaDeployment\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// handleReconcileFailure handles failures during resource reconciliation\nfunc (r *LlamaDeploymentReconciler) handleReconcileFailure(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment, reconcileErr error) (ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\tlogger.Error(reconcileErr, \"Failed to reconcile resources\")\n\n\t// Update status to Failed only if not already Failed\n\tif llamaDeploy.Status.Phase != PhaseFailed {\n\t\tllamaDeploy.Status.Phase = PhaseFailed\n\t\tllamaDeploy.Status.Message = fmt.Sprintf(\"Failed to reconcile resources: %v\", reconcileErr)\n\t\tnow := metav1.Now()\n\t\tllamaDeploy.Status.LastUpdated = &now\n\n\t\tif statusErr := r.Status().Update(ctx, llamaDeploy); statusErr != nil {\n\t\t\tlogger.Error(statusErr, \"Failed to update LlamaDeployment status to Failed\")\n\t\t} else {\n\t\t\tlogger.Info(\"Updated status to Failed\")\n\t\t\tif r.Recorder != nil {\n\t\t\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeWarning, PhaseFailed, fmt.Sprintf(\"Failed to reconcile resources: %v\", reconcileErr))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ctrl.Result{}, reconcileErr\n}\n\n// handleInvalidDNSName tears down owned resources and marks the deployment as\n// terminally failed when the metadata.name is not a valid DNS-1035 label.\nfunc (r *LlamaDeploymentReconciler) handleInvalidDNSName(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) (ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\tmessage := fmt.Sprintf(\n\t\t\"Deployment name %q is not a valid DNS-1035 label: must start with a lowercase letter, \"+\n\t\t\t\"contain only lowercase alphanumeric characters or '-', end with an alphanumeric character, \"+\n\t\t\t\"and be at most 63 characters (regex: '[a-z]([a-z0-9-]*[a-z0-9])?')\",\n\t\tllamaDeploy.Name,\n\t)\n\tlogger.Error(fmt.Errorf(\"invalid DNS-1035 label: %s\", llamaDeploy.Name), \"Tearing down resources for non-compliant deployment name\")\n\n\t// Tear down any existing resources that were created with the invalid name\n\tr.deleteOwnedResources(ctx, llamaDeploy)\n\n\t// Mark as Failed (terminal — metadata.name cannot be changed)\n\tif llamaDeploy.Status.Phase != PhaseFailed || llamaDeploy.Status.Message != message {\n\t\tllamaDeploy.Status.Phase = PhaseFailed\n\t\tllamaDeploy.Status.Message = message\n\t\tnow := metav1.Now()\n\t\tllamaDeploy.Status.LastUpdated = &now\n\n\t\tif statusErr := r.Status().Update(ctx, llamaDeploy); statusErr != nil {\n\t\t\tlogger.Error(statusErr, \"Failed to update status for invalid DNS name\")\n\t\t\treturn ctrl.Result{}, statusErr\n\t\t}\n\t\tif r.Recorder != nil {\n\t\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeWarning, PhaseFailed, message)\n\t\t}\n\t}\n\n\t// Don't requeue — this is a terminal failure\n\treturn ctrl.Result{}, nil\n}\n\n// deleteOwnedResources removes Deployment, Service, ConfigMap, and\n// ServiceAccount resources that were created for a LlamaDeployment.\nfunc (r *LlamaDeploymentReconciler) deleteOwnedResources(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) {\n\tlogger := log.FromContext(ctx)\n\tns := llamaDeploy.Namespace\n\tname := llamaDeploy.Name\n\n\t// Delete Deployment\n\tdep := &appsv1.Deployment{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, dep); err == nil {\n\t\tif err := r.Delete(ctx, dep); err != nil && !errors.IsNotFound(err) {\n\t\t\tlogger.Error(err, \"Failed to delete Deployment during teardown\", \"name\", name)\n\t\t} else {\n\t\t\tlogger.Info(\"Deleted Deployment during teardown\", \"name\", name)\n\t\t}\n\t}\n\n\t// Delete Service\n\tsvc := &corev1.Service{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, svc); err == nil {\n\t\tif err := r.Delete(ctx, svc); err != nil && !errors.IsNotFound(err) {\n\t\t\tlogger.Error(err, \"Failed to delete Service during teardown\", \"name\", name)\n\t\t} else {\n\t\t\tlogger.Info(\"Deleted Service during teardown\", \"name\", name)\n\t\t}\n\t}\n\n\t// Delete ConfigMap\n\tcm := &corev1.ConfigMap{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: name + \"-nginx-config\", Namespace: ns}, cm); err == nil {\n\t\tif err := r.Delete(ctx, cm); err != nil && !errors.IsNotFound(err) {\n\t\t\tlogger.Error(err, \"Failed to delete ConfigMap during teardown\", \"name\", name+\"-nginx-config\")\n\t\t} else {\n\t\t\tlogger.Info(\"Deleted ConfigMap during teardown\", \"name\", name+\"-nginx-config\")\n\t\t}\n\t}\n\n\t// Delete ServiceAccount\n\tsa := &corev1.ServiceAccount{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: name + \"-sa\", Namespace: ns}, sa); err == nil {\n\t\tif err := r.Delete(ctx, sa); err != nil && !errors.IsNotFound(err) {\n\t\t\tlogger.Error(err, \"Failed to delete ServiceAccount during teardown\", \"name\", name+\"-sa\")\n\t\t} else {\n\t\t\tlogger.Info(\"Deleted ServiceAccount during teardown\", \"name\", name+\"-sa\")\n\t\t}\n\t}\n}\n\n// updateVersionTracking updates the schema version and generation tracking after successful reconciliation\nfunc (r *LlamaDeploymentReconciler) updateVersionTracking(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) error {\n\tlogger := log.FromContext(ctx)\n\n\t// Refresh the resource to get latest status before updating version tracking\n\tif err := r.Get(ctx, client.ObjectKey{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}, llamaDeploy); err != nil {\n\t\treturn fmt.Errorf(\"failed to refresh LlamaDeployment after reconciliation: %w\", err)\n\t}\n\n\t// Append release history entry if git sha changed and is set\n\tfunc() {\n\t\t// avoid panics on nil slices\n\t\thistory := llamaDeploy.Status.ReleaseHistory\n\t\tcurrentSha := llamaDeploy.Spec.GitSha\n\t\tif currentSha == \"\" {\n\t\t\treturn\n\t\t}\n\t\t// only add if different from the last entry\n\t\tvar lastSha string\n\t\tif len(history) > 0 {\n\t\t\tlastSha = history[len(history)-1].GitSha\n\t\t}\n\t\tif lastSha == currentSha {\n\t\t\treturn\n\t\t}\n\t\t// append new entry\n\t\tnow := metav1.Now()\n\t\tentry := llamadeployv1.ReleaseHistoryEntry{\n\t\t\tGitSha:     currentSha,\n\t\t\tImageTag:   getContainerImageTag(llamaDeploy),\n\t\t\tReleasedAt: now,\n\t\t}\n\t\tllamaDeploy.Status.ReleaseHistory = append(history, entry)\n\t\t// enforce max 20 entries (keep most recent)\n\t\tif len(llamaDeploy.Status.ReleaseHistory) > 20 {\n\t\t\tllamaDeploy.Status.ReleaseHistory = llamaDeploy.Status.ReleaseHistory[1:]\n\t\t}\n\t}()\n\n\tllamaDeploy.Status.SchemaVersion = CurrentSchemaVersion\n\tllamaDeploy.Status.LastReconciledGeneration = llamaDeploy.Generation\n\tnow := metav1.Now()\n\tllamaDeploy.Status.LastUpdated = &now\n\n\tif err := r.Status().Update(ctx, llamaDeploy); err != nil {\n\t\treturn fmt.Errorf(\"failed to update version tracking: %w\", err)\n\t}\n\tlogger.Info(\"Updated version tracking after successful reconciliation\",\n\t\t\"schemaVersion\", CurrentSchemaVersion,\n\t\t\"generation\", llamaDeploy.Generation)\n\n\treturn nil\n}\n\n// handleDeletion handles cleanup when a LlamaDeployment is being deleted\nfunc (r *LlamaDeploymentReconciler) handleDeletion(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) (ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\n\t// Delete the secret if it exists and is specified\n\tif llamaDeploy.Spec.SecretName != \"\" {\n\t\tif err := r.deleteSecret(ctx, llamaDeploy); err != nil {\n\t\t\tlogger.Error(err, \"Failed to delete secret\", \"secretName\", llamaDeploy.Spec.SecretName)\n\t\t\treturn ctrl.Result{}, err\n\t\t}\n\t\tlogger.Info(\"Deleted secret\", \"secretName\", llamaDeploy.Spec.SecretName)\n\t}\n\n\t// Remove finalizer\n\tcontrollerutil.RemoveFinalizer(llamaDeploy, llamaDeploymentFinalizer)\n\tif err := r.Update(ctx, llamaDeploy); err != nil {\n\t\tlogger.Error(err, \"Failed to remove finalizer\")\n\t\treturn ctrl.Result{}, err\n\t}\n\n\tlogger.Info(\"Finalizer removed, LlamaDeployment cleanup complete\")\n\treturn ctrl.Result{}, nil\n}\n\n// deleteSecret removes the secret associated with the LlamaDeployment\nfunc (r *LlamaDeploymentReconciler) deleteSecret(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) error {\n\tsecret := &corev1.Secret{}\n\terr := r.Get(ctx, client.ObjectKey{Name: llamaDeploy.Spec.SecretName, Namespace: llamaDeploy.Namespace}, secret)\n\tif err != nil && errors.IsNotFound(err) {\n\t\t// Secret doesn't exist, nothing to delete\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn err\n\t}\n\n\treturn r.Delete(ctx, secret)\n}\n\n// SetupWithManager sets up the controller with the Manager.\nfunc (r *LlamaDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {\n\treturn ctrl.NewControllerManagedBy(mgr).\n\t\tFor(&llamadeployv1.LlamaDeployment{}).\n\t\tOwns(&appsv1.Deployment{}).\n\t\tOwns(&corev1.Service{}).\n\t\tOwns(&corev1.ConfigMap{}).\n\t\tOwns(&batchv1.Job{}).\n\t\t// Watch template changes and enqueue all LlamaDeployments in the namespace\n\t\tWatches(\n\t\t\t&llamadeployv1.LlamaDeploymentTemplate{},\n\t\t\thandler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {\n\t\t\t\tvar list llamadeployv1.LlamaDeploymentList\n\t\t\t\tif err := mgr.GetClient().List(ctx, &list, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treqs := make([]reconcile.Request, 0, len(list.Items))\n\t\t\t\tfor i := range list.Items {\n\t\t\t\t\tld := &list.Items[i]\n\t\t\t\t\treqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(ld)})\n\t\t\t\t}\n\t\t\t\treturn reqs\n\t\t\t}),\n\t\t).\n\t\t// Self-watch: when a LlamaDeployment frees capacity (deleted, or\n\t\t// transitions to Suspended/Failed), wake up other deployments that\n\t\t// may be gated by MaxDeployments.\n\t\tWatches(\n\t\t\t&llamadeployv1.LlamaDeployment{},\n\t\t\thandler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {\n\t\t\t\tif r.MaxDeployments <= 0 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tvar list llamadeployv1.LlamaDeploymentList\n\t\t\t\tif err := mgr.GetClient().List(ctx, &list, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tvar reqs []reconcile.Request\n\t\t\t\tfor i := range list.Items {\n\t\t\t\t\tld := &list.Items[i]\n\t\t\t\t\tif ld.Name == obj.GetName() && ld.Namespace == obj.GetNamespace() {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif ld.Generation != ld.Status.LastReconciledGeneration {\n\t\t\t\t\t\treqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(ld)})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn reqs\n\t\t\t}),\n\t\t\tbuilder.WithPredicates(predicate.Funcs{\n\t\t\t\tCreateFunc: func(e event.CreateEvent) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t\tUpdateFunc: func(e event.UpdateEvent) bool {\n\t\t\t\t\toldLD, ok1 := e.ObjectOld.(*llamadeployv1.LlamaDeployment)\n\t\t\t\t\tnewLD, ok2 := e.ObjectNew.(*llamadeployv1.LlamaDeployment)\n\t\t\t\t\tif !ok1 || !ok2 {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\t// Trigger when an active deployment becomes suspended or failed\n\t\t\t\t\treturn isActivePhase(oldLD.Status.Phase) && !isActivePhase(newLD.Status.Phase)\n\t\t\t\t},\n\t\t\t\tDeleteFunc: func(e event.DeleteEvent) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t\tGenericFunc: func(e event.GenericEvent) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}),\n\t\t).\n\t\t// Self-watch: when a LlamaDeployment transitions OUT of a rolling\n\t\t// phase (Pending/RollingOut/Building), immediately wake CRs gated by\n\t\t// checkRolloutCapacity instead of waiting for their jitter timer.\n\t\tWatches(\n\t\t\t&llamadeployv1.LlamaDeployment{},\n\t\t\thandler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {\n\t\t\t\tif r.MaxConcurrentRollouts <= 0 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tvar list llamadeployv1.LlamaDeploymentList\n\t\t\t\tif err := mgr.GetClient().List(ctx, &list, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tvar reqs []reconcile.Request\n\t\t\t\tfor i := range list.Items {\n\t\t\t\t\tld := &list.Items[i]\n\t\t\t\t\tif ld.Name == obj.GetName() && ld.Namespace == obj.GetNamespace() {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif ld.Generation != ld.Status.LastReconciledGeneration {\n\t\t\t\t\t\treqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(ld)})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn reqs\n\t\t\t}),\n\t\t\tbuilder.WithPredicates(predicate.Funcs{\n\t\t\t\tCreateFunc: func(e event.CreateEvent) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t\tUpdateFunc: func(e event.UpdateEvent) bool {\n\t\t\t\t\toldLD, ok1 := e.ObjectOld.(*llamadeployv1.LlamaDeployment)\n\t\t\t\t\tnewLD, ok2 := e.ObjectNew.(*llamadeployv1.LlamaDeployment)\n\t\t\t\t\tif !ok1 || !ok2 {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\t// Fire when transitioning OUT of a rolling phase\n\t\t\t\t\treturn isRollingPhase(oldLD.Status.Phase) && !isRollingPhase(newLD.Status.Phase)\n\t\t\t\t},\n\t\t\t\tDeleteFunc: func(e event.DeleteEvent) bool {\n\t\t\t\t\t// A deleted CR frees a rollout slot\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t\tGenericFunc: func(e event.GenericEvent) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}),\n\t\t).\n\t\tComplete(r)\n}\n\n// Note: No custom EventHandler needed; we map template changes to all LlamaDeployments via EnqueueRequestsFromMapFunc.\n"
  },
  {
    "path": "operator/internal/controller/reconcile_test.go",
    "content": "//go:build !integration\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client/fake\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\n// ---------------------------------------------------------------------------\n// needsFullReconciliation tests\n// ---------------------------------------------------------------------------\n\nfunc TestNeedsFullReconciliation(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tschemaVersion  string\n\t\tgeneration     int64\n\t\tlastReconciled int64\n\t\twant           bool\n\t}{\n\t\t{\n\t\t\tname:           \"schema version mismatch triggers full reconcile\",\n\t\t\tschemaVersion:  \"0\",\n\t\t\tgeneration:     1,\n\t\t\tlastReconciled: 1,\n\t\t\twant:           true,\n\t\t},\n\t\t{\n\t\t\tname:           \"generation mismatch triggers full reconcile\",\n\t\t\tschemaVersion:  CurrentSchemaVersion,\n\t\t\tgeneration:     2,\n\t\t\tlastReconciled: 1,\n\t\t\twant:           true,\n\t\t},\n\t\t{\n\t\t\tname:           \"initial reconciliation (lastReconciled=0) triggers full reconcile\",\n\t\t\tschemaVersion:  CurrentSchemaVersion,\n\t\t\tgeneration:     0,\n\t\t\tlastReconciled: 0,\n\t\t\twant:           true,\n\t\t},\n\t\t{\n\t\t\tname:           \"everything matches, no full reconcile needed\",\n\t\t\tschemaVersion:  CurrentSchemaVersion,\n\t\t\tgeneration:     3,\n\t\t\tlastReconciled: 3,\n\t\t\twant:           false,\n\t\t},\n\t\t{\n\t\t\tname:           \"empty schema version triggers full reconcile\",\n\t\t\tschemaVersion:  \"\",\n\t\t\tgeneration:     1,\n\t\t\tlastReconciled: 1,\n\t\t\twant:           true,\n\t\t},\n\t}\n\n\tr := &LlamaDeploymentReconciler{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"test\", Namespace: \"default\", Generation: tt.generation},\n\t\t\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\t\t\tSchemaVersion:            tt.schemaVersion,\n\t\t\t\t\tLastReconciledGeneration: tt.lastReconciled,\n\t\t\t\t},\n\t\t\t}\n\t\t\tgot := r.needsFullReconciliation(ld)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"needsFullReconciliation() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// isTerminalFailure tests\n// ---------------------------------------------------------------------------\n\nfunc TestIsTerminalFailure(t *testing.T) {\n\ttests := []struct {\n\t\tphase string\n\t\twant  bool\n\t}{\n\t\t{PhaseRolloutFailed, true},\n\t\t{PhaseFailed, true},\n\t\t{PhaseBuildFailed, true},\n\t\t{PhaseRunning, false},\n\t\t{PhasePending, false},\n\t\t{PhaseBuilding, false},\n\t\t{PhaseSuspended, false},\n\t\t{PhaseAwaitingCode, false},\n\t\t{\"\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.phase, func(t *testing.T) {\n\t\t\tif got := isTerminalFailure(tt.phase); got != tt.want {\n\t\t\t\tt.Errorf(\"isTerminalFailure(%q) = %v, want %v\", tt.phase, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// isActivePhase tests\n// ---------------------------------------------------------------------------\n\nfunc TestIsActivePhase(t *testing.T) {\n\ttests := []struct {\n\t\tphase string\n\t\twant  bool\n\t}{\n\t\t{PhaseRunning, true},\n\t\t{PhasePending, true},\n\t\t{PhaseRollingOut, true},\n\t\t{PhaseRolloutFailed, true},\n\t\t{PhaseSuspended, false},\n\t\t{PhaseFailed, false},\n\t\t{PhaseBuilding, false},\n\t\t{PhaseBuildFailed, false},\n\t\t{PhaseAwaitingCode, false},\n\t\t{\"\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.phase, func(t *testing.T) {\n\t\t\tif got := isActivePhase(tt.phase); got != tt.want {\n\t\t\t\tt.Errorf(\"isActivePhase(%q) = %v, want %v\", tt.phase, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// handleAlreadyFailed tests\n// ---------------------------------------------------------------------------\n\nfunc TestHandleAlreadyFailed_SkipsWhenNoFullReconcile(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\", Generation: 1},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:                   PhaseFailed,\n\t\t\tFailedRolloutGeneration: 1,\n\t\t},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tresult, err := r.handleAlreadyFailed(ctx, ld, false)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result.RequeueAfter != 0 {\n\t\tt.Errorf(\"expected empty result, got %+v\", result)\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// handleReconcileFailure tests\n// ---------------------------------------------------------------------------\n\nfunc TestHandleReconcileFailure_UpdatesStatusToFailed(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhasePending,\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\treconcileErr := fmt.Errorf(\"something went wrong\")\n\t_, err := r.handleReconcileFailure(ctx, ld, reconcileErr)\n\tif err == nil {\n\t\tt.Fatal(\"expected error to be returned\")\n\t}\n\tif ld.Status.Phase != PhaseFailed {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhaseFailed, ld.Status.Phase)\n\t}\n\tif ld.Status.LastUpdated == nil {\n\t\tt.Error(\"expected LastUpdated to be set\")\n\t}\n}\n\nfunc TestHandleReconcileFailure_DoesNotOverwriteAlreadyFailed(t *testing.T) {\n\tscheme := testSchemeWithApps()\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{ProjectId: \"p\", RepoUrl: \"r\"},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:     PhaseFailed,\n\t\t\tMessage:   \"original failure\",\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\treconcileErr := fmt.Errorf(\"new error\")\n\t_, _ = r.handleReconcileFailure(ctx, ld, reconcileErr)\n\n\t// Message should remain the original since phase was already Failed\n\tif ld.Status.Message != \"original failure\" {\n\t\tt.Errorf(\"expected original message preserved, got %q\", ld.Status.Message)\n\t}\n}\n"
  },
  {
    "path": "operator/internal/controller/resources.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/errors\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/util/intstr\"\n\t\"k8s.io/apimachinery/pkg/util/strategicpatch\"\n\tctrl \"sigs.k8s.io/controller-runtime\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil\"\n\t\"sigs.k8s.io/controller-runtime/pkg/log\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nconst (\n\t// Container image configuration\n\tDefaultImage    = \"llamaindex/llama-agents-appserver\"\n\tDefaultImageTag = \"latest\"\n\n\t// Environment variables for image configuration\n\tEnvImageName       = \"LLAMA_DEPLOY_IMAGE\"\n\tEnvImageTag        = \"LLAMA_DEPLOY_IMAGE_TAG\"\n\tEnvImagePullPolicy = \"LLAMA_DEPLOY_IMAGE_PULL_POLICY\"\n\n\t// Nginx sidecar image configuration (configurable via Helm → operator env)\n\tDefaultNginxImage       = \"nginxinc/nginx-unprivileged\"\n\tDefaultNginxImageTag    = \"1.27-alpine\"\n\tEnvNginxImageName       = \"LLAMA_DEPLOY_NGINX_IMAGE\"\n\tEnvNginxImageTag        = \"LLAMA_DEPLOY_NGINX_IMAGE_TAG\"\n\tEnvNginxImagePullPolicy = \"LLAMA_DEPLOY_NGINX_IMAGE_PULL_POLICY\"\n\n\t// Default resource request env overrides for app container\n\tEnvDefaultCPURequest    = \"LLAMA_DEPLOY_DEFAULT_CPU_REQUEST\"\n\tEnvDefaultMemoryRequest = \"LLAMA_DEPLOY_DEFAULT_MEMORY_REQUEST\"\n\n\t// Default resource limit env overrides for app container\n\t// CPU limit defaults to unset (empty); memory limit defaults to 4096Mi\n\tEnvDefaultCPULimit    = \"LLAMA_DEPLOY_DEFAULT_CPU_LIMIT\"\n\tEnvDefaultMemoryLimit = \"LLAMA_DEPLOY_DEFAULT_MEMORY_LIMIT\"\n\n\t// Container name constants\n\tContainerNameApp   = \"app\"\n\tContainerNameBuild = \"build\"\n\n\t// Container user/group IDs\n\tAppServerUID int64 = 1001\n\tAppServerGID int64 = 1001\n\tNginxUID     int64 = 101\n\tNginxGID     int64 = 101\n\n\t// Default pull policy\n\tDefaultImagePullPolicy = \"IfNotPresent\"\n\n\t// Default rollout timeout in seconds (30 minutes)\n\tDefaultRolloutTimeoutSeconds int32 = 1800\n\n\t// Build artifact GC walks ReplicaSets up to this limit to discover\n\t// referenced buildIds, so pin it rather than relying on the apps/v1 default.\n\tDeploymentRevisionHistoryLimit int32 = 10\n\n\t// Environment variable for rollout timeout configuration\n\tEnvRolloutTimeoutSeconds = \"LLAMA_DEPLOY_ROLLOUT_TIMEOUT_SECONDS\"\n\n\t// appserverTagPrefix is retained for backward compatibility with old-style\n\t// image tags like \"appserver-0.8.1\". New tags are plain versions (\"0.8.1\").\n\tappserverTagPrefix = \"appserver-\"\n)\n\n// buildJobName computes the Job name for a given deployment/buildId combo,\n// truncated to Kubernetes' 63-character name limit.\nfunc buildJobName(deploymentName, buildId string) string {\n\tname := fmt.Sprintf(\"%s-build-%s\", deploymentName, buildId)\n\tif len(name) > 63 {\n\t\tname = name[:63]\n\t}\n\treturn name\n}\n\n// looksLikeFilePath provides a lightweight heuristic to determine if a\n// deployment file path refers to a file rather than a directory. It avoids\n// filesystem access and relies on common file extensions and presence of an extension.\nfunc looksLikeFilePath(p string) bool {\n\tbase := path.Base(p)\n\tif p == \".\" || p == \"/\" {\n\t\treturn false\n\t}\n\tif strings.Contains(base, \".\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// getDefaultImage returns the operator-level default container image,\n// ignoring any per-deployment spec overrides.\n// 1. Use environment variable LLAMA_DEPLOY_IMAGE if set\n// 2. Fall back to default: \"llamaindex/llama-agents-appserver\"\nfunc getDefaultImage() string {\n\tif envImage := os.Getenv(EnvImageName); envImage != \"\" {\n\t\treturn envImage\n\t}\n\treturn DefaultImage\n}\n\n// getDefaultImageTag returns the operator-level default container image tag,\n// ignoring any per-deployment spec overrides.\n// 1. Use environment variable LLAMA_DEPLOY_IMAGE_TAG if set\n// 2. Fall back to default: \"latest\"\nfunc getDefaultImageTag() string {\n\tif envTag := os.Getenv(EnvImageTag); envTag != \"\" {\n\t\treturn envTag\n\t}\n\treturn DefaultImageTag\n}\n\n// getContainerImage returns the container image to use, with fallback logic:\n// 1. Use spec.Image if specified\n// 2. Use environment variable LLAMA_DEPLOY_IMAGE if set\n// 3. Fall back to default: \"llamaindex/llama-agents-appserver\"\nfunc getContainerImage(llamaDeploy *llamadeployv1.LlamaDeployment) string {\n\tif llamaDeploy.Spec.Image != \"\" {\n\t\treturn llamaDeploy.Spec.Image\n\t}\n\tif envImage := os.Getenv(EnvImageName); envImage != \"\" {\n\t\treturn envImage\n\t}\n\treturn DefaultImage\n}\n\n// getContainerImageTag returns the container image tag to use, with fallback logic:\n// 1. Use spec.ImageTag if specified (per-deployment pinning via control plane)\n// 2. Use environment variable LLAMA_DEPLOY_IMAGE_TAG if set (fallback for CRDs without imageTag)\n// 3. Fall back to default: \"appserver-latest\"\nfunc getContainerImageTag(llamaDeploy *llamadeployv1.LlamaDeployment) string {\n\tif llamaDeploy.Spec.ImageTag != \"\" {\n\t\t// Backward compat: strip legacy \"appserver-\" prefix if present.\n\t\t// New-style tags are plain versions (e.g. \"0.8.1\"), old-style are \"appserver-0.8.1\".\n\t\treturn strings.TrimPrefix(llamaDeploy.Spec.ImageTag, appserverTagPrefix)\n\t}\n\tif envTag := os.Getenv(EnvImageTag); envTag != \"\" {\n\t\treturn envTag\n\t}\n\treturn DefaultImageTag\n}\n\n// getContainerImagePullPolicy returns the container image pull policy to use, with fallback logic:\n// 1. Use environment variable LLAMA_DEPLOY_IMAGE_PULL_POLICY if set\n// 2. Fall back to default: \"IfNotPresent\"\nfunc getContainerImagePullPolicy() corev1.PullPolicy {\n\tif envPullPolicy := os.Getenv(EnvImagePullPolicy); envPullPolicy != \"\" {\n\t\tswitch envPullPolicy {\n\t\tcase \"Always\":\n\t\t\treturn corev1.PullAlways\n\t\tcase \"Never\":\n\t\t\treturn corev1.PullNever\n\t\tcase \"IfNotPresent\":\n\t\t\treturn corev1.PullIfNotPresent\n\t\tdefault:\n\t\t\t// Log warning for invalid value and use default\n\t\t\treturn corev1.PullIfNotPresent\n\t\t}\n\t}\n\treturn corev1.PullIfNotPresent\n}\n\n// getPullPolicyFromEnv returns a pull policy based on the given env var name.\nfunc getPullPolicyFromEnv(envVarName string) corev1.PullPolicy {\n\tif envPullPolicy := os.Getenv(envVarName); envPullPolicy != \"\" {\n\t\tswitch envPullPolicy {\n\t\tcase \"Always\":\n\t\t\treturn corev1.PullAlways\n\t\tcase \"Never\":\n\t\t\treturn corev1.PullNever\n\t\tcase \"IfNotPresent\":\n\t\t\treturn corev1.PullIfNotPresent\n\t\tdefault:\n\t\t\treturn corev1.PullIfNotPresent\n\t\t}\n\t}\n\treturn corev1.PullIfNotPresent\n}\n\n// getNginxImage returns the nginx sidecar image repository to use.\nfunc getNginxImage() string {\n\tif envImage := os.Getenv(EnvNginxImageName); envImage != \"\" {\n\t\treturn envImage\n\t}\n\treturn DefaultNginxImage\n}\n\n// getNginxImageTag returns the nginx sidecar image tag to use.\nfunc getNginxImageTag() string {\n\tif envTag := os.Getenv(EnvNginxImageTag); envTag != \"\" {\n\t\treturn envTag\n\t}\n\treturn DefaultNginxImageTag\n}\n\n// getNginxImagePullPolicy returns the nginx sidecar image pull policy to use.\nfunc getNginxImagePullPolicy() corev1.PullPolicy {\n\treturn getPullPolicyFromEnv(EnvNginxImagePullPolicy)\n}\n\n// parseOptionalQuantityFromEnv reads a quantity from an env var.\n// Returns (nil, true) if the env var is present but intentionally unset\n// (empty/none/unset/null/nil). Returns (quantity, true) if present and valid;\n// (nil, false) if not present or invalid.\nfunc parseOptionalQuantityFromEnv(envVarName string) (*resource.Quantity, bool) {\n\traw, present := os.LookupEnv(envVarName)\n\tif !present {\n\t\treturn nil, false\n\t}\n\tv := strings.TrimSpace(raw)\n\tif v == \"\" {\n\t\treturn nil, true\n\t}\n\tlower := strings.ToLower(v)\n\tif lower == \"none\" || lower == \"unset\" || lower == \"null\" || lower == \"nil\" {\n\t\treturn nil, true\n\t}\n\tif q, err := resource.ParseQuantity(v); err == nil {\n\t\treturn &q, true\n\t}\n\treturn nil, false\n}\n\n// getDefaultResourceRequests constructs the default ResourceList for requests.\n// Defaults: CPU 750m, memory 2Gi. If env var is present but empty/none, the field is omitted.\nfunc getDefaultResourceRequests() corev1.ResourceList {\n\trequests := corev1.ResourceList{}\n\tif q, present := parseOptionalQuantityFromEnv(EnvDefaultCPURequest); present {\n\t\tif q != nil {\n\t\t\trequests[corev1.ResourceCPU] = *q\n\t\t}\n\t} else {\n\t\trequests[corev1.ResourceCPU] = resource.MustParse(\"750m\")\n\t}\n\tif q, present := parseOptionalQuantityFromEnv(EnvDefaultMemoryRequest); present {\n\t\tif q != nil {\n\t\t\trequests[corev1.ResourceMemory] = *q\n\t\t}\n\t} else {\n\t\trequests[corev1.ResourceMemory] = resource.MustParse(\"2Gi\")\n\t}\n\treturn requests\n}\n\n// getDefaultResourceLimits constructs the default ResourceList for limits.\n// Defaults: CPU unset, memory 4096Mi. If env var is present but empty/none, the field is omitted.\nfunc getDefaultResourceLimits() corev1.ResourceList {\n\tlimits := corev1.ResourceList{}\n\tif q, present := parseOptionalQuantityFromEnv(EnvDefaultCPULimit); present {\n\t\tif q != nil {\n\t\t\tlimits[corev1.ResourceCPU] = *q\n\t\t}\n\t}\n\tif q, present := parseOptionalQuantityFromEnv(EnvDefaultMemoryLimit); present {\n\t\tif q != nil {\n\t\t\tlimits[corev1.ResourceMemory] = *q\n\t\t}\n\t} else {\n\t\tlimits[corev1.ResourceMemory] = resource.MustParse(\"4096Mi\")\n\t}\n\treturn limits\n}\n\n// defaultPodSecurityContext returns the shared pod-level security context.\nfunc defaultPodSecurityContext() *corev1.PodSecurityContext {\n\treturn &corev1.PodSecurityContext{\n\t\tFSGroup: ptr(AppServerGID),\n\t}\n}\n\n// defaultContainerSecurityContext returns the hardened container security context\n// for containers running as the appserver user (uid/gid 1001).\nfunc defaultContainerSecurityContext() *corev1.SecurityContext {\n\treturn &corev1.SecurityContext{\n\t\tRunAsNonRoot:             ptr(true),\n\t\tRunAsUser:                ptr(AppServerUID),\n\t\tRunAsGroup:               ptr(AppServerGID),\n\t\tAllowPrivilegeEscalation: ptr(false),\n\t\tCapabilities:             &corev1.Capabilities{Drop: []corev1.Capability{\"ALL\"}},\n\t}\n}\n\n// hardenedSecurityContext returns a minimal hardened security context that drops\n// all capabilities and disables privilege escalation, without setting uid/gid\n// (for containers whose uid is set by the image, e.g. the appserver).\nfunc hardenedSecurityContext() *corev1.SecurityContext {\n\treturn &corev1.SecurityContext{\n\t\tAllowPrivilegeEscalation: ptr(false),\n\t\tCapabilities:             &corev1.Capabilities{Drop: []corev1.Capability{\"ALL\"}},\n\t}\n}\n\n// getRolloutTimeout returns the configured rollout timeout duration.\n// Reads from LLAMA_DEPLOY_ROLLOUT_TIMEOUT_SECONDS env var, falling back to DefaultRolloutTimeoutSeconds.\nfunc getRolloutTimeout() time.Duration {\n\treturn time.Duration(getRolloutTimeoutSecondsValue()) * time.Second\n}\n\n// getRolloutTimeoutSeconds returns the configured rollout timeout as *int32.\nfunc getRolloutTimeoutSeconds() *int32 {\n\tv := getRolloutTimeoutSecondsValue()\n\treturn &v\n}\n\n// getRolloutTimeoutSecondsValue reads the timeout from env var or returns the default.\nfunc getRolloutTimeoutSecondsValue() int32 {\n\tif raw := os.Getenv(EnvRolloutTimeoutSeconds); raw != \"\" {\n\t\tif v, err := strconv.ParseInt(raw, 10, 32); err == nil && v > 0 {\n\t\t\treturn int32(v)\n\t\t}\n\t}\n\treturn DefaultRolloutTimeoutSeconds\n}\n\n// isValidDNS1035Label validates that a name is a valid DNS-1035 label.\n// DNS-1035 labels must: start with [a-z], end with [a-z0-9], contain only\n// [a-z0-9-], and be 1-63 characters long.\nfunc isValidDNS1035Label(name string) bool {\n\tif len(name) == 0 || len(name) > 63 {\n\t\treturn false\n\t}\n\tfor i := 0; i < len(name); i++ {\n\t\tc := name[i]\n\t\tif c >= 'a' && c <= 'z' {\n\t\t\tcontinue\n\t\t}\n\t\tif c >= '0' && c <= '9' {\n\t\t\tif i == 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif c == '-' {\n\t\t\tif i == 0 || i == len(name)-1 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\n// computeBuildId computes a deterministic build identifier from the inputs that\n// affect build output: name + gitSha + buildGeneration. spec.ImageTag is NOT\n// included because the build Job always uses the operator's default image, not\n// the deployment's pinned image.\nfunc computeBuildId(llamaDeploy *llamadeployv1.LlamaDeployment) string {\n\tparts := []string{llamaDeploy.Name, llamaDeploy.Spec.GitSha}\n\tif llamaDeploy.Spec.BuildGeneration > 0 {\n\t\tparts = append(parts, fmt.Sprintf(\"bg:%d\", llamaDeploy.Spec.BuildGeneration))\n\t}\n\tcontent := strings.Join(parts, \"|\")\n\thash := fmt.Sprintf(\"%x\", sha256.Sum256([]byte(content)))\n\treturn hash[:16]\n}\n\n// supersedeStaleBuildJob cancels any in-flight build Job whose buildId has\n// been superseded by a new spec. Succeeded stale Jobs are preserved for the\n// A → B → A rollback-by-cache-hit path. A missing Job is a no-op (TTL may\n// have reaped it, or a prior reconcile deleted it).\nfunc (r *LlamaDeploymentReconciler) supersedeStaleBuildJob(\n\tctx context.Context,\n\tllamaDeploy *llamadeployv1.LlamaDeployment,\n\tnewBuildId string,\n) error {\n\tif llamaDeploy.Status.BuildId == \"\" || llamaDeploy.Status.BuildId == newBuildId {\n\t\treturn nil\n\t}\n\tif llamaDeploy.Status.BuildStatus != BuildStatusPending &&\n\t\tllamaDeploy.Status.BuildStatus != BuildStatusRunning {\n\t\treturn nil\n\t}\n\n\tlogger := log.FromContext(ctx)\n\tstaleBuildId := llamaDeploy.Status.BuildId\n\tstaleJobName := buildJobName(llamaDeploy.Name, staleBuildId)\n\tstaleJob := &batchv1.Job{}\n\tgetErr := r.Get(ctx, client.ObjectKey{Name: staleJobName, Namespace: llamaDeploy.Namespace}, staleJob)\n\tswitch {\n\tcase errors.IsNotFound(getErr):\n\t\treturn nil\n\tcase getErr != nil:\n\t\treturn fmt.Errorf(\"failed to check stale build job: %w\", getErr)\n\tcase staleJob.Status.Succeeded > 0:\n\t\tlogger.V(1).Info(\"Prior build succeeded; leaving Job in place\",\n\t\t\t\"staleBuildId\", staleBuildId, \"jobName\", staleJobName)\n\t\treturn nil\n\t}\n\n\tlogger.Info(\"Superseding in-flight build Job for previous buildId\",\n\t\t\"staleBuildId\", staleBuildId,\n\t\t\"newBuildId\", newBuildId,\n\t\t\"jobName\", staleJobName)\n\tif err := r.Delete(ctx, staleJob,\n\t\tclient.PropagationPolicy(metav1.DeletePropagationBackground),\n\t); err != nil && !errors.IsNotFound(err) {\n\t\treturn fmt.Errorf(\"failed to delete superseded build job: %w\", err)\n\t}\n\tif r.Recorder != nil {\n\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeNormal, PhaseBuilding,\n\t\t\tfmt.Sprintf(\"Superseded stale build Job: %s\", staleJobName))\n\t}\n\treturn nil\n}\n\n// reconcileBuild ensures a build artifact exists for the current deployment inputs.\n// Returns the buildId if a build is ready, or (\"\", result, nil) if a build is in progress\n// and the caller should requeue.\nfunc (r *LlamaDeploymentReconciler) reconcileBuild(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) (string, *ctrl.Result, error) {\n\tlogger := log.FromContext(ctx)\n\n\t// No code source configured — wait for a git push to set RepoUrl.\n\tif isAwaitingCodePush(llamaDeploy) {\n\t\tlogger.Info(\"Skipping build: RepoUrl is empty, waiting for code push\")\n\t\treturn \"\", nil, nil\n\t}\n\n\t// Suspended deployments should not start or continue builds unless\n\t// spec.buildGeneration was explicitly bumped past status.lastBuiltGeneration.\n\t// This lets operators pre-build artifacts for suspended deployments\n\t// so that unsuspending is instant.\n\tif llamaDeploy.Spec.Suspended &&\n\t\tllamaDeploy.Spec.BuildGeneration <= llamaDeploy.Status.LastBuiltGeneration {\n\t\tlogger.V(1).Info(\"Skipping build for suspended deployment\")\n\t\treturn \"\", nil, nil\n\t}\n\n\tbuildId := computeBuildId(llamaDeploy)\n\n\t// If the CRD already has this buildId marked as succeeded, skip the build\n\tif llamaDeploy.Status.BuildId == buildId && llamaDeploy.Status.BuildStatus == BuildStatusSucceeded {\n\t\tlogger.V(1).Info(\"Build artifact already exists\", \"buildId\", buildId)\n\t\treturn buildId, nil, nil\n\t}\n\n\t// If the spec advanced mid-build, cancel the stale in-flight Job so it\n\t// doesn't race the new one uploading.\n\tif err := r.supersedeStaleBuildJob(ctx, llamaDeploy, buildId); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\t// Check if a build Job already exists for this buildId\n\tjobName := buildJobName(llamaDeploy.Name, buildId)\n\n\texistingJob := &batchv1.Job{}\n\terr := r.Get(ctx, client.ObjectKey{Name: jobName, Namespace: llamaDeploy.Namespace}, existingJob)\n\n\tif err != nil && !errors.IsNotFound(err) {\n\t\treturn \"\", nil, fmt.Errorf(\"failed to check build job: %w\", err)\n\t}\n\n\tif errors.IsNotFound(err) {\n\t\t// No existing job — create one\n\t\tlogger.Info(\"Creating build job\", \"buildId\", buildId, \"jobName\", jobName)\n\n\t\tjob := r.createBuildJob(llamaDeploy, buildId)\n\t\tif err := r.applyBuildJobTemplateOverlay(ctx, llamaDeploy, job); err != nil {\n\t\t\tlogger.Error(err, \"Failed to apply template overlay to build job\")\n\t\t\t// Continue without overlay — scheduling preferences are optional\n\t\t}\n\t\tif err := controllerutil.SetControllerReference(llamaDeploy, job, r.Scheme); err != nil {\n\t\t\treturn \"\", nil, fmt.Errorf(\"failed to set owner reference on build job: %w\", err)\n\t\t}\n\t\tif err := r.Create(ctx, job); err != nil {\n\t\t\tif errors.IsAlreadyExists(err) {\n\t\t\t\t// Race condition — another reconcile created it. Requeue to check status.\n\t\t\t\tresult := ctrl.Result{RequeueAfter: 5 * time.Second}\n\t\t\t\treturn \"\", &result, nil\n\t\t\t}\n\t\t\treturn \"\", nil, fmt.Errorf(\"failed to create build job: %w\", err)\n\t\t}\n\n\t\t// Update CRD status to Building\n\t\tllamaDeploy.Status.BuildId = buildId\n\t\tllamaDeploy.Status.BuildStatus = BuildStatusPending\n\t\tllamaDeploy.Status.Phase = PhaseBuilding\n\t\tllamaDeploy.Status.Message = fmt.Sprintf(\"Build job created: %s\", jobName)\n\t\tnow := metav1.Now()\n\t\tllamaDeploy.Status.LastUpdated = &now\n\t\tif err := r.Status().Update(ctx, llamaDeploy); err != nil {\n\t\t\tlogger.Error(err, \"Failed to update build status\")\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tif r.Recorder != nil {\n\t\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeNormal, PhaseBuilding, fmt.Sprintf(\"Build job created: %s\", jobName))\n\t\t}\n\n\t\tresult := ctrl.Result{RequeueAfter: 60 * time.Second}\n\t\treturn \"\", &result, nil\n\t}\n\n\t// Job exists — check its status\n\tif existingJob.Status.Succeeded > 0 {\n\t\tlogger.Info(\"Build job succeeded\", \"buildId\", buildId, \"jobName\", jobName)\n\n\t\t// Update CRD status\n\t\tllamaDeploy.Status.BuildId = buildId\n\t\tllamaDeploy.Status.BuildStatus = BuildStatusSucceeded\n\t\tllamaDeploy.Status.LastBuiltGeneration = llamaDeploy.Spec.BuildGeneration\n\t\tnow := metav1.Now()\n\t\tllamaDeploy.Status.LastUpdated = &now\n\t\tif err := r.Status().Update(ctx, llamaDeploy); err != nil {\n\t\t\tlogger.Error(err, \"Failed to update build status to Succeeded\")\n\t\t\treturn \"\", nil, err\n\t\t}\n\n\t\treturn buildId, nil, nil\n\t}\n\n\tif existingJob.Status.Failed > 0 {\n\t\t// If generation advanced (user wants a retry), delete the stale Job\n\t\t// and fall through to create a new one\n\t\tif llamaDeploy.Status.FailedRolloutGeneration != llamaDeploy.Generation {\n\t\t\tlogger.Info(\"Generation advanced past failed build, deleting stale job for retry\",\n\t\t\t\t\"buildId\", buildId, \"jobName\", jobName,\n\t\t\t\t\"failedGeneration\", llamaDeploy.Status.FailedRolloutGeneration,\n\t\t\t\t\"currentGeneration\", llamaDeploy.Generation)\n\t\t\tif err := r.Delete(ctx, existingJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) {\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"failed to delete stale build job: %w\", err)\n\t\t\t}\n\t\t\t// Record that we've consumed this generation's retry attempt.\n\t\t\t// If the new Job also fails, the \"same generation\" path below\n\t\t\t// will mark it as a genuine BuildFailed instead of looping.\n\t\t\tllamaDeploy.Status.FailedRolloutGeneration = llamaDeploy.Generation\n\t\t\tif err := r.Status().Update(ctx, llamaDeploy); err != nil {\n\t\t\t\tlogger.Error(err, \"Failed to update failedRolloutGeneration after retry\")\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\t\t\t// Requeue to let the deletion propagate, then create a new Job\n\t\t\tresult := ctrl.Result{RequeueAfter: 5 * time.Second}\n\t\t\treturn \"\", &result, nil\n\t\t}\n\n\t\t// Same generation — genuine failure, mark as BuildFailed\n\t\tlogger.Info(\"Build job failed\", \"buildId\", buildId, \"jobName\", jobName)\n\n\t\tllamaDeploy.Status.BuildId = buildId\n\t\tllamaDeploy.Status.BuildStatus = BuildStatusFailed\n\t\tllamaDeploy.Status.Phase = PhaseBuildFailed\n\t\tllamaDeploy.Status.Message = fmt.Sprintf(\"Build job failed: %s\", jobName)\n\t\tnow := metav1.Now()\n\t\tllamaDeploy.Status.LastUpdated = &now\n\t\tllamaDeploy.Status.FailedRolloutGeneration = llamaDeploy.Generation\n\t\tif err := r.Status().Update(ctx, llamaDeploy); err != nil {\n\t\t\tlogger.Error(err, \"Failed to update build status to Failed\")\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tif r.Recorder != nil {\n\t\t\tr.Recorder.Event(llamaDeploy, corev1.EventTypeWarning, PhaseBuildFailed, fmt.Sprintf(\"Build job failed: %s\", jobName))\n\t\t}\n\n\t\t// Don't requeue — wait for spec change (new generation)\n\t\tresult := ctrl.Result{}\n\t\treturn \"\", &result, nil\n\t}\n\n\t// Job is still running\n\tif llamaDeploy.Status.BuildStatus != BuildStatusRunning {\n\t\tllamaDeploy.Status.BuildStatus = BuildStatusRunning\n\t\tllamaDeploy.Status.Phase = PhaseBuilding\n\t\tllamaDeploy.Status.Message = fmt.Sprintf(\"Build in progress: %s\", jobName)\n\t\tnow := metav1.Now()\n\t\tllamaDeploy.Status.LastUpdated = &now\n\t\tif err := r.Status().Update(ctx, llamaDeploy); err != nil {\n\t\t\tlogger.Error(err, \"Failed to update build status to Running\")\n\t\t}\n\t}\n\n\tresult := ctrl.Result{RequeueAfter: 60 * time.Second}\n\treturn \"\", &result, nil\n}\n\n// createBuildJob creates a Job that runs the build process and uploads artifacts to S3.\nfunc (r *LlamaDeploymentReconciler) createBuildJob(llamaDeploy *llamadeployv1.LlamaDeployment, buildId string) *batchv1.Job {\n\t// Build environment variables from common helper, plus build-specific BUILD_ID\n\tenvVars := append(r.commonEnvVars(llamaDeploy), corev1.EnvVar{\n\t\tName:  \"LLAMA_DEPLOY_BUILD_ID\",\n\t\tValue: buildId,\n\t})\n\n\tenvFrom := r.commonEnvFrom(llamaDeploy)\n\n\tjobName := buildJobName(llamaDeploy.Name, buildId)\n\n\treturn &batchv1.Job{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      jobName,\n\t\t\tNamespace: llamaDeploy.Namespace,\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"app.kubernetes.io/managed-by\":    \"llama-deploy-operator\",\n\t\t\t\t\"deploy.llamaindex.ai/deployment\": llamaDeploy.Name,\n\t\t\t\t\"deploy.llamaindex.ai/build-id\":   buildId,\n\t\t\t},\n\t\t},\n\t\tSpec: batchv1.JobSpec{\n\t\t\tBackoffLimit:            ptr(int32(1)),\n\t\t\tTTLSecondsAfterFinished: ptr(int32(3600)),\n\t\t\tActiveDeadlineSeconds:   ptr(int64(1800)),\n\t\t\tTemplate: corev1.PodTemplateSpec{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\"app.kubernetes.io/managed-by\":    \"llama-deploy-operator\",\n\t\t\t\t\t\t\"deploy.llamaindex.ai/deployment\": llamaDeploy.Name,\n\t\t\t\t\t\t\"deploy.llamaindex.ai/build-id\":   buildId,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tSecurityContext: defaultPodSecurityContext(),\n\t\t\t\t\tRestartPolicy:   corev1.RestartPolicyNever,\n\t\t\t\t\tVolumes: []corev1.Volume{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"app-data\",\n\t\t\t\t\t\t\tVolumeSource: corev1.VolumeSource{\n\t\t\t\t\t\t\t\tEmptyDir: &corev1.EmptyDirVolumeSource{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:            ContainerNameBuild,\n\t\t\t\t\t\t\tImage:           fmt.Sprintf(\"%s:%s\", getDefaultImage(), getDefaultImageTag()),\n\t\t\t\t\t\t\tImagePullPolicy: getContainerImagePullPolicy(),\n\t\t\t\t\t\t\tCommand:         []string{\"python\", \"-m\", \"llama_deploy.appserver.build\"},\n\t\t\t\t\t\t\tEnv:             envVars,\n\t\t\t\t\t\t\tEnvFrom:         envFrom,\n\t\t\t\t\t\t\tVolumeMounts: []corev1.VolumeMount{\n\t\t\t\t\t\t\t\t{Name: \"app-data\", MountPath: \"/opt/app\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tResources:       corev1.ResourceRequirements{Requests: getDefaultResourceRequests(), Limits: getDefaultResourceLimits()},\n\t\t\t\t\t\t\tSecurityContext: defaultContainerSecurityContext(),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// applyBuildJobTemplateOverlay applies scheduling fields from a LlamaDeploymentTemplate to a build Job.\n// Pod-level scheduling fields are always applied (nodeSelector, tolerations, affinity, priorityClassName,\n// serviceAccountName). Container-level resource overrides are merged (not replaced) from a \"build\"\n// container if present in the template, otherwise from the \"app\" container as a fallback.\nfunc (r *LlamaDeploymentReconciler) applyBuildJobTemplateOverlay(ctx context.Context, ld *llamadeployv1.LlamaDeployment, job *batchv1.Job) error {\n\ttemplateName := ld.Spec.TemplateName\n\tif templateName == \"\" {\n\t\ttemplateName = \"default\"\n\t}\n\n\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: templateName, Namespace: ld.Namespace}, tmpl); err != nil {\n\t\tif errors.IsNotFound(err) {\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\n\toverlay := tmpl.Spec.PodSpec.Spec\n\n\t// Apply pod-level scheduling fields\n\tif overlay.NodeSelector != nil {\n\t\tjob.Spec.Template.Spec.NodeSelector = overlay.NodeSelector\n\t}\n\tif overlay.Tolerations != nil {\n\t\tjob.Spec.Template.Spec.Tolerations = overlay.Tolerations\n\t}\n\tif overlay.Affinity != nil {\n\t\tjob.Spec.Template.Spec.Affinity = overlay.Affinity\n\t}\n\tif overlay.PriorityClassName != \"\" {\n\t\tjob.Spec.Template.Spec.PriorityClassName = overlay.PriorityClassName\n\t}\n\tif overlay.ServiceAccountName != \"\" {\n\t\tjob.Spec.Template.Spec.ServiceAccountName = overlay.ServiceAccountName\n\t}\n\tif overlay.SecurityContext != nil {\n\t\tjob.Spec.Template.Spec.SecurityContext = overlay.SecurityContext\n\t}\n\n\t// Merge template metadata (annotations and labels) into the build job pod template\n\toverlayMeta := tmpl.Spec.PodSpec.ObjectMeta\n\tif len(overlayMeta.Annotations) > 0 {\n\t\tif job.Spec.Template.Annotations == nil {\n\t\t\tjob.Spec.Template.Annotations = make(map[string]string)\n\t\t}\n\t\tfor k, v := range overlayMeta.Annotations {\n\t\t\tjob.Spec.Template.Annotations[k] = v\n\t\t}\n\t}\n\tif len(overlayMeta.Labels) > 0 {\n\t\tif job.Spec.Template.Labels == nil {\n\t\t\tjob.Spec.Template.Labels = make(map[string]string)\n\t\t}\n\t\tfor k, v := range overlayMeta.Labels {\n\t\t\tjob.Spec.Template.Labels[k] = v\n\t\t}\n\t}\n\n\t// Build the final resource requirements by starting from the template and\n\t// backfilling defaults for any keys the template doesn't set. Look for a\n\t// dedicated \"build\" container first, falling back to \"app\".\n\ttemplateResources := findContainerResources(overlay.Containers, ContainerNameBuild)\n\tif templateResources == nil {\n\t\ttemplateResources = findContainerResources(overlay.Containers, ContainerNameApp)\n\t}\n\tif templateResources != nil {\n\t\tfor i := range job.Spec.Template.Spec.Containers {\n\t\t\tif job.Spec.Template.Spec.Containers[i].Name == ContainerNameBuild {\n\t\t\t\tjob.Spec.Template.Spec.Containers[i].Resources = mergeResourceRequirements(templateResources, &job.Spec.Template.Spec.Containers[i].Resources)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Propagate container-level SecurityContext from the template overlay\n\ttemplateSC := findContainerSecurityContext(overlay.Containers, ContainerNameBuild)\n\tif templateSC == nil {\n\t\ttemplateSC = findContainerSecurityContext(overlay.Containers, ContainerNameApp)\n\t}\n\tif templateSC != nil {\n\t\tfor i := range job.Spec.Template.Spec.Containers {\n\t\t\tif job.Spec.Template.Spec.Containers[i].Name == ContainerNameBuild {\n\t\t\t\tjob.Spec.Template.Spec.Containers[i].SecurityContext = templateSC\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// findContainerSecurityContext returns the SecurityContext for the named container,\n// or nil if the container is not found or has no security context set.\nfunc findContainerSecurityContext(containers []corev1.Container, name string) *corev1.SecurityContext {\n\tfor _, c := range containers {\n\t\tif c.Name == name {\n\t\t\tif c.SecurityContext != nil {\n\t\t\t\treturn c.SecurityContext\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn nil\n}\n\n// findContainerResources returns the ResourceRequirements for the named container,\n// or nil if the container is not found or has no resources set.\nfunc findContainerResources(containers []corev1.Container, name string) *corev1.ResourceRequirements {\n\tfor _, c := range containers {\n\t\tif c.Name == name {\n\t\t\tif c.Resources.Requests != nil || c.Resources.Limits != nil {\n\t\t\t\treturn &c.Resources\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn nil\n}\n\n// mergeResourceRequirements starts from the overlay resources and backfills any\n// keys from defaults that the overlay doesn't set.\nfunc mergeResourceRequirements(overlay, defaults *corev1.ResourceRequirements) corev1.ResourceRequirements {\n\tmerged := overlay.DeepCopy()\n\tif merged.Requests == nil {\n\t\tmerged.Requests = corev1.ResourceList{}\n\t}\n\tfor k, v := range defaults.Requests {\n\t\tif _, exists := merged.Requests[k]; !exists {\n\t\t\tmerged.Requests[k] = v\n\t\t}\n\t}\n\tif merged.Limits == nil {\n\t\tmerged.Limits = corev1.ResourceList{}\n\t}\n\tfor k, v := range defaults.Limits {\n\t\tif _, exists := merged.Limits[k]; !exists {\n\t\t\tmerged.Limits[k] = v\n\t\t}\n\t}\n\treturn *merged\n}\n\n// reconcileResources handles the creation of all Kubernetes resources for the LlamaDeployment.\n// buildId is the resolved build artifact ID (empty string if no build was needed).\nfunc (r *LlamaDeploymentReconciler) reconcileResources(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment, buildId string) error {\n\t// Apply ServiceAccount via SSA\n\tsa := &corev1.ServiceAccount{\n\t\tTypeMeta:                     metav1.TypeMeta{APIVersion: \"v1\", Kind: \"ServiceAccount\"},\n\t\tObjectMeta:                   metav1.ObjectMeta{Name: llamaDeploy.Name + \"-sa\", Namespace: llamaDeploy.Namespace},\n\t\tAutomountServiceAccountToken: ptr(false),\n\t}\n\tif err := controllerutil.SetControllerReference(llamaDeploy, sa, r.Scheme); err != nil {\n\t\treturn err\n\t}\n\tsaPatchOpts := []client.PatchOption{client.FieldOwner(\"llama-deploy-operator\")}\n\tif r.shouldForceOwnership(llamaDeploy) {\n\t\tsaPatchOpts = append(saPatchOpts, client.ForceOwnership)\n\t}\n\tif err := r.Patch(ctx, sa, client.Apply, saPatchOpts...); err != nil {\n\t\treturn err\n\t}\n\n\t// Verify secret exists if specified\n\tif llamaDeploy.Spec.SecretName != \"\" {\n\t\tif err := r.verifySecret(ctx, llamaDeploy); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// CreateOrUpdate ConfigMap for nginx configuration\n\tif err := r.reconcileNginxConfigMap(ctx, llamaDeploy); err != nil {\n\t\treturn err\n\t}\n\n\t// CreateOrUpdate Deployment\n\tif err := r.reconcileDeployment(ctx, llamaDeploy, buildId); err != nil {\n\t\treturn err\n\t}\n\n\t// Create Service\n\tif err := r.reconcileService(ctx, llamaDeploy); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// verifySecret ensures the Secret exists but does NOT create it\n// Secrets should be created by the API server or user before creating LlamaDeployment\nfunc (r *LlamaDeploymentReconciler) verifySecret(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) error {\n\tfound := &corev1.Secret{}\n\terr := r.Get(ctx, client.ObjectKey{Name: llamaDeploy.Spec.SecretName, Namespace: llamaDeploy.Namespace}, found)\n\tif err != nil && errors.IsNotFound(err) {\n\t\treturn fmt.Errorf(\"secret %s not found - secrets must be created separately before creating LlamaDeployment\", llamaDeploy.Spec.SecretName)\n\t}\n\treturn err\n}\n\n// reconcileDeployment creates or updates the Deployment for the LlamaDeployment.\n// buildId is the resolved build artifact ID passed explicitly rather than read from Status.\nfunc (r *LlamaDeploymentReconciler) reconcileDeployment(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment, buildId string) error {\n\tlogger := log.FromContext(ctx)\n\n\t// Unpause the Deployment if it was paused by a previous timeout remediation,\n\t// but ONLY when the spec generation has advanced past the failed one.\n\t// We require FailedRolloutGeneration > 0 to guard against a stale informer\n\t// cache: a racing reconcile triggered by the Deployment-pause event may read\n\t// the LlamaDeployment before the timeout handler's status update (which sets\n\t// FailedRolloutGeneration) has landed. In that case FailedRolloutGeneration\n\t// is still 0 and we must not unpause, otherwise the Kubernetes Deployment\n\t// controller scales the failing ReplicaSet back up.\n\texisting := &appsv1.Deployment{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}, existing); err == nil {\n\t\tif existing.Spec.Paused &&\n\t\t\tllamaDeploy.Status.FailedRolloutGeneration > 0 &&\n\t\t\tllamaDeploy.Generation > llamaDeploy.Status.FailedRolloutGeneration {\n\t\t\tpatch := client.MergeFrom(existing.DeepCopy())\n\t\t\texisting.Spec.Paused = false\n\t\t\tif patchErr := r.Patch(ctx, existing, patch); patchErr != nil {\n\t\t\t\tlogger.Error(patchErr, \"Failed to unpause deployment\")\n\t\t\t\treturn patchErr\n\t\t\t}\n\t\t\tlogger.Info(\"Unpaused Deployment for new rollout\")\n\t\t}\n\t}\n\n\tdesired := r.createDeploymentForLlama(llamaDeploy, buildId)\n\n\t// Apply LlamaDeploymentTemplate via strategic merge, template takes precedence\n\tif err := r.applyTemplateOverlay(ctx, llamaDeploy, desired); err != nil {\n\t\treturn err\n\t}\n\n\tdep := &appsv1.Deployment{\n\t\tTypeMeta: metav1.TypeMeta{APIVersion: \"apps/v1\", Kind: \"Deployment\"},\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      llamaDeploy.Name,\n\t\t\tNamespace: llamaDeploy.Namespace,\n\t\t},\n\t\tSpec: desired.Spec,\n\t}\n\tif err := controllerutil.SetControllerReference(llamaDeploy, dep, r.Scheme); err != nil {\n\t\treturn err\n\t}\n\tdepPatchOpts := []client.PatchOption{client.FieldOwner(\"llama-deploy-operator\")}\n\tif r.shouldForceOwnership(llamaDeploy) {\n\t\tdepPatchOpts = append(depPatchOpts, client.ForceOwnership)\n\t}\n\tif err := r.Patch(ctx, dep, client.Apply, depPatchOpts...); err != nil {\n\t\treturn err\n\t}\n\tlogger.V(1).Info(\"Reconciled Deployment\", \"name\", llamaDeploy.Name)\n\treturn nil\n}\n\n// reconcileNginxConfigMap creates or updates the ConfigMap containing nginx configuration\nfunc (r *LlamaDeploymentReconciler) reconcileNginxConfigMap(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) error {\n\tcm := &corev1.ConfigMap{\n\t\tTypeMeta:   metav1.TypeMeta{APIVersion: \"v1\", Kind: \"ConfigMap\"},\n\t\tObjectMeta: metav1.ObjectMeta{Name: llamaDeploy.Name + \"-nginx-config\", Namespace: llamaDeploy.Namespace},\n\t\tData:       map[string]string{\"nginx.conf\": r.generateNginxConfig(llamaDeploy)},\n\t}\n\tif err := controllerutil.SetControllerReference(llamaDeploy, cm, r.Scheme); err != nil {\n\t\treturn err\n\t}\n\tcmPatchOpts := []client.PatchOption{client.FieldOwner(\"llama-deploy-operator\")}\n\tif r.shouldForceOwnership(llamaDeploy) {\n\t\tcmPatchOpts = append(cmPatchOpts, client.ForceOwnership)\n\t}\n\treturn r.Patch(ctx, cm, client.Apply, cmPatchOpts...)\n}\n\n// reconcileService creates or updates the Service for the LlamaDeployment\nfunc (r *LlamaDeploymentReconciler) reconcileService(ctx context.Context, llamaDeploy *llamadeployv1.LlamaDeployment) error {\n\tsvc := &corev1.Service{\n\t\tTypeMeta: metav1.TypeMeta{APIVersion: \"v1\", Kind: \"Service\"},\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      llamaDeploy.Name,\n\t\t\tNamespace: llamaDeploy.Namespace,\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"app\":                          llamaDeploy.Name,\n\t\t\t\t\"app.kubernetes.io/managed-by\": \"llama-deploy-operator\",\n\t\t\t\t\"component\":                    \"appserver\",\n\t\t\t},\n\t\t},\n\t\tSpec: corev1.ServiceSpec{\n\t\t\tSelector: map[string]string{\"app\": llamaDeploy.Name},\n\t\t\tType:     corev1.ServiceTypeClusterIP,\n\t\t\tPorts: []corev1.ServicePort{\n\t\t\t\t{\n\t\t\t\t\tProtocol:   corev1.ProtocolTCP,\n\t\t\t\t\tName:       \"http\",\n\t\t\t\t\tPort:       80,\n\t\t\t\t\tTargetPort: intstr.FromInt(8081),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tif err := controllerutil.SetControllerReference(llamaDeploy, svc, r.Scheme); err != nil {\n\t\treturn err\n\t}\n\tsvcPatchOpts := []client.PatchOption{client.FieldOwner(\"llama-deploy-operator\")}\n\tif r.shouldForceOwnership(llamaDeploy) {\n\t\tsvcPatchOpts = append(svcPatchOpts, client.ForceOwnership)\n\t}\n\treturn r.Patch(ctx, svc, client.Apply, svcPatchOpts...)\n}\n\n// commonEnvVars returns the environment variables shared by deployment pods and build jobs.\nfunc (r *LlamaDeploymentReconciler) commonEnvVars(llamaDeploy *llamadeployv1.LlamaDeployment) []corev1.EnvVar {\n\tdeploymentFilePath := llamaDeploy.Spec.DeploymentFilePath\n\tif deploymentFilePath == \"\" {\n\t\tdeploymentFilePath = \".\"\n\t}\n\n\t// Build API base URL for git proxy\n\tbuildAPIHost := os.Getenv(\"LLAMA_DEPLOY_BUILD_API_HOST\")\n\tif buildAPIHost == \"\" {\n\t\tbuildAPIHost = \"llama-agents-build.llama-agents.svc.cluster.local:8001\"\n\t}\n\n\t// Build authenticated repo URL with embedded token\n\trepoURL := fmt.Sprintf(\"http://%s/deployments/%s\", buildAPIHost, llamaDeploy.Name)\n\n\tenvVars := []corev1.EnvVar{\n\t\t// used in llama_deploy/docker/bootstrap.sh to determine the repo to clone via build API\n\t\t{Name: \"LLAMA_DEPLOY_REPO_URL\", Value: repoURL},\n\t\t// used in llama_deploy/docker/bootstrap.sh to determine the git ref to clone via build API\n\t\t{Name: \"LLAMA_DEPLOY_GIT_REF\", Value: llamaDeploy.Spec.GitRef},\n\t\t// used in llama_deploy/docker/bootstrap.sh to determine the git sha to clone via build API\n\t\t{Name: \"LLAMA_DEPLOY_GIT_SHA\", Value: llamaDeploy.Spec.GitSha},\n\t\t// used in llama_deploy/docker/bootstrap.sh to determine the config to autodeploy\n\t\t{Name: \"LLAMA_DEPLOY_DEPLOYMENT_FILE_PATH\", Value: deploymentFilePath},\n\t\t// used in llama_cloud_services/llama_cloud_services/beta/agent_data/client.py to infer an agent environment\n\t\t{Name: \"LLAMA_DEPLOY_DEPLOYMENT_NAME\", Value: llamaDeploy.Name},\n\t\t// Auth token for accessing build control plane API\n\t\t{Name: \"LLAMA_DEPLOY_AUTH_TOKEN\", Value: llamaDeploy.Status.AuthToken},\n\t\t// Build API service address for git proxy\n\t\t{Name: \"LLAMA_DEPLOY_BUILD_API_HOST\", Value: buildAPIHost},\n\t\t// the project ID\n\t\t{Name: \"LLAMA_DEPLOY_PROJECT_ID\", Value: llamaDeploy.Spec.ProjectId},\n\t\t// used in llama_deploy/apiserver/deployment.py to determine if the deployment is running locally or in a deployed environment\n\t\t{Name: \"LLAMA_DEPLOY_IS_DEPLOYED\", Value: \"true\"},\n\t\t// configure structured JSON logging for the appserver\n\t\t{Name: \"LOG_FORMAT\", Value: \"json\"},\n\t\t// Trust /opt/app regardless of ownership so git operations work when\n\t\t// the container uid differs from the EmptyDir volume owner.\n\t\t{Name: \"GIT_CONFIG_COUNT\", Value: \"1\"},\n\t\t{Name: \"GIT_CONFIG_KEY_0\", Value: \"safe.directory\"},\n\t\t{Name: \"GIT_CONFIG_VALUE_0\", Value: \"/opt/app\"},\n\t}\n\n\t// If the deployment is pinned to a specific appserver version via imageTag,\n\t// pass that version to the build Job / init container so it installs the\n\t// correct appserver instead of the one bundled in the image.\n\tif llamaDeploy.Spec.ImageTag != \"\" {\n\t\tenvVars = append(envVars, corev1.EnvVar{\n\t\t\tName:  \"LLAMA_DEPLOY_APPSERVER_VERSION\",\n\t\t\tValue: getContainerImageTag(llamaDeploy),\n\t\t})\n\t}\n\n\treturn envVars\n}\n\n// commonEnvFrom returns the envFrom sources shared by deployment pods and build jobs.\nfunc (r *LlamaDeploymentReconciler) commonEnvFrom(llamaDeploy *llamadeployv1.LlamaDeployment) []corev1.EnvFromSource {\n\tif llamaDeploy.Spec.SecretName != \"\" {\n\t\treturn []corev1.EnvFromSource{\n\t\t\t{\n\t\t\t\tSecretRef: &corev1.SecretEnvSource{\n\t\t\t\t\tLocalObjectReference: corev1.LocalObjectReference{\n\t\t\t\t\t\tName: llamaDeploy.Spec.SecretName,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\treturn nil\n}\n\n// createDeploymentForLlama creates a Deployment object for the LlamaDeployment.\n// buildId is passed explicitly rather than read from Status.BuildId.\nfunc (r *LlamaDeploymentReconciler) createDeploymentForLlama(llamaDeploy *llamadeployv1.LlamaDeployment, buildId string) *appsv1.Deployment {\n\tdeploymentFilePath := llamaDeploy.Spec.DeploymentFilePath\n\tif deploymentFilePath == \"\" {\n\t\tdeploymentFilePath = \".\"\n\t}\n\n\t// Compute container working directory based on deployment file path\n\tworkingDir := \"/opt/app\"\n\tif deploymentFilePath != \".\" {\n\t\tnormalized := strings.TrimPrefix(deploymentFilePath, \"./\")\n\t\tnormalized = strings.TrimPrefix(normalized, \"/\")\n\t\tresolved := normalized\n\t\tif looksLikeFilePath(normalized) {\n\t\t\tresolved = path.Dir(normalized)\n\t\t\tif resolved == \".\" || resolved == \"/\" {\n\t\t\t\tresolved = \"\"\n\t\t\t}\n\t\t}\n\t\tif resolved != \"\" {\n\t\t\tworkingDir = \"/opt/app/\" + resolved\n\t\t}\n\t}\n\n\t// Build environment variables from common helper\n\tenvVars := r.commonEnvVars(llamaDeploy)\n\n\t// If a build artifact exists, tell the init container to download it instead of building\n\tif buildId != \"\" {\n\t\tenvVars = append(envVars, corev1.EnvVar{\n\t\t\tName:  \"LLAMA_DEPLOY_BUILD_ID\",\n\t\t\tValue: buildId,\n\t\t})\n\t}\n\n\tenvFrom := r.commonEnvFrom(llamaDeploy)\n\n\t// Prepare pod template annotations\n\tpodAnnotations := map[string]string{}\n\n\t// Add git source annotation to trigger redeployment when repo or ref changes\n\tgitSource := llamaDeploy.Spec.RepoUrl\n\tif llamaDeploy.Spec.GitSha != \"\" || llamaDeploy.Spec.GitRef != \"\" {\n\t\tgitSource = fmt.Sprintf(\"%s@%s\", llamaDeploy.Spec.RepoUrl, llamaDeploy.Spec.GitSha)\n\t}\n\tpodAnnotations[\"deploy.llamaindex.ai/git-source\"] = gitSource\n\n\t// Add secret hash annotation if present on the LlamaDeployment\n\tif llamaDeploy.Annotations != nil {\n\t\tif secretHash, exists := llamaDeploy.Annotations[\"deploy.llamaindex.ai/secret-hash\"]; exists {\n\t\t\tpodAnnotations[\"deploy.llamaindex.ai/secret-hash\"] = secretHash\n\t\t}\n\t}\n\n\treplicas := int32(1)\n\tif llamaDeploy.Spec.Suspended ||\n\t\t(llamaDeploy.Status.FailedRolloutGeneration == llamaDeploy.Generation &&\n\t\t\tllamaDeploy.Status.Phase == PhaseFailed) {\n\t\treplicas = int32(0)\n\t}\n\treturn &appsv1.Deployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      llamaDeploy.Name,\n\t\t\tNamespace: llamaDeploy.Namespace,\n\t\t},\n\t\tSpec: appsv1.DeploymentSpec{\n\t\t\tReplicas:                &replicas,\n\t\t\tProgressDeadlineSeconds: getRolloutTimeoutSeconds(),\n\t\t\tRevisionHistoryLimit:    ptr(DeploymentRevisionHistoryLimit),\n\t\t\tSelector: &metav1.LabelSelector{\n\t\t\t\tMatchLabels: map[string]string{\"app\": llamaDeploy.Name},\n\t\t\t},\n\t\t\tTemplate: corev1.PodTemplateSpec{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\"app\":                          llamaDeploy.Name,\n\t\t\t\t\t\t\"app.kubernetes.io/managed-by\": \"llama-deploy-operator\",\n\t\t\t\t\t},\n\t\t\t\t\tAnnotations: podAnnotations,\n\t\t\t\t},\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tSecurityContext:              defaultPodSecurityContext(),\n\t\t\t\t\tServiceAccountName:           llamaDeploy.Name + \"-sa\",\n\t\t\t\t\tAutomountServiceAccountToken: ptr(false),\n\t\t\t\t\tVolumes: []corev1.Volume{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"app-data\",\n\t\t\t\t\t\t\tVolumeSource: corev1.VolumeSource{\n\t\t\t\t\t\t\t\tEmptyDir: &corev1.EmptyDirVolumeSource{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"nginx-config\",\n\t\t\t\t\t\t\tVolumeSource: corev1.VolumeSource{\n\t\t\t\t\t\t\t\tConfigMap: &corev1.ConfigMapVolumeSource{\n\t\t\t\t\t\t\t\t\tLocalObjectReference: corev1.LocalObjectReference{\n\t\t\t\t\t\t\t\t\t\tName: llamaDeploy.Name + \"-nginx-config\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tInitContainers: []corev1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:            \"bootstrap\",\n\t\t\t\t\t\t\tImage:           fmt.Sprintf(\"%s:%s\", getDefaultImage(), getDefaultImageTag()),\n\t\t\t\t\t\t\tImagePullPolicy: getContainerImagePullPolicy(),\n\t\t\t\t\t\t\tEnv:             envVars,\n\t\t\t\t\t\t\tEnvFrom:         envFrom,\n\t\t\t\t\t\t\tCommand:         []string{\"python\", \"-m\", \"llama_deploy.appserver.bootstrap\"},\n\t\t\t\t\t\t\tVolumeMounts: []corev1.VolumeMount{\n\t\t\t\t\t\t\t\t{Name: \"app-data\", MountPath: \"/opt/app\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSecurityContext: defaultContainerSecurityContext(),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:            \"file-server\",\n\t\t\t\t\t\t\tImage:           fmt.Sprintf(\"%s:%s\", getNginxImage(), getNginxImageTag()),\n\t\t\t\t\t\t\tImagePullPolicy: getNginxImagePullPolicy(),\n\t\t\t\t\t\t\tPorts:           []corev1.ContainerPort{{ContainerPort: 8081, Protocol: corev1.ProtocolTCP}},\n\t\t\t\t\t\t\tVolumeMounts: []corev1.VolumeMount{\n\t\t\t\t\t\t\t\t{Name: \"app-data\", MountPath: \"/opt/app\"},\n\t\t\t\t\t\t\t\t{Name: \"nginx-config\", MountPath: \"/etc/nginx/nginx.conf\", SubPath: \"nginx.conf\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCommand: []string{\"nginx\", \"-g\", \"daemon off;\"},\n\t\t\t\t\t\t\tSecurityContext: &corev1.SecurityContext{\n\t\t\t\t\t\t\t\tRunAsNonRoot:             ptr(true),\n\t\t\t\t\t\t\t\tRunAsUser:                ptr(NginxUID),\n\t\t\t\t\t\t\t\tRunAsGroup:               ptr(NginxGID),\n\t\t\t\t\t\t\t\tAllowPrivilegeEscalation: ptr(false),\n\t\t\t\t\t\t\t\tCapabilities:             &corev1.Capabilities{Drop: []corev1.Capability{\"ALL\"}},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:            ContainerNameApp,\n\t\t\t\t\t\t\tImage:           fmt.Sprintf(\"%s:%s\", getContainerImage(llamaDeploy), getContainerImageTag(llamaDeploy)),\n\t\t\t\t\t\t\tImagePullPolicy: getContainerImagePullPolicy(),\n\t\t\t\t\t\t\tWorkingDir:      workingDir,\n\t\t\t\t\t\t\tCommand:         []string{\"uv\", \"run\", \"--no-sync\", \"python\", \"-m\", \"llama_deploy.appserver.app\"},\n\t\t\t\t\t\t\tEnv:             envVars,\n\t\t\t\t\t\t\tEnvFrom:         envFrom,\n\t\t\t\t\t\t\tVolumeMounts:    []corev1.VolumeMount{{Name: \"app-data\", MountPath: \"/opt/app\"}},\n\t\t\t\t\t\t\tPorts:           []corev1.ContainerPort{{ContainerPort: 8080, Protocol: corev1.ProtocolTCP}},\n\t\t\t\t\t\t\tStartupProbe:    &corev1.Probe{ProbeHandler: corev1.ProbeHandler{HTTPGet: &corev1.HTTPGetAction{Path: \"/health\", Port: intstr.FromInt(8080)}}, PeriodSeconds: 5, FailureThreshold: 24},\n\t\t\t\t\t\t\tLivenessProbe:   &corev1.Probe{ProbeHandler: corev1.ProbeHandler{HTTPGet: &corev1.HTTPGetAction{Path: \"/health\", Port: intstr.FromInt(8080)}}, PeriodSeconds: 5, FailureThreshold: 12},\n\t\t\t\t\t\t\tResources:       corev1.ResourceRequirements{Requests: getDefaultResourceRequests(), Limits: getDefaultResourceLimits()},\n\t\t\t\t\t\t\tSecurityContext: hardenedSecurityContext(),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// generateNginxConfig builds the nginx.conf content for the deployment\nfunc (r *LlamaDeploymentReconciler) generateNginxConfig(llamaDeploy *llamadeployv1.LlamaDeployment) string {\n\tbasePath := fmt.Sprintf(\"/deployments/%s/ui\", llamaDeploy.GetObjectMeta().GetName())\n\tassetsPath := llamaDeploy.Spec.StaticAssetsPath\n\tvar staticLocation string\n\tif assetsPath != \"\" {\n\t\t// Serve static files from /opt/app/<assetsPath>, fallback to python for misses\n\t\tstaticLocation = fmt.Sprintf(\"location %s { alias /opt/app/%s/; try_files $uri $uri/ /index.html @python_upstream; }\", basePath, assetsPath)\n\t} else {\n\t\t// If not provided, proxy UI base to python\n\t\tstaticLocation = fmt.Sprintf(\"location %s { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }\", basePath)\n\t}\n\n\t// Everything else proxies to python on 8081; also define named upstream for try_files fallback\n\tproxyLocation := \"location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }\\nlocation @python_upstream { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }\"\n\n\treturn fmt.Sprintf(`pid /tmp/nginx.pid;\nworker_processes  1;\nevents { worker_connections  1024; }\nhttp {\n  client_body_temp_path /tmp/client_temp;\n  proxy_temp_path       /tmp/proxy_temp;\n  fastcgi_temp_path     /tmp/fastcgi_temp;\n  uwsgi_temp_path       /tmp/uwsgi_temp;\n  scgi_temp_path        /tmp/scgi_temp;\n  include       mime.types;\n  default_type  application/octet-stream;\n  sendfile        on;\n  keepalive_timeout  65;\n  access_log    off;\n  server {\n    # Ensure redirects are relative (do not include scheme/host)\n    absolute_redirect off;\n    listen 8081;\n    %s\n    %s\n  }\n}`, staticLocation, proxyLocation)\n}\n\n// shouldForceOwnership returns true when the controller should force SSA field ownership\n// during migration windows, i.e., when the controller's static schema version is\n// greater than the resource's current status schema version.\nfunc (r *LlamaDeploymentReconciler) shouldForceOwnership(ld *llamadeployv1.LlamaDeployment) bool {\n\t// Force when CR spec changed since last successful reconcile\n\tif ld.Status.LastReconciledGeneration != ld.Generation {\n\t\treturn true\n\t}\n\t// Explicit override via annotation\n\tif ld.Annotations != nil {\n\t\tif ld.Annotations[\"deploy.llamaindex.ai/force-ownership\"] == \"true\" {\n\t\t\treturn true\n\t\t}\n\t}\n\t// Treat empty status schema version as -1 (always migrate up)\n\tparse := func(s string) (int, bool) {\n\t\tif s == \"\" {\n\t\t\treturn -1, true\n\t\t}\n\t\tn := 0\n\t\tfor i := 0; i < len(s); i++ {\n\t\t\tc := s[i]\n\t\t\tif c < '0' || c > '9' {\n\t\t\t\treturn 0, false\n\t\t\t}\n\t\t\tn = n*10 + int(c-'0')\n\t\t}\n\t\treturn n, true\n\t}\n\n\tcur, okCur := parse(CurrentSchemaVersion)\n\tprev, okPrev := parse(ld.Status.SchemaVersion)\n\tif okCur && okPrev {\n\t\treturn cur > prev\n\t}\n\t// Fallback to lexicographic comparison if parsing fails\n\treturn CurrentSchemaVersion > ld.Status.SchemaVersion\n}\n\n// applyTemplateOverlay merges a referenced LlamaDeploymentTemplate into the desired Deployment's PodTemplate.\n// The template's values take precedence over operator defaults.\nfunc (r *LlamaDeploymentReconciler) applyTemplateOverlay(ctx context.Context, ld *llamadeployv1.LlamaDeployment, dep *appsv1.Deployment) error {\n\t// Determine template name: spec.templateName or \"default\"\n\ttemplateName := ld.Spec.TemplateName\n\tif templateName == \"\" {\n\t\ttemplateName = \"default\"\n\t}\n\n\t// Try to fetch template in the same namespace\n\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{}\n\tif err := r.Get(ctx, client.ObjectKey{Name: templateName, Namespace: ld.Namespace}, tmpl); err != nil {\n\t\tif errors.IsNotFound(err) {\n\t\t\treturn nil // no template; nothing to merge\n\t\t}\n\t\treturn err\n\t}\n\n\t// Strategic merge allows specifying containers by name to override specific fields.\n\t// We don't need to validate this here - the merge will fail naturally if something is wrong.\n\n\t// Use Kubernetes strategic merge patch semantics to merge overlay into the desired PodTemplateSpec.\n\t// This respects list merge keys (e.g., containers by name) and map merges (labels/annotations).\n\tbase := dep.Spec.Template\n\tpatch := tmpl.Spec.PodSpec\n\n\tbaseJSON, err := json.Marshal(base)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"marshal base template: %w\", err)\n\t}\n\tpatchJSON, err := json.Marshal(patch)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"marshal overlay template: %w\", err)\n\t}\n\n\t// Clean up the patch to remove empty/nil fields that would override base values\n\tvar patchMap map[string]interface{}\n\tif err := json.Unmarshal(patchJSON, &patchMap); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal patch for cleanup: %w\", err)\n\t}\n\n\t// Remove metadata.creationTimestamp which can appear in serialized objects\n\tif metadata, ok := patchMap[\"metadata\"].(map[string]interface{}); ok {\n\t\tdelete(metadata, \"creationTimestamp\")\n\t\tif len(metadata) == 0 {\n\t\t\tdelete(patchMap, \"metadata\")\n\t\t}\n\t}\n\n\t// Clean empty arrays and nil values from spec to avoid clearing base values\n\tif spec, ok := patchMap[\"spec\"].(map[string]interface{}); ok {\n\t\t// Remove nil or empty container-related arrays\n\t\tfor _, field := range []string{\"containers\", \"initContainers\", \"volumes\", \"imagePullSecrets\", \"hostAliases\"} {\n\t\t\tif val := spec[field]; val == nil {\n\t\t\t\tdelete(spec, field)\n\t\t\t} else if arr, ok := val.([]interface{}); ok && len(arr) == 0 {\n\t\t\t\tdelete(spec, field)\n\t\t\t}\n\t\t}\n\t\t// Remove nil or empty maps\n\t\tfor _, field := range []string{\"nodeSelector\", \"securityContext\", \"affinity\"} {\n\t\t\tif val := spec[field]; val == nil {\n\t\t\t\tdelete(spec, field)\n\t\t\t} else if m, ok := val.(map[string]interface{}); ok && len(m) == 0 {\n\t\t\t\tdelete(spec, field)\n\t\t\t}\n\t\t}\n\n\t\t// Strip \"build\" containers from the runtime overlay. The \"build\" container\n\t\t// is only meaningful for build jobs (handled by applyBuildJobTemplateOverlay).\n\t\t// Strategic merge would add it as a new container to the runtime Deployment\n\t\t// with no image or command, producing an invalid pod spec.\n\t\tif containers, ok := spec[\"containers\"].([]interface{}); ok {\n\t\t\tfiltered := make([]interface{}, 0, len(containers))\n\t\t\tfor _, c := range containers {\n\t\t\t\tif cm, ok := c.(map[string]interface{}); ok {\n\t\t\t\t\tif cm[\"name\"] == ContainerNameBuild {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfiltered = append(filtered, c)\n\t\t\t}\n\t\t\tif len(filtered) == 0 {\n\t\t\t\tdelete(spec, \"containers\")\n\t\t\t} else {\n\t\t\t\tspec[\"containers\"] = filtered\n\t\t\t}\n\t\t}\n\t}\n\n\tpatchJSON, err = json.Marshal(patchMap)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"remarshal cleaned patch: %w\", err)\n\t}\n\n\tmergedJSON, err := strategicpatch.StrategicMergePatch(baseJSON, patchJSON, corev1.PodTemplateSpec{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"strategic merge template: %w\", err)\n\t}\n\tvar merged corev1.PodTemplateSpec\n\tif err := json.Unmarshal(mergedJSON, &merged); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal merged template: %w\", err)\n\t}\n\n\tdep.Spec.Template = merged\n\treturn nil\n}\n"
  },
  {
    "path": "operator/internal/controller/resources_build_unit_test.go",
    "content": "//go:build !integration\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\tctrl \"sigs.k8s.io/controller-runtime\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client/fake\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\nfunc TestCreateBuildJob_HasDefaultResources(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t},\n\t}\n\n\tjob := r.createBuildJob(ld, \"abc123\")\n\n\tcontainers := job.Spec.Template.Spec.Containers\n\tif len(containers) != 1 {\n\t\tt.Fatalf(\"expected 1 container, got %d\", len(containers))\n\t}\n\n\tres := containers[0].Resources\n\n\t// Verify requests are set\n\tcpuReq := res.Requests[corev1.ResourceCPU]\n\tif cpuReq.Cmp(resource.MustParse(\"750m\")) != 0 {\n\t\tt.Errorf(\"expected CPU request 750m, got %s\", cpuReq.String())\n\t}\n\tmemReq := res.Requests[corev1.ResourceMemory]\n\tif memReq.Cmp(resource.MustParse(\"2Gi\")) != 0 {\n\t\tt.Errorf(\"expected memory request 2Gi, got %s\", memReq.String())\n\t}\n\n\t// Verify limits are set\n\tmemLimit := res.Limits[corev1.ResourceMemory]\n\tif memLimit.Cmp(resource.MustParse(\"4096Mi\")) != 0 {\n\t\tt.Errorf(\"expected memory limit 4096Mi, got %s\", memLimit.String())\n\t}\n}\n\nfunc TestApplyBuildJobTemplateOverlay_MergesAppResources(t *testing.T) {\n\tscheme := newTestScheme()\n\n\t// Template with only ephemeral-storage on the \"app\" container (no \"build\" container).\n\t// This should be merged into the build job's defaults, not replace them.\n\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"default\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"app\",\n\t\t\t\t\t\t\tResources: corev1.ResourceRequirements{\n\t\t\t\t\t\t\t\tRequests: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\tcorev1.ResourceEphemeralStorage: resource.MustParse(\"1500Mi\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tLimits: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\tcorev1.ResourceEphemeralStorage: resource.MustParse(\"3Gi\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(tmpl).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{TemplateName: \"default\"},\n\t}\n\n\tjob := r.createBuildJob(ld, \"abc123\")\n\n\tctx := context.Background()\n\tif err := r.applyBuildJobTemplateOverlay(ctx, ld, job); err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tres := job.Spec.Template.Spec.Containers[0].Resources\n\n\t// Default CPU/memory requests must still be present\n\tcpuReq := res.Requests[corev1.ResourceCPU]\n\tif cpuReq.Cmp(resource.MustParse(\"750m\")) != 0 {\n\t\tt.Errorf(\"expected CPU request 750m, got %s\", cpuReq.String())\n\t}\n\tmemReq := res.Requests[corev1.ResourceMemory]\n\tif memReq.Cmp(resource.MustParse(\"2Gi\")) != 0 {\n\t\tt.Errorf(\"expected memory request 2Gi, got %s\", memReq.String())\n\t}\n\n\t// ephemeral-storage from template must be merged in\n\tephReq := res.Requests[corev1.ResourceEphemeralStorage]\n\tif ephReq.Cmp(resource.MustParse(\"1500Mi\")) != 0 {\n\t\tt.Errorf(\"expected ephemeral-storage request 1500Mi, got %s\", ephReq.String())\n\t}\n\n\t// Default memory limit must still be present\n\tmemLimit := res.Limits[corev1.ResourceMemory]\n\tif memLimit.Cmp(resource.MustParse(\"4096Mi\")) != 0 {\n\t\tt.Errorf(\"expected memory limit 4096Mi, got %s\", memLimit.String())\n\t}\n\n\t// ephemeral-storage limit from template must be merged in\n\tephLimit := res.Limits[corev1.ResourceEphemeralStorage]\n\tif ephLimit.Cmp(resource.MustParse(\"3Gi\")) != 0 {\n\t\tt.Errorf(\"expected ephemeral-storage limit 3Gi, got %s\", ephLimit.String())\n\t}\n}\n\nfunc TestApplyBuildJobTemplateOverlay_TemplateOverridesDefaults(t *testing.T) {\n\tscheme := newTestScheme()\n\n\t// Template with a \"build\" container that overrides CPU request and memory limit.\n\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"default\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"build\",\n\t\t\t\t\t\t\tResources: corev1.ResourceRequirements{\n\t\t\t\t\t\t\t\tRequests: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\tcorev1.ResourceCPU: resource.MustParse(\"2\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tLimits: corev1.ResourceList{\n\t\t\t\t\t\t\t\t\tcorev1.ResourceMemory: resource.MustParse(\"8Gi\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(tmpl).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{TemplateName: \"default\"},\n\t}\n\n\tjob := r.createBuildJob(ld, \"abc123\")\n\n\tctx := context.Background()\n\tif err := r.applyBuildJobTemplateOverlay(ctx, ld, job); err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tres := job.Spec.Template.Spec.Containers[0].Resources\n\n\t// Template CPU request should override the default 750m\n\tcpuReq := res.Requests[corev1.ResourceCPU]\n\tif cpuReq.Cmp(resource.MustParse(\"2\")) != 0 {\n\t\tt.Errorf(\"expected CPU request 2, got %s\", cpuReq.String())\n\t}\n\n\t// Default memory request should be backfilled\n\tmemReq := res.Requests[corev1.ResourceMemory]\n\tif memReq.Cmp(resource.MustParse(\"2Gi\")) != 0 {\n\t\tt.Errorf(\"expected memory request 2Gi (default), got %s\", memReq.String())\n\t}\n\n\t// Template memory limit should override the default 4096Mi\n\tmemLimit := res.Limits[corev1.ResourceMemory]\n\tif memLimit.Cmp(resource.MustParse(\"8Gi\")) != 0 {\n\t\tt.Errorf(\"expected memory limit 8Gi, got %s\", memLimit.String())\n\t}\n}\n\n// newTestScheme returns a scheme with all types needed for reconcileBuild tests.\nfunc newTestScheme() *runtime.Scheme {\n\tscheme := runtime.NewScheme()\n\t_ = llamadeployv1.AddToScheme(scheme)\n\t_ = batchv1.AddToScheme(scheme)\n\t_ = corev1.AddToScheme(scheme)\n\treturn scheme\n}\n\n// buildSupersedeFixture describes a \"stale build vs new spec\" scenario. If\n// staleJobStatus is nil the stale Job is omitted from the fake client (models\n// the case where the Job was already TTL-reaped).\ntype buildSupersedeFixture struct {\n\tdeploymentName string\n\tstaleBuildId   string\n\tnewGitSha      string\n\tstaleJobStatus *batchv1.JobStatus\n}\n\n// newBuildSupersedeFixture wires up an LlamaDeployment whose Status points at\n// staleBuildId/Running and whose Spec forces a new buildId via newGitSha,\n// optionally with a stale Job already in the cluster.\nfunc newBuildSupersedeFixture(\n\tt *testing.T,\n\tf buildSupersedeFixture,\n) (*LlamaDeploymentReconciler, *llamadeployv1.LlamaDeployment, client.Client) {\n\tt.Helper()\n\tscheme := newTestScheme()\n\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:       f.deploymentName,\n\t\t\tNamespace:  \"default\",\n\t\t\tGeneration: 2,\n\t\t},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t\tGitSha:    f.newGitSha,\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tBuildId:     f.staleBuildId,\n\t\t\tBuildStatus: BuildStatusRunning,\n\t\t\tPhase:       PhaseBuilding,\n\t\t},\n\t}\n\n\tobjs := []client.Object{llamaDeploy}\n\tif f.staleJobStatus != nil {\n\t\tobjs = append(objs, &batchv1.Job{\n\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\tName:      buildJobName(f.deploymentName, f.staleBuildId),\n\t\t\t\tNamespace: \"default\",\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"deploy.llamaindex.ai/deployment\": f.deploymentName,\n\t\t\t\t\t\"deploy.llamaindex.ai/build-id\":   f.staleBuildId,\n\t\t\t\t},\n\t\t\t},\n\t\t\tStatus: *f.staleJobStatus,\n\t\t})\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(objs...).\n\t\tWithStatusSubresource(llamaDeploy).\n\t\tBuild()\n\n\treturn &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}, llamaDeploy, fakeClient\n}\n\n// ---------------------------------------------------------------------------\n// computeBuildId tests\n// ---------------------------------------------------------------------------\n\nfunc TestComputeBuildId_Deterministic(t *testing.T) {\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\"},\n\t}\n\tid1 := computeBuildId(ld)\n\tid2 := computeBuildId(ld)\n\tif id1 != id2 {\n\t\tt.Errorf(\"expected deterministic result, got %q and %q\", id1, id2)\n\t}\n\tif len(id1) != 16 {\n\t\tt.Errorf(\"expected 16-char hash, got %d chars: %q\", len(id1), id1)\n\t}\n}\n\nfunc TestComputeBuildId_DifferentInputs(t *testing.T) {\n\tld1 := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"app-a\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"sha1\"},\n\t}\n\tld2 := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"app-b\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"sha1\"},\n\t}\n\tif computeBuildId(ld1) == computeBuildId(ld2) {\n\t\tt.Error(\"expected different build IDs for different deployment names\")\n\t}\n}\n\nfunc TestComputeBuildId_IncludesBuildGeneration(t *testing.T) {\n\tld1 := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\", BuildGeneration: 0},\n\t}\n\tld2 := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\", BuildGeneration: 1},\n\t}\n\tid1 := computeBuildId(ld1)\n\tid2 := computeBuildId(ld2)\n\tif id1 == id2 {\n\t\tt.Error(\"expected different build IDs when buildGeneration differs\")\n\t}\n}\n\nfunc TestComputeBuildId_DoesNotIncludeImageTag(t *testing.T) {\n\tld1 := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\", ImageTag: \"v1\"},\n\t}\n\tld2 := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\", ImageTag: \"v2\"},\n\t}\n\tif computeBuildId(ld1) != computeBuildId(ld2) {\n\t\tt.Error(\"expected same build ID regardless of imageTag\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// createBuildJob shape tests\n// ---------------------------------------------------------------------------\n\nfunc TestCreateBuildJob_JobNameTruncation(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tlongName := strings.Repeat(\"a\", 60)\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: longName, Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\"},\n\t}\n\tjob := r.createBuildJob(ld, \"abcdef1234567890\")\n\tif len(job.Name) > 63 {\n\t\tt.Errorf(\"job name exceeds 63 chars: %q (%d chars)\", job.Name, len(job.Name))\n\t}\n}\n\nfunc TestCreateBuildJob_Labels(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\"},\n\t}\n\tjob := r.createBuildJob(ld, \"build123\")\n\n\tif job.Labels[\"deploy.llamaindex.ai/deployment\"] != \"my-app\" {\n\t\tt.Errorf(\"expected deployment label my-app, got %q\", job.Labels[\"deploy.llamaindex.ai/deployment\"])\n\t}\n\tif job.Labels[\"deploy.llamaindex.ai/build-id\"] != \"build123\" {\n\t\tt.Errorf(\"expected build-id label build123, got %q\", job.Labels[\"deploy.llamaindex.ai/build-id\"])\n\t}\n}\n\nconst envBuildID = \"LLAMA_DEPLOY_BUILD_ID\"\n\nfunc TestCreateBuildJob_HasBuildIdEnvVar(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{GitSha: \"abc123\"},\n\t}\n\tjob := r.createBuildJob(ld, \"build123\")\n\n\tfound := false\n\tfor _, env := range job.Spec.Template.Spec.Containers[0].Env {\n\t\tif env.Name == envBuildID {\n\t\t\tfound = true\n\t\t\tif env.Value != \"build123\" {\n\t\t\t\tt.Errorf(\"expected LLAMA_DEPLOY_BUILD_ID=build123, got %q\", env.Value)\n\t\t\t}\n\t\t}\n\t}\n\tif !found {\n\t\tt.Error(\"expected LLAMA_DEPLOY_BUILD_ID env var in build job\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// findContainerResources tests\n// ---------------------------------------------------------------------------\n\nfunc TestFindContainerResources(t *testing.T) {\n\tcontainers := []corev1.Container{\n\t\t{\n\t\t\tName: \"app\",\n\t\t\tResources: corev1.ResourceRequirements{\n\t\t\t\tRequests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse(\"1\")},\n\t\t\t},\n\t\t},\n\t\t{Name: \"sidecar\"},\n\t}\n\n\tt.Run(\"found with resources\", func(t *testing.T) {\n\t\tres := findContainerResources(containers, \"app\")\n\t\tif res == nil {\n\t\t\tt.Fatal(\"expected non-nil resources for app container\")\n\t\t}\n\t})\n\n\tt.Run(\"found without resources\", func(t *testing.T) {\n\t\tres := findContainerResources(containers, \"sidecar\")\n\t\tif res != nil {\n\t\t\tt.Error(\"expected nil for container with no resources\")\n\t\t}\n\t})\n\n\tt.Run(\"not found\", func(t *testing.T) {\n\t\tres := findContainerResources(containers, \"nonexistent\")\n\t\tif res != nil {\n\t\t\tt.Error(\"expected nil for nonexistent container\")\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// mergeResourceRequirements tests\n// ---------------------------------------------------------------------------\n\nfunc TestMergeResourceRequirements(t *testing.T) {\n\toverlay := &corev1.ResourceRequirements{\n\t\tRequests: corev1.ResourceList{\n\t\t\tcorev1.ResourceCPU: resource.MustParse(\"2\"),\n\t\t},\n\t\tLimits: corev1.ResourceList{\n\t\t\tcorev1.ResourceMemory: resource.MustParse(\"8Gi\"),\n\t\t},\n\t}\n\tdefaults := &corev1.ResourceRequirements{\n\t\tRequests: corev1.ResourceList{\n\t\t\tcorev1.ResourceCPU:    resource.MustParse(\"750m\"),\n\t\t\tcorev1.ResourceMemory: resource.MustParse(\"2Gi\"),\n\t\t},\n\t\tLimits: corev1.ResourceList{\n\t\t\tcorev1.ResourceMemory: resource.MustParse(\"4Gi\"),\n\t\t},\n\t}\n\n\tmerged := mergeResourceRequirements(overlay, defaults)\n\n\t// Overlay CPU should win\n\tcpuReq := merged.Requests[corev1.ResourceCPU]\n\tif cpuReq.Cmp(resource.MustParse(\"2\")) != 0 {\n\t\tt.Errorf(\"expected CPU request 2, got %s\", cpuReq.String())\n\t}\n\n\t// Default memory request should be backfilled\n\tmemReq := merged.Requests[corev1.ResourceMemory]\n\tif memReq.Cmp(resource.MustParse(\"2Gi\")) != 0 {\n\t\tt.Errorf(\"expected memory request 2Gi, got %s\", memReq.String())\n\t}\n\n\t// Overlay memory limit should win\n\tmemLimit := merged.Limits[corev1.ResourceMemory]\n\tif memLimit.Cmp(resource.MustParse(\"8Gi\")) != 0 {\n\t\tt.Errorf(\"expected memory limit 8Gi, got %s\", memLimit.String())\n\t}\n}\n\nfunc TestReconcileBuild_StaleJobDeletion_OnGenerationAdvance(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:       \"my-app\",\n\t\t\tNamespace:  \"default\",\n\t\t\tGeneration: 2,\n\t\t},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t\tGitSha:    \"abc123\",\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tFailedRolloutGeneration: 1,\n\t\t},\n\t}\n\n\tbuildId := computeBuildId(llamaDeploy)\n\tjobName := fmt.Sprintf(\"%s-build-%s\", llamaDeploy.Name, buildId)\n\tif len(jobName) > 63 {\n\t\tjobName = jobName[:63]\n\t}\n\n\texistingJob := &batchv1.Job{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      jobName,\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tStatus: batchv1.JobStatus{\n\t\t\tFailed: 1,\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(llamaDeploy, existingJob).\n\t\tWithStatusSubresource(llamaDeploy).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient: fakeClient,\n\t\tScheme: scheme,\n\t}\n\n\tctx := context.Background()\n\t_, result, err := r.reconcileBuild(ctx, llamaDeploy)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result == nil {\n\t\tt.Fatal(\"expected non-nil result for requeue\")\n\t}\n\tif result.RequeueAfter == 0 {\n\t\tt.Errorf(\"expected RequeueAfter > 0, got %v\", result.RequeueAfter)\n\t}\n\n\t// Verify the Job was deleted\n\tvar fetchedJob batchv1.Job\n\terr = fakeClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: \"default\"}, &fetchedJob)\n\tif err == nil {\n\t\tt.Errorf(\"expected Job to be deleted, but it still exists\")\n\t}\n\n\t// Verify failedRolloutGeneration was updated to current generation\n\t// so that if the retry also fails, it stops instead of looping\n\tvar updated llamadeployv1.LlamaDeployment\n\tif err := fakeClient.Get(ctx, types.NamespacedName{Name: \"my-app\", Namespace: \"default\"}, &updated); err != nil {\n\t\tt.Fatalf(\"failed to re-read LlamaDeployment: %v\", err)\n\t}\n\tif updated.Status.FailedRolloutGeneration != llamaDeploy.Generation {\n\t\tt.Errorf(\"expected failedRolloutGeneration=%d after retry, got %d\",\n\t\t\tllamaDeploy.Generation, updated.Status.FailedRolloutGeneration)\n\t}\n}\n\nfunc TestReconcileBuild_NoJobDeletion_OnSameGeneration(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:       \"my-app\",\n\t\t\tNamespace:  \"default\",\n\t\t\tGeneration: 1,\n\t\t},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t\tGitSha:    \"abc123\",\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tFailedRolloutGeneration: 1,\n\t\t},\n\t}\n\n\tbuildId := computeBuildId(llamaDeploy)\n\tjobName := fmt.Sprintf(\"%s-build-%s\", llamaDeploy.Name, buildId)\n\tif len(jobName) > 63 {\n\t\tjobName = jobName[:63]\n\t}\n\n\texistingJob := &batchv1.Job{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      jobName,\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tStatus: batchv1.JobStatus{\n\t\t\tFailed: 1,\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(llamaDeploy, existingJob).\n\t\tWithStatusSubresource(llamaDeploy).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient: fakeClient,\n\t\tScheme: scheme,\n\t}\n\n\tctx := context.Background()\n\t_, result, err := r.reconcileBuild(ctx, llamaDeploy)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result == nil {\n\t\tt.Fatal(\"expected non-nil result\")\n\t}\n\texpected := ctrl.Result{}\n\tif *result != expected {\n\t\tt.Errorf(\"expected empty Result (no requeue), got %+v\", *result)\n\t}\n\n\t// Verify the phase is BuildFailed\n\tif llamaDeploy.Status.Phase != PhaseBuildFailed {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhaseBuildFailed, llamaDeploy.Status.Phase)\n\t}\n\n\t// Verify the Job still exists\n\tvar fetchedJob batchv1.Job\n\terr = fakeClient.Get(ctx, types.NamespacedName{Name: jobName, Namespace: \"default\"}, &fetchedJob)\n\tif err != nil {\n\t\tt.Errorf(\"expected Job to still exist, but got error: %v\", err)\n\t}\n}\n\nfunc TestInitializeStatus_PreservesPhaseBuilding(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:       \"my-app\",\n\t\t\tNamespace:  \"default\",\n\t\t\tGeneration: 2,\n\t\t},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitSha:    \"abc123\",\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:                    PhaseBuilding,\n\t\t\tLastReconciledGeneration: 1, // generation changed → needsFullReconcile\n\t\t\tSchemaVersion:            CurrentSchemaVersion,\n\t\t\tAuthToken:                \"existing-token\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(llamaDeploy).\n\t\tWithStatusSubresource(llamaDeploy).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient: fakeClient,\n\t\tScheme: scheme,\n\t}\n\n\tctx := context.Background()\n\terr := r.initializeStatus(ctx, llamaDeploy, true /* needsFullReconcile */)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\t// Phase should still be Building, not reset to Pending\n\tif llamaDeploy.Status.Phase != PhaseBuilding {\n\t\tt.Errorf(\"expected phase %q to be preserved, got %q\", PhaseBuilding, llamaDeploy.Status.Phase)\n\t}\n}\n\nfunc TestIsRollingPhase(t *testing.T) {\n\t// isRollingPhase mirrors the predicate logic in the rollout-aware self-watch\n\tisRollingPhase := func(phase string) bool {\n\t\treturn phase == PhasePending || phase == PhaseRollingOut || phase == PhaseBuilding\n\t}\n\n\ttests := []struct {\n\t\tphase    string\n\t\texpected bool\n\t}{\n\t\t{PhasePending, true},\n\t\t{PhaseRollingOut, true},\n\t\t{PhaseBuilding, true},\n\t\t{PhaseRunning, false},\n\t\t{PhaseFailed, false},\n\t\t{PhaseRolloutFailed, false},\n\t\t{PhaseBuildFailed, false},\n\t\t{PhaseSuspended, false},\n\t\t{\"\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tgot := isRollingPhase(tt.phase)\n\t\tif got != tt.expected {\n\t\t\tt.Errorf(\"isRollingPhase(%q) = %v, want %v\", tt.phase, got, tt.expected)\n\t\t}\n\t}\n\n\t// Verify the predicate would fire on transitions OUT of rolling phases\n\ttransitions := []struct {\n\t\toldPhase, newPhase string\n\t\tshouldFire         bool\n\t}{\n\t\t{PhaseBuilding, PhaseRunning, true},         // build complete\n\t\t{PhaseBuilding, PhaseBuildFailed, true},     // build failed\n\t\t{PhasePending, PhaseRunning, true},          // rollout complete\n\t\t{PhaseRollingOut, PhaseRunning, true},       // rollout complete\n\t\t{PhaseRollingOut, PhaseRolloutFailed, true}, // rollout failed\n\t\t{PhaseRunning, PhaseSuspended, false},       // not a rolling phase transition\n\t\t{PhaseBuilding, PhasePending, false},        // still in rolling phase\n\t\t{PhasePending, PhaseBuilding, false},        // still in rolling phase\n\t}\n\n\tfor _, tt := range transitions {\n\t\tfires := isRollingPhase(tt.oldPhase) && !isRollingPhase(tt.newPhase)\n\t\tif fires != tt.shouldFire {\n\t\t\tt.Errorf(\"transition %q→%q: fires=%v, want %v\", tt.oldPhase, tt.newPhase, fires, tt.shouldFire)\n\t\t}\n\t}\n}\n\nfunc TestReconcileBuild_SkipsSuspendedDeployment(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId:       \"proj-123\",\n\t\t\tRepoUrl:         \"https://github.com/example/repo\",\n\t\t\tGitSha:          \"abc123\",\n\t\t\tSuspended:       true,\n\t\t\tBuildGeneration: 1,\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tLastBuiltGeneration: 1, // equal to spec.buildGeneration\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tbuildId, result, err := r.reconcileBuild(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif buildId != \"\" {\n\t\tt.Errorf(\"expected empty buildId for suspended deployment, got %q\", buildId)\n\t}\n\tif result != nil {\n\t\tt.Errorf(\"expected nil result for suspended deployment, got %+v\", result)\n\t}\n}\n\nfunc TestReconcileBuild_CacheHitSucceeded(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitSha:    \"abc123\",\n\t\t},\n\t}\n\n\texpectedBuildId := computeBuildId(ld)\n\tld.Status = llamadeployv1.LlamaDeploymentStatus{\n\t\tBuildId:     expectedBuildId,\n\t\tBuildStatus: BuildStatusSucceeded,\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tbuildId, result, err := r.reconcileBuild(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif buildId != expectedBuildId {\n\t\tt.Errorf(\"expected buildId %q, got %q\", expectedBuildId, buildId)\n\t}\n\tif result != nil {\n\t\tt.Errorf(\"expected nil result for cache hit, got %+v\", result)\n\t}\n}\n\nfunc TestReconcileBuild_JobRunning(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitSha:    \"abc123\",\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tAuthToken: \"tok\",\n\t\t},\n\t}\n\n\tbuildId := computeBuildId(ld)\n\tjobName := fmt.Sprintf(\"%s-build-%s\", ld.Name, buildId)\n\tif len(jobName) > 63 {\n\t\tjobName = jobName[:63]\n\t}\n\n\t// Seed a running Job (no Succeeded/Failed)\n\texistingJob := &batchv1.Job{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      jobName,\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tStatus: batchv1.JobStatus{},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(ld, existingJob).\n\t\tWithStatusSubresource(ld).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\tctx := context.Background()\n\n\tgotBuildId, result, err := r.reconcileBuild(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif gotBuildId != \"\" {\n\t\tt.Errorf(\"expected empty buildId for running job, got %q\", gotBuildId)\n\t}\n\tif result == nil {\n\t\tt.Fatal(\"expected non-nil result for running job\")\n\t}\n\tif result.RequeueAfter == 0 {\n\t\tt.Error(\"expected RequeueAfter > 0 for running job\")\n\t}\n}\n\nfunc TestInitializeStatus_ResetsPendingPhase(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:       \"my-app\",\n\t\t\tNamespace:  \"default\",\n\t\t\tGeneration: 2,\n\t\t},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t},\n\t\tStatus: llamadeployv1.LlamaDeploymentStatus{\n\t\t\tPhase:                    PhaseRunning,\n\t\t\tLastReconciledGeneration: 1,\n\t\t\tSchemaVersion:            CurrentSchemaVersion,\n\t\t\tAuthToken:                \"existing-token\",\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(llamaDeploy).\n\t\tWithStatusSubresource(llamaDeploy).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient: fakeClient,\n\t\tScheme: scheme,\n\t}\n\n\tctx := context.Background()\n\terr := r.initializeStatus(ctx, llamaDeploy, true /* needsFullReconcile */)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\t// Non-terminal, non-Building phases should be reset to Pending\n\tif llamaDeploy.Status.Phase != PhasePending {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhasePending, llamaDeploy.Status.Phase)\n\t}\n}\n\nfunc TestReconcileBuild_SkipsWhenRepoUrlEmpty(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      \"pending-app\",\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"\", // no code source configured\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(llamaDeploy).\n\t\tWithStatusSubresource(llamaDeploy).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient: fakeClient,\n\t\tScheme: scheme,\n\t}\n\n\tctx := context.Background()\n\tbuildId, result, err := r.reconcileBuild(ctx, llamaDeploy)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result != nil {\n\t\tt.Errorf(\"expected nil result for skipped build, got %+v\", result)\n\t}\n\tif buildId != \"\" {\n\t\tt.Errorf(\"expected empty buildId, got %q\", buildId)\n\t}\n}\n\n// When the spec advances mid-build, the in-flight Job for the old buildId\n// must be deleted so it doesn't race the new one to upload.\nfunc TestReconcileBuild_SupersedesInFlightJob_OnBuildIdChange(t *testing.T) {\n\tr, ld, c := newBuildSupersedeFixture(t, buildSupersedeFixture{\n\t\tdeploymentName: \"my-app\",\n\t\tstaleBuildId:   \"stalebuild1234\",\n\t\tnewGitSha:      \"abc123\",\n\t\tstaleJobStatus: &batchv1.JobStatus{}, // in-flight: no Succeeded, no Failed\n\t})\n\tctx := context.Background()\n\n\t_, result, err := r.reconcileBuild(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif result == nil {\n\t\tt.Fatal(\"expected non-nil result after creating new build job\")\n\t}\n\n\t// The stale Job should have been deleted.\n\tstaleJobName := buildJobName(ld.Name, \"stalebuild1234\")\n\tvar fetchedStale batchv1.Job\n\tif err := c.Get(ctx, types.NamespacedName{Name: staleJobName, Namespace: \"default\"}, &fetchedStale); err == nil {\n\t\tt.Error(\"expected stale Job to be deleted, but it still exists\")\n\t}\n\n\t// The new build Job should exist.\n\tnewBuildId := computeBuildId(ld)\n\tif newBuildId == \"stalebuild1234\" {\n\t\tt.Fatalf(\"test invariant broken: stale and new buildId are both %q\", newBuildId)\n\t}\n\tnewJobName := buildJobName(ld.Name, newBuildId)\n\tvar newJob batchv1.Job\n\tif err := c.Get(ctx, types.NamespacedName{Name: newJobName, Namespace: \"default\"}, &newJob); err != nil {\n\t\tt.Errorf(\"expected new build Job %q to be created: %v\", newJobName, err)\n\t}\n}\n\n// Succeeded stale Jobs must be left alone so their artifacts remain available\n// for A → B → A rollback-by-cache-hit.\nfunc TestReconcileBuild_DoesNotDeleteSucceededSupersededJob(t *testing.T) {\n\tr, ld, c := newBuildSupersedeFixture(t, buildSupersedeFixture{\n\t\tdeploymentName: \"my-app\",\n\t\tstaleBuildId:   \"succeededstale1\",\n\t\tnewGitSha:      \"new-sha\",\n\t\t// BuildStatus is Running (stale) but the Job itself has Succeeded —\n\t\t// the operator should inspect the Job and leave it alone.\n\t\tstaleJobStatus: &batchv1.JobStatus{Succeeded: 1},\n\t})\n\tctx := context.Background()\n\n\tif _, _, err := r.reconcileBuild(ctx, ld); err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tstaleJobName := buildJobName(ld.Name, \"succeededstale1\")\n\tvar fetchedStale batchv1.Job\n\tif err := c.Get(ctx, types.NamespacedName{Name: staleJobName, Namespace: \"default\"}, &fetchedStale); err != nil {\n\t\tt.Errorf(\"expected succeeded stale Job to be preserved, got err: %v\", err)\n\t}\n}\n\n// A stale Job that's already been reaped by TTL should not error — we just\n// proceed to create the new Job.\nfunc TestReconcileBuild_SupersedesJob_WhenStaleJobAlreadyGone(t *testing.T) {\n\tr, ld, c := newBuildSupersedeFixture(t, buildSupersedeFixture{\n\t\tdeploymentName: \"my-app\",\n\t\tstaleBuildId:   \"reapedstale12\",\n\t\tnewGitSha:      \"abc123\",\n\t\tstaleJobStatus: nil, // no stale Job in the cluster\n\t})\n\tctx := context.Background()\n\n\t_, result, err := r.reconcileBuild(ctx, ld)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error when stale Job is absent: %v\", err)\n\t}\n\tif result == nil {\n\t\tt.Fatal(\"expected non-nil result when creating new build job\")\n\t}\n\n\tnewJobName := buildJobName(ld.Name, computeBuildId(ld))\n\tvar newJob batchv1.Job\n\tif err := c.Get(ctx, types.NamespacedName{Name: newJobName, Namespace: \"default\"}, &newJob); err != nil {\n\t\tt.Errorf(\"expected new build Job %q to be created: %v\", newJobName, err)\n\t}\n}\n\nfunc TestReconcileBuild_ProceedsWhenRepoUrlSetButGitShaEmpty(t *testing.T) {\n\tscheme := newTestScheme()\n\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      \"ready-app\",\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t\t// GitSha intentionally empty — build should still proceed\n\t\t},\n\t}\n\n\tfakeClient := fake.NewClientBuilder().\n\t\tWithScheme(scheme).\n\t\tWithObjects(llamaDeploy).\n\t\tWithStatusSubresource(llamaDeploy).\n\t\tBuild()\n\n\tr := &LlamaDeploymentReconciler{\n\t\tClient: fakeClient,\n\t\tScheme: scheme,\n\t}\n\n\tctx := context.Background()\n\t_, result, err := r.reconcileBuild(ctx, llamaDeploy)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\t// When RepoUrl is set, the build should proceed (create a job and requeue),\n\t// not skip with nil result like a pending deployment.\n\tif result == nil {\n\t\tt.Fatal(\"expected non-nil result (build should proceed and requeue), got nil\")\n\t}\n\tif result.RequeueAfter == 0 {\n\t\tt.Error(\"expected RequeueAfter > 0 indicating build job was created\")\n\t}\n\t// Verify status was updated to Building\n\tif llamaDeploy.Status.Phase != PhaseBuilding {\n\t\tt.Errorf(\"expected phase %q, got %q\", PhaseBuilding, llamaDeploy.Status.Phase)\n\t}\n}\n"
  },
  {
    "path": "operator/internal/controller/resources_security_test.go",
    "content": "//go:build !integration\n\npackage controller\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client/fake\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\n// ---------------------------------------------------------------------------\n// findContainerSecurityContext tests\n// ---------------------------------------------------------------------------\n\nfunc TestFindContainerSecurityContext(t *testing.T) {\n\tsc := &corev1.SecurityContext{\n\t\tRunAsNonRoot:             ptr(true),\n\t\tAllowPrivilegeEscalation: ptr(false),\n\t}\n\tcontainers := []corev1.Container{\n\t\t{Name: \"app\", SecurityContext: sc},\n\t\t{Name: \"sidecar\"},\n\t}\n\n\tt.Run(\"found with security context\", func(t *testing.T) {\n\t\tgot := findContainerSecurityContext(containers, \"app\")\n\t\tif got == nil {\n\t\t\tt.Fatal(\"expected non-nil security context for app container\")\n\t\t}\n\t\tif got != sc {\n\t\t\tt.Error(\"expected same SecurityContext pointer\")\n\t\t}\n\t})\n\n\tt.Run(\"found without security context\", func(t *testing.T) {\n\t\tgot := findContainerSecurityContext(containers, \"sidecar\")\n\t\tif got != nil {\n\t\t\tt.Error(\"expected nil for container with no security context\")\n\t\t}\n\t})\n\n\tt.Run(\"not found\", func(t *testing.T) {\n\t\tgot := findContainerSecurityContext(containers, \"nonexistent\")\n\t\tif got != nil {\n\t\t\tt.Error(\"expected nil for nonexistent container\")\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// Build job security context tests\n// ---------------------------------------------------------------------------\n\nfunc TestCreateBuildJob_SecurityContext(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t},\n\t}\n\tjob := r.createBuildJob(ld, \"abc123\")\n\n\tpodSC := job.Spec.Template.Spec.SecurityContext\n\tif podSC == nil {\n\t\tt.Fatal(\"expected pod security context\")\n\t}\n\tif podSC.FSGroup == nil || *podSC.FSGroup != AppServerGID {\n\t\tt.Errorf(\"expected FSGroup %d, got %v\", AppServerGID, podSC.FSGroup)\n\t}\n\n\tc := job.Spec.Template.Spec.Containers[0]\n\tassertFullSecurityContext(t, c.SecurityContext, AppServerUID, AppServerGID)\n}\n\n// ---------------------------------------------------------------------------\n// Deployment security context tests\n// ---------------------------------------------------------------------------\n\nfunc TestCreateDeployment_SecurityContexts(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t},\n\t}\n\tdep := r.createDeploymentForLlama(ld, \"build123\")\n\n\tpodSC := dep.Spec.Template.Spec.SecurityContext\n\tif podSC == nil {\n\t\tt.Fatal(\"expected pod security context\")\n\t}\n\tif podSC.FSGroup == nil || *podSC.FSGroup != AppServerGID {\n\t\tt.Errorf(\"expected FSGroup %d, got %v\", AppServerGID, podSC.FSGroup)\n\t}\n\n\tt.Run(\"bootstrap init container\", func(t *testing.T) {\n\t\tinitContainers := dep.Spec.Template.Spec.InitContainers\n\t\tif len(initContainers) == 0 {\n\t\t\tt.Fatal(\"expected at least one init container\")\n\t\t}\n\t\tassertFullSecurityContext(t, initContainers[0].SecurityContext, AppServerUID, AppServerGID)\n\t})\n\n\tt.Run(\"file-server container\", func(t *testing.T) {\n\t\tc := findContainer(dep.Spec.Template.Spec.Containers, \"file-server\")\n\t\tif c == nil {\n\t\t\tt.Fatal(\"expected file-server container\")\n\t\t}\n\t\tassertFullSecurityContext(t, c.SecurityContext, NginxUID, NginxGID)\n\t})\n\n\tt.Run(\"app container\", func(t *testing.T) {\n\t\tc := findContainer(dep.Spec.Template.Spec.Containers, ContainerNameApp)\n\t\tif c == nil {\n\t\t\tt.Fatal(\"expected app container\")\n\t\t}\n\t\tsc := c.SecurityContext\n\t\tif sc == nil {\n\t\t\tt.Fatal(\"expected security context on app container\")\n\t\t}\n\t\tif sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation != false {\n\t\t\tt.Error(\"expected AllowPrivilegeEscalation=false\")\n\t\t}\n\t\tif sc.Capabilities == nil || len(sc.Capabilities.Drop) == 0 || sc.Capabilities.Drop[0] != \"ALL\" {\n\t\t\tt.Error(\"expected capabilities drop ALL\")\n\t\t}\n\t\t// App container intentionally omits RunAsUser/RunAsNonRoot for backward compat\n\t\tif sc.RunAsUser != nil {\n\t\t\tt.Error(\"expected RunAsUser to be nil on app container (backward compat)\")\n\t\t}\n\t\tif sc.RunAsNonRoot != nil {\n\t\t\tt.Error(\"expected RunAsNonRoot to be nil on app container (backward compat)\")\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// GIT_CONFIG env vars\n// ---------------------------------------------------------------------------\n\nfunc TestCreateBuildJob_HasGitSafeDirectoryEnvVars(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec: llamadeployv1.LlamaDeploymentSpec{\n\t\t\tProjectId: \"proj-123\",\n\t\t\tRepoUrl:   \"https://github.com/example/repo\",\n\t\t\tGitRef:    \"main\",\n\t\t},\n\t}\n\tjob := r.createBuildJob(ld, \"abc123\")\n\n\tenvs := job.Spec.Template.Spec.Containers[0].Env\n\tassertEnvVar(t, envs, \"GIT_CONFIG_COUNT\", \"1\")\n\tassertEnvVar(t, envs, \"GIT_CONFIG_KEY_0\", \"safe.directory\")\n\tassertEnvVar(t, envs, \"GIT_CONFIG_VALUE_0\", \"/opt/app\")\n}\n\n// ---------------------------------------------------------------------------\n// Overlay security context propagation\n// ---------------------------------------------------------------------------\n\nfunc TestApplyBuildJobTemplateOverlay_PropagatesSecurityContext(t *testing.T) {\n\tscheme := newTestScheme()\n\n\toverrideSC := &corev1.SecurityContext{\n\t\tRunAsNonRoot:             ptr(true),\n\t\tRunAsUser:                ptr(int64(2000)),\n\t\tRunAsGroup:               ptr(int64(2000)),\n\t\tAllowPrivilegeEscalation: ptr(false),\n\t}\n\n\tt.Run(\"from build container\", func(t *testing.T) {\n\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"sc-build\", Namespace: \"default\"},\n\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t{Name: ContainerNameBuild, SecurityContext: overrideSC},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tfakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tmpl).Build()\n\t\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{TemplateName: \"sc-build\"},\n\t\t}\n\n\t\tjob := r.createBuildJob(ld, \"abc123\")\n\t\tif err := r.applyBuildJobTemplateOverlay(context.Background(), ld, job); err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tsc := job.Spec.Template.Spec.Containers[0].SecurityContext\n\t\tif sc == nil || sc.RunAsUser == nil || *sc.RunAsUser != 2000 {\n\t\t\tt.Error(\"expected build container SecurityContext to be replaced by template overlay (RunAsUser=2000)\")\n\t\t}\n\t})\n\n\tt.Run(\"falls back from app container\", func(t *testing.T) {\n\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"sc-app\", Namespace: \"default\"},\n\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\tContainers: []corev1.Container{\n\t\t\t\t\t\t\t{Name: ContainerNameApp, SecurityContext: overrideSC},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tfakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tmpl).Build()\n\t\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{TemplateName: \"sc-app\"},\n\t\t}\n\n\t\tjob := r.createBuildJob(ld, \"abc123\")\n\t\tif err := r.applyBuildJobTemplateOverlay(context.Background(), ld, job); err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tsc := job.Spec.Template.Spec.Containers[0].SecurityContext\n\t\tif sc == nil || sc.RunAsUser == nil || *sc.RunAsUser != 2000 {\n\t\t\tt.Error(\"expected build container SecurityContext to fall back to app container overlay (RunAsUser=2000)\")\n\t\t}\n\t})\n\n\tt.Run(\"pod-level security context override\", func(t *testing.T) {\n\t\tpodSC := &corev1.PodSecurityContext{\n\t\t\tRunAsNonRoot: ptr(true),\n\t\t\tFSGroup:      ptr(int64(3000)),\n\t\t}\n\t\ttmpl := &llamadeployv1.LlamaDeploymentTemplate{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"sc-pod\", Namespace: \"default\"},\n\t\t\tSpec: llamadeployv1.LlamaDeploymentTemplateSpec{\n\t\t\t\tPodSpec: corev1.PodTemplateSpec{\n\t\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\t\tSecurityContext: podSC,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tfakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tmpl).Build()\n\t\tr := &LlamaDeploymentReconciler{Client: fakeClient, Scheme: scheme}\n\t\tld := &llamadeployv1.LlamaDeployment{\n\t\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{TemplateName: \"sc-pod\"},\n\t\t}\n\n\t\tjob := r.createBuildJob(ld, \"abc123\")\n\t\tif err := r.applyBuildJobTemplateOverlay(context.Background(), ld, job); err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tgot := job.Spec.Template.Spec.SecurityContext\n\t\tif got == nil || got.FSGroup == nil || *got.FSGroup != 3000 {\n\t\t\tt.Error(\"expected pod SecurityContext FSGroup to be overridden to 3000\")\n\t\t}\n\t})\n}\n\n// ---------------------------------------------------------------------------\n// Nginx config tests\n// ---------------------------------------------------------------------------\n\nfunc TestGenerateNginxConfig_NonRootDirectives(t *testing.T) {\n\tr := &LlamaDeploymentReconciler{}\n\tld := &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: \"my-app\", Namespace: \"default\"},\n\t\tSpec:       llamadeployv1.LlamaDeploymentSpec{},\n\t}\n\tconfig := r.generateNginxConfig(ld)\n\n\trequired := []string{\n\t\t\"pid /tmp/nginx.pid\",\n\t\t\"client_body_temp_path /tmp/client_temp\",\n\t\t\"proxy_temp_path       /tmp/proxy_temp\",\n\t\t\"fastcgi_temp_path     /tmp/fastcgi_temp\",\n\t\t\"uwsgi_temp_path       /tmp/uwsgi_temp\",\n\t\t\"scgi_temp_path        /tmp/scgi_temp\",\n\t}\n\n\tfor _, directive := range required {\n\t\tif !strings.Contains(config, directive) {\n\t\t\tt.Errorf(\"expected nginx config to contain %q\", directive)\n\t\t}\n\t}\n\n\tif strings.Contains(config, \"user root\") {\n\t\tt.Error(\"nginx config should not contain 'user root' directive\")\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunc assertFullSecurityContext(t *testing.T, sc *corev1.SecurityContext, uid, gid int64) {\n\tt.Helper()\n\tif sc == nil {\n\t\tt.Fatal(\"expected non-nil security context\")\n\t}\n\tif sc.RunAsNonRoot == nil || *sc.RunAsNonRoot != true {\n\t\tt.Error(\"expected RunAsNonRoot=true\")\n\t}\n\tif sc.RunAsUser == nil || *sc.RunAsUser != uid {\n\t\tt.Errorf(\"expected RunAsUser=%d, got %v\", uid, sc.RunAsUser)\n\t}\n\tif sc.RunAsGroup == nil || *sc.RunAsGroup != gid {\n\t\tt.Errorf(\"expected RunAsGroup=%d, got %v\", gid, sc.RunAsGroup)\n\t}\n\tif sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation != false {\n\t\tt.Error(\"expected AllowPrivilegeEscalation=false\")\n\t}\n\tif sc.Capabilities == nil || len(sc.Capabilities.Drop) == 0 || sc.Capabilities.Drop[0] != \"ALL\" {\n\t\tt.Error(\"expected capabilities drop ALL\")\n\t}\n}\n\nfunc assertEnvVar(t *testing.T, envs []corev1.EnvVar, name, value string) {\n\tt.Helper()\n\tfor _, e := range envs {\n\t\tif e.Name == name {\n\t\t\tif e.Value != value {\n\t\t\t\tt.Errorf(\"expected %s=%q, got %q\", name, value, e.Value)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\tt.Errorf(\"expected env var %s not found\", name)\n}\n\nfunc findContainer(containers []corev1.Container, name string) *corev1.Container {\n\tfor i := range containers {\n\t\tif containers[i].Name == name {\n\t\t\treturn &containers[i]\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "operator/internal/controller/suite_test.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t. \"github.com/onsi/ginkgo/v2\"\n\t. \"github.com/onsi/gomega\"\n\n\t\"k8s.io/client-go/kubernetes/scheme\"\n\t\"k8s.io/client-go/rest\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/envtest\"\n\tlogf \"sigs.k8s.io/controller-runtime/pkg/log\"\n\t\"sigs.k8s.io/controller-runtime/pkg/log/zap\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n\t// +kubebuilder:scaffold:imports\n)\n\n// These tests use Ginkgo (BDD-style Go testing framework). Refer to\n// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.\n\nvar (\n\tctx       context.Context\n\tcancel    context.CancelFunc\n\ttestEnv   *envtest.Environment\n\tcfg       *rest.Config\n\tk8sClient client.Client\n)\n\nfunc TestControllers(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\n\tRunSpecs(t, \"Controller Suite\")\n}\n\nvar _ = BeforeSuite(func() {\n\tlogf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))\n\n\t// Set build API host for tests (matches the hardcoded default)\n\tExpect(os.Setenv(\"LLAMA_DEPLOY_BUILD_API_HOST\", \"llama-agents-build.llama-agents.svc.cluster.local:8001\")).To(Succeed())\n\n\tctx, cancel = context.WithCancel(context.TODO())\n\n\tvar err error\n\terr = llamadeployv1.AddToScheme(scheme.Scheme)\n\tExpect(err).NotTo(HaveOccurred())\n\n\t// +kubebuilder:scaffold:scheme\n\n\tBy(\"bootstrapping test environment\")\n\ttestEnv = &envtest.Environment{\n\t\tCRDDirectoryPaths:     []string{filepath.Join(\"..\", \"..\", \"config\", \"crd\", \"bases\")},\n\t\tErrorIfCRDPathMissing: true,\n\t}\n\n\t// Retrieve the first found binary directory to allow running tests from IDEs\n\tif getFirstFoundEnvTestBinaryDir() != \"\" {\n\t\ttestEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()\n\t}\n\n\t// cfg is defined in this file globally.\n\tcfg, err = testEnv.Start()\n\tExpect(err).NotTo(HaveOccurred())\n\tExpect(cfg).NotTo(BeNil())\n\n\tk8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})\n\tExpect(err).NotTo(HaveOccurred())\n\tExpect(k8sClient).NotTo(BeNil())\n})\n\nvar _ = AfterSuite(func() {\n\tBy(\"tearing down the test environment\")\n\tcancel()\n\terr := testEnv.Stop()\n\tExpect(err).NotTo(HaveOccurred())\n})\n\n// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.\n// ENVTEST-based tests depend on specific binaries, usually located in paths set by\n// controller-runtime. When running tests directly (e.g., via an IDE) without using\n// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.\n//\n// This function streamlines the process by finding the required binaries, similar to\n// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are\n// properly set up, run 'make setup-envtest' beforehand.\nfunc getFirstFoundEnvTestBinaryDir() string {\n\tbasePath := filepath.Join(\"..\", \"..\", \"bin\", \"k8s\")\n\tentries, err := os.ReadDir(basePath)\n\tif err != nil {\n\t\tlogf.Log.Error(err, \"Failed to read directory\", \"path\", basePath)\n\t\treturn \"\"\n\t}\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\treturn filepath.Join(basePath, entry.Name())\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "operator/internal/controller/test_utils_test.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t. \"github.com/onsi/gomega\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tapierrors \"k8s.io/apimachinery/pkg/api/errors\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\t\"k8s.io/client-go/tools/record\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/reconcile\"\n\n\tllamadeployv1 \"llama-agents-operator/api/v1\"\n)\n\n// GetDeploymentEventually fetches a Deployment with Eventually retry.\nfunc GetDeploymentEventually(ctx context.Context, name, ns string) *appsv1.Deployment {\n\tdepl := &appsv1.Deployment{}\n\tEventually(func() error {\n\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, depl)\n\t}).Should(Succeed())\n\treturn depl\n}\n\n// GetConfigMapEventually fetches a ConfigMap with Eventually retry.\nfunc GetConfigMapEventually(ctx context.Context, name, ns string) *corev1.ConfigMap {\n\tcm := &corev1.ConfigMap{}\n\tEventually(func() error {\n\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, cm)\n\t}).Should(Succeed())\n\treturn cm\n}\n\n// FindContainer returns a container by name from a PodSpec.\nfunc FindContainer(podSpec corev1.PodSpec, name string) (corev1.Container, bool) {\n\tfor _, c := range podSpec.Containers {\n\t\tif c.Name == name {\n\t\t\treturn c, true\n\t\t}\n\t}\n\treturn corev1.Container{}, false\n}\n\n// FindInitContainer returns an init container by name from a PodSpec.\nfunc FindInitContainer(podSpec corev1.PodSpec, name string) (corev1.Container, bool) {\n\tfor _, c := range podSpec.InitContainers {\n\t\tif c.Name == name {\n\t\t\treturn c, true\n\t\t}\n\t}\n\treturn corev1.Container{}, false\n}\n\n// EnvMap builds a map of non-empty env var values from a container.\nfunc EnvMap(c corev1.Container) map[string]string {\n\tout := make(map[string]string, len(c.Env))\n\tfor _, e := range c.Env {\n\t\tif e.Value != \"\" {\n\t\t\tout[e.Name] = e.Value\n\t\t}\n\t}\n\treturn out\n}\n\n// ExpectEnv asserts an exact env var value on a container.\nfunc ExpectEnv(c corev1.Container, key, value string) {\n\tenvs := EnvMap(c)\n\tExpect(envs[key]).To(Equal(value), fmt.Sprintf(\"env %s mismatch\", key))\n}\n\n// ExpectEnvMatches asserts an env var matches a regex pattern.\nfunc ExpectEnvMatches(c corev1.Container, key, pattern string) {\n\tenvs := EnvMap(c)\n\tExpect(envs[key]).To(MatchRegexp(pattern), fmt.Sprintf(\"env %s mismatch\", key))\n}\n\n// ExpectServicePort asserts a service exposes port->target mapping.\nfunc ExpectServicePort(ctx context.Context, name, ns string, port int32, target int) {\n\tsvc := &corev1.Service{}\n\tEventually(func() error {\n\t\treturn k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, svc)\n\t}).Should(Succeed())\n\tExpect(svc.Spec.Ports).To(HaveLen(1))\n\tExpect(svc.Spec.Ports[0].Port).To(Equal(port))\n\tExpect(svc.Spec.Ports[0].TargetPort.IntValue()).To(Equal(target))\n}\n\n// CleanupLlama deletes a LlamaDeployment and associated ServiceAccount if present.\nfunc CleanupLlama(ctx context.Context, name, ns string) {\n\t// Delete LlamaDeployment with finalizer handling\n\tllamaDeploy := &llamadeployv1.LlamaDeployment{}\n\tif err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, llamaDeploy); err == nil {\n\t\tif len(llamaDeploy.Finalizers) > 0 {\n\t\t\tllamaDeploy.Finalizers = []string{}\n\t\t\t_ = k8sClient.Update(ctx, llamaDeploy)\n\t\t}\n\t\t_ = k8sClient.Delete(ctx, llamaDeploy)\n\t\t// Wait until deleted\n\t\tEventually(func() bool {\n\t\t\terr := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, llamaDeploy)\n\t\t\treturn apierrors.IsNotFound(err)\n\t\t}, \"5s\", \"100ms\").Should(BeTrue())\n\t}\n\n\t// Delete ServiceAccount if exists\n\tsa := &corev1.ServiceAccount{}\n\tif err := k8sClient.Get(ctx, types.NamespacedName{Name: name + \"-sa\", Namespace: ns}, sa); err == nil {\n\t\t_ = k8sClient.Delete(ctx, sa)\n\t}\n}\n\n// Functional options for building LlamaDeployment spec\ntype LlamaSpecOption func(*llamadeployv1.LlamaDeploymentSpec)\n\nfunc WithImage(image string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.Image = image }\n}\nfunc WithImageTag(tag string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.ImageTag = tag }\n}\nfunc WithSecret(name string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.SecretName = name }\n}\nfunc WithDeploymentFilePath(path string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.DeploymentFilePath = path }\n}\nfunc WithGitRef(ref string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.GitRef = ref }\n}\nfunc WithGitSha(sha string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.GitSha = sha }\n}\nfunc WithRepoUrl(url string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.RepoUrl = url }\n}\nfunc WithAssetsPath(path string) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.StaticAssetsPath = path }\n}\nfunc WithBuildGeneration(gen int64) LlamaSpecOption {\n\treturn func(s *llamadeployv1.LlamaDeploymentSpec) { s.BuildGeneration = gen }\n}\n\n// NewLlama creates a LlamaDeployment with defaults and applies options.\nfunc NewLlama(name, ns, projectId, repoURL string, opts ...LlamaSpecOption) *llamadeployv1.LlamaDeployment {\n\tspec := llamadeployv1.LlamaDeploymentSpec{\n\t\tProjectId: projectId,\n\t\tRepoUrl:   repoURL,\n\t}\n\tfor _, opt := range opts {\n\t\topt(&spec)\n\t}\n\treturn &llamadeployv1.LlamaDeployment{\n\t\tObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},\n\t\tSpec:       spec,\n\t}\n}\n\n// SetDeploymentAvailableReplicas sets AvailableReplicas on a Deployment's status.\n// Use this in tests that need checkRolloutTimeout to pick PhaseRolloutFailed (>0)\n// vs PhaseFailed (0), since envtest defaults to 0 available replicas.\nfunc SetDeploymentAvailableReplicas(ctx context.Context, name, ns string, replicas int32) {\n\tdeployment := &appsv1.Deployment{}\n\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: ns}, deployment)).To(Succeed())\n\tdeployment.Status.AvailableReplicas = replicas\n\tif replicas > 0 {\n\t\tdeployment.Status.ReadyReplicas = replicas\n\t\tdeployment.Status.Replicas = replicas\n\t}\n\tExpect(k8sClient.Status().Update(ctx, deployment)).To(Succeed())\n}\n\n// ReconcilerOption configures a LlamaDeploymentReconciler created by NewTestReconciler.\ntype ReconcilerOption func(*LlamaDeploymentReconciler)\n\nfunc WithMaxConcurrentRollouts(n int) ReconcilerOption {\n\treturn func(r *LlamaDeploymentReconciler) { r.MaxConcurrentRollouts = n }\n}\n\nfunc WithMaxDeployments(n int) ReconcilerOption {\n\treturn func(r *LlamaDeploymentReconciler) { r.MaxDeployments = n }\n}\n\n// NewTestReconciler creates a reconciler wired to the test envtest client.\nfunc NewTestReconciler(opts ...ReconcilerOption) *LlamaDeploymentReconciler {\n\tr := &LlamaDeploymentReconciler{\n\t\tClient:       k8sClient,\n\t\tScheme:       k8sClient.Scheme(),\n\t\tRecorder:     record.NewFakeRecorder(100),\n\t\tDirectClient: k8sClient,\n\t}\n\tfor _, o := range opts {\n\t\to(r)\n\t}\n\treturn r\n}\n\n// CreateAndReconcile creates the CR and runs one reconciliation.\nfunc CreateAndReconcile(ctx context.Context, r *LlamaDeploymentReconciler, obj *llamadeployv1.LlamaDeployment) {\n\tExpect(k8sClient.Create(ctx, obj)).To(Succeed())\n\t_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: obj.Name, Namespace: obj.Namespace}})\n\tExpect(err).NotTo(HaveOccurred())\n}\n\n// CompleteBuild marks the build Job for a LlamaDeployment as succeeded and reconciles\n// again so that reconcileBuild proceeds past the build phase and creates the Deployment.\n// Call this after the first Reconcile when spec.GitSha is set.\nfunc CompleteBuild(ctx context.Context, r *LlamaDeploymentReconciler, llamaDeploy *llamadeployv1.LlamaDeployment) {\n\t// Re-read to get latest status (buildId set by first reconcile)\n\tExpect(k8sClient.Get(ctx, types.NamespacedName{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}, llamaDeploy)).To(Succeed())\n\tbuildId := computeBuildId(llamaDeploy)\n\tjobName := fmt.Sprintf(\"%s-build-%s\", llamaDeploy.Name, buildId)\n\tif len(jobName) > 63 {\n\t\tjobName = jobName[:63]\n\t}\n\n\t// Mark the build Job as succeeded\n\tjob := &batchv1.Job{}\n\tExpect(k8sClient.Get(ctx, client.ObjectKey{Name: jobName, Namespace: llamaDeploy.Namespace}, job)).To(Succeed())\n\tjob.Status.Succeeded = 1\n\tExpect(k8sClient.Status().Update(ctx, job)).To(Succeed())\n\n\t// Reconcile again to proceed past build\n\t_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: llamaDeploy.Name, Namespace: llamaDeploy.Namespace}})\n\tExpect(err).NotTo(HaveOccurred())\n}\n"
  },
  {
    "path": "operator/package.json",
    "content": "{\n  \"name\": \"llama-agents-operator\",\n  \"version\": \"0.11.1\",\n  \"private\": false,\n  \"docker\": {\n    \"dockerfile\": \"docker/operator.Dockerfile\",\n    \"imageName\": \"llamaindex/llama-agents-operator\",\n    \"platforms\": [\n      \"linux/amd64\",\n      \"linux/arm64\"\n    ],\n    \"cacheMode\": \"min\"\n  }\n}\n"
  },
  {
    "path": "operator/tilt/.gitignore",
    "content": "k8s-manifests/secrets.yaml\nk8s-manifests/backup-secrets.yaml\n"
  },
  {
    "path": "operator/tilt/env-to-secret.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Convert .env file to a Kubernetes secret YAML manifest.\n\nHandles JSON-encoded values (e.g. PEM keys stored with \\\\n escapes).\nUsage: python3 tilt/env-to-secret.py > secret.yaml\n\"\"\"\n\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nenv_file = Path(__file__).parent.parent.parent / \".env\"\nif not env_file.exists():\n    sys.exit(0)\n\ncmd = [\n    \"kubectl\",\n    \"create\",\n    \"secret\",\n    \"generic\",\n    \"control-plane-secrets\",\n    \"-n\",\n    \"llama-agents\",\n    \"--dry-run=client\",\n    \"-o\",\n    \"yaml\",\n]\n\nfor line in env_file.read_text().splitlines():\n    if \"=\" not in line or line.startswith(\"#\"):\n        continue\n    k, v = line.split(\"=\", 1)\n    if v.startswith('\"') and v.endswith('\"'):\n        v = json.loads(v)\n    cmd.append(f\"--from-literal={k}={v}\")\n\nsys.stdout.buffer.write(subprocess.check_output(cmd))\n"
  },
  {
    "path": "operator/tilt/helm/values-dev.yaml",
    "content": "# Development values for llama-agents\n# Two containers: main app (API) + dev server (hot reload only)\n\n# Container configuration for development\ncontrolPlane:\n  # Defer image tag selection to the operator's LLAMA_DEPLOY_IMAGE_TAG\n  # env var (set by tilt) so rebuilds automatically apply to\n  # existing deployments without baking tags into CRDs.\n  defaultAppserverImageTag: \"operator-default\"\n  container:\n    env:\n      - name: FASTAPI_ENV\n        value: \"development\"\n      - name: LOG_LEVEL\n        value: \"debug\"\n      - name: LOCAL_DEV_INGRESS\n        value: \"true\"\n    envFrom:\n      - secretRef:\n          name: control-plane-secrets\n          optional: true\n    resources:\n      requests:\n        memory: \"1Gi\"\n        cpu: \"1000m\"\n      limits:\n        memory: \"2Gi\"\n        cpu: \"2000m\"\n    startupProbe:\n      httpGet:\n        path: /health\n        port: 8000\n      periodSeconds: 1\n      timeoutSeconds: 2\n      failureThreshold: 60\n    livenessProbe:\n      httpGet:\n        path: /health\n        port: 8000\n      periodSeconds: 10\n  hpa:\n    enabled: false\n  objectStorage:\n    s3:\n      endpointUrl: \"http://seaweedfs.llama-agents.svc:8333\"\n      bucket: \"backups\"\n      region: \"us-east-1\"\n    secretRef: \"object-storage-secrets\"\n    backupEncryptionSecretRef: \"backup-encryption-secrets\"\n\n# Tilt rewrites managed-by labels to \"tilt\" on all pods it deploys,\n# which causes the deployment-pods network policy to match infra pods too.\n# Exclude them so the control plane and operator are not blocked.\nnetworkPolicy:\n  extraMatchExpressions:\n    - key: app\n      operator: NotIn\n      values:\n        - llama-agents-control-plane\n        - llama-agents-operator\n\noperator:\n  rolloutTimeoutSeconds: \"300\"\n  maxConcurrentRollouts: 1\n  maxDeployments: 10\n  hpa:\n    enabled: false\n\nmetrics:\n  enabled: true\n\n# Enable local development features\nlocalDev:\n  enabled: true\n  ingressDomain: \"127.0.0.1.nip.io\"\n"
  },
  {
    "path": "operator/tilt/k8s-manifests/.gitkeep",
    "content": ""
  },
  {
    "path": "operator/tilt/k8s-manifests/backup-encryption-secrets.yaml",
    "content": "apiVersion: v1\nkind: Secret\nmetadata:\n  name: backup-encryption-secrets\n  namespace: llama-agents\nstringData:\n  BACKUP_ENCRYPTION_PASSWORD: \"dev-test-password\"\ntype: Opaque\n"
  },
  {
    "path": "operator/tilt/k8s-manifests/kind-gc-cronjob.yaml",
    "content": "# Prunes orphaned containerd images/snapshots inside the kind node.\n# Tilt and `kind load` create new content-addressed layers on every rebuild\n# but never clean up old ones, so overlayfs storage grows unbounded.\napiVersion: batch/v1\nkind: CronJob\nmetadata:\n  name: kind-gc\n  namespace: llama-agents\nspec:\n  schedule: \"0 */6 * * *\"  # every 6 hours\n  successfulJobsHistoryLimit: 1\n  failedJobsHistoryLimit: 1\n  jobTemplate:\n    spec:\n      template:\n        spec:\n          restartPolicy: Never\n          containers:\n            - name: gc\n              image: kindest/node:v1.33.1\n              command:\n                - sh\n                - \"-c\"\n                - |\n                  export CONTAINER_RUNTIME_ENDPOINT=unix:///run/containerd/containerd.sock\n                  for img in $(ctr -n k8s.io images ls -q | grep '^sha256:'); do\n                    ctr -n k8s.io images rm \"$img\" 2>/dev/null\n                  done\n                  crictl rmi --prune 2>/dev/null\n                  echo \"GC complete\"\n              volumeMounts:\n                - name: containerd-sock\n                  mountPath: /run/containerd/containerd.sock\n          volumes:\n            - name: containerd-sock\n              hostPath:\n                path: /run/containerd/containerd.sock\n                type: Socket\n"
  },
  {
    "path": "operator/tilt/k8s-manifests/object-storage-secrets.yaml",
    "content": "apiVersion: v1\nkind: Secret\nmetadata:\n  name: object-storage-secrets\n  namespace: llama-agents\nstringData:\n  S3_ACCESS_KEY: \"dev\"\n  S3_SECRET_KEY: \"dev\"\ntype: Opaque\n"
  },
  {
    "path": "operator/tilt/k8s-manifests/prometheus.yaml",
    "content": "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: prometheus-dev\n  namespace: monitoring\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: prometheus-dev\nrules:\n  - apiGroups: [\"\"]\n    resources: [nodes, nodes/metrics, services, endpoints, pods]\n    verbs: [get, list, watch]\n  - apiGroups: [\"networking.k8s.io\"]\n    resources: [ingresses]\n    verbs: [get, list, watch]\n  - nonResourceURLs: [\"/metrics\"]\n    verbs: [get]\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: prometheus-dev\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: prometheus-dev\nsubjects:\n  - kind: ServiceAccount\n    name: prometheus-dev\n    namespace: monitoring\n---\napiVersion: monitoring.coreos.com/v1\nkind: Prometheus\nmetadata:\n  name: dev\n  namespace: monitoring\nspec:\n  serviceAccountName: prometheus-dev\n  replicas: 1\n  serviceMonitorSelector: {}\n  serviceMonitorNamespaceSelector:\n    matchLabels:\n      kubernetes.io/metadata.name: llama-agents\n  resources:\n    requests:\n      memory: 256Mi\n      cpu: 100m\n    limits:\n      memory: 512Mi\n  retention: 1d\n  securityContext:\n    runAsNonRoot: false\n    runAsUser: 0\n    fsGroup: 0\n"
  },
  {
    "path": "operator/tilt/k8s-manifests/seaweedfs.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: seaweedfs\n  namespace: llama-agents\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: seaweedfs\n  template:\n    metadata:\n      labels:\n        app: seaweedfs\n    spec:\n      containers:\n        - name: seaweedfs\n          image: chrislusf/seaweedfs:latest\n          command: [\"weed\"]\n          args:\n            - \"server\"\n            - \"-dir=/data\"\n            - \"-s3\"\n            - \"-s3.port=8333\"\n            - \"-s3.iam=false\"\n          ports:\n            - containerPort: 8333\n              name: s3\n          readinessProbe:\n            httpGet:\n              path: /\n              port: 8333\n            initialDelaySeconds: 2\n            periodSeconds: 3\n            failureThreshold: 10\n          volumeMounts:\n            - name: data\n              mountPath: /data\n      volumes:\n        - name: data\n          emptyDir: {}\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: seaweedfs\n  namespace: llama-agents\nspec:\n  selector:\n    app: seaweedfs\n  ports:\n    - name: s3\n      port: 8333\n      targetPort: 8333\n"
  },
  {
    "path": "operator/tilt/scripts/install-prometheus.sh",
    "content": "#!/usr/bin/env bash\n# Installs a minimal prometheus-operator + Prometheus instance for local dev.\n# Idempotent — safe to re-run.\nset -euo pipefail\n\nPROM_OP_VERSION=\"v0.89.0\"\nNAMESPACE=\"monitoring\"\nCONTEXT=\"${1:-kind-kind}\"\nKUBECTL=\"kubectl --context=$CONTEXT\"\n\n# Create namespace if needed\n$KUBECTL get namespace \"$NAMESPACE\" &>/dev/null || \\\n  $KUBECTL create namespace \"$NAMESPACE\"\n\n# Install prometheus-operator bundle (CRDs + RBAC + Deployment).\n# The upstream bundle targets the default namespace — rewrite to monitoring.\ncurl -sL \"https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/${PROM_OP_VERSION}/bundle.yaml\" \\\n  | sed \"s/namespace: default/namespace: ${NAMESPACE}/g\" \\\n  | $KUBECTL apply --server-side -f - -n \"$NAMESPACE\"\n\n# Wait for the operator to be ready before creating the Prometheus CR\n$KUBECTL rollout status deployment/prometheus-operator -n \"$NAMESPACE\" --timeout=120s\n\n# Apply the Prometheus instance\n$KUBECTL apply -f \"$(dirname \"$0\")/../k8s-manifests/prometheus.yaml\"\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"llama-agents-workspace\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"pre-commit-version\": \"pnpm changeset\",\n    \"version\": \"uv run dev changeset-version\",\n    \"publish\": \"uv run dev changeset-publish --tag\"\n  },\n  \"devDependencies\": {\n    \"@changesets/cli\": \"^2.29.7\"\n  },\n  \"packageManager\": \"pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912\"\n}\n"
  },
  {
    "path": "packages/llama-agents-agentcore/CHANGELOG.md",
    "content": "# llama-agents-agentcore\n\n## 0.9.3\n\n### Patch Changes\n\n- Updated dependencies [c3fac21]\n  - llama-agents-core@0.10.2\n  - llama-agents-appserver@0.11.4\n\n## 0.9.2\n\n### Patch Changes\n\n- Updated dependencies [463c79d]\n  - llama-agents-core@0.10.1\n  - llama-agents-appserver@0.11.3\n\n## 0.9.1\n\n### Patch Changes\n\n- Updated dependencies [2280e04]\n  - llama-agents-core@0.10.0\n  - llama-agents-appserver@0.11.2\n\n## 0.9.0\n\n### Minor Changes\n\n- 5976e22: Remove the unused AgentCore memory-backed store implementation.\n\n### Patch Changes\n\n- Updated dependencies [916b157]\n  - llama-agents-appserver@0.11.1\n\n## 0.8.19\n\n### Patch Changes\n\n- Updated dependencies [facbac4]\n  - llama-agents-appserver@0.11.0\n\n## 0.8.18\n\n### Patch Changes\n\n- 7cf1c10: Remove duplicate runtime decorator\n\n## 0.8.17\n\n### Patch Changes\n\n- 44f444f: Add ServerRuntimeDecorator to chain\n- Updated dependencies [e8b8f47]\n  - llama-agents-core@0.9.0\n  - llama-agents-appserver@0.10.5\n\n## 0.8.16\n\n### Patch Changes\n\n- d4448f8: Run agentcore workflows in background tasks\n\n## 0.8.15\n\n### Patch Changes\n\n- c95ffac: Run agentcore workflows in background tasks\n\n## 0.8.14\n\n### Patch Changes\n\n- Updated dependencies [7ad3049]\n  - llama-agents-appserver@0.10.4\n  - llama-agents-core@0.8.5\n\n## 0.8.13\n\n### Patch Changes\n\n- 3850844: Support single connection mode for sqlite\n\n## 0.8.12\n\n### Patch Changes\n\n- Updated dependencies [286c91a]\n  - llama-agents-appserver@0.10.3\n\n## 0.8.11\n\n### Patch Changes\n\n- 9f52f40: Make the sqlite db more resliant to locking\n\n## 0.8.10\n\n### Patch Changes\n\n- 30ae959: Bump boto3 dep\n- 30ae959: Fix session storage mount\n\n## 0.8.9\n\n### Patch Changes\n\n- cc313f8: Bump boto3 dep\n\n## 0.8.8\n\n### Patch Changes\n\n- dde4030: feat: enable agentcore env var reading\n\n## 0.8.7\n\n### Patch Changes\n\n- Updated dependencies [f27d98f]\n  - llama-agents-core@0.8.4\n  - llama-agents-appserver@0.10.2\n\n## 0.8.6\n\n### Patch Changes\n\n- Updated dependencies [3f12660]\n  - llama-agents-core@0.8.3\n  - llama-agents-appserver@0.10.1\n\n## 0.8.5\n\n### Patch Changes\n\n- Updated dependencies [3e2e7b8]\n  - llama-agents-appserver@0.10.0\n\n## 0.8.4\n\n### Patch Changes\n\n- Updated dependencies [46f2675]\n  - llama-agents-core@0.8.2\n  - llama-agents-appserver@0.9.1\n\n## 0.8.3\n\n### Patch Changes\n\n- Updated dependencies [58e7942]\n  - llama-agents-appserver@0.9.0\n  - llama-agents-core@0.8.1\n\n## 0.8.2\n\n### Patch Changes\n\n- 68b1ec5: Use sqlite in agentcore, add local mode\n\n## 0.8.1\n\n### Patch Changes\n\n- Updated dependencies [e2f3abd]\n  - llama-agents-core@0.8.0\n  - llama-agents-appserver@0.8.1\n\n## 0.8.0\n\n### Patch Changes\n\n- 005024a: Move deployment tooling into agentcore package\n  - llama-agents-appserver@0.8.0\n\n## 0.7.2\n\n### Patch Changes\n\n- llama-agents-appserver@0.7.2\n\n## 0.7.1\n\n### Patch Changes\n\n- Updated dependencies [7bb9a90]\n  - llama-agents-core@0.7.0\n  - llama-agents-appserver@0.7.1\n\n## 0.7.0\n\n### Patch Changes\n\n- llama-agents-appserver@0.7.0\n\n## 0.6.5\n\n### Patch Changes\n\n- 669986c: Improve agent core durability performance\n  - llama-agents-appserver@0.6.5\n\n## 0.6.4\n\n### Patch Changes\n\n- llama-agents-appserver@0.6.4\n\n## 0.6.3\n\n### Patch Changes\n\n- Updated dependencies [4127101]\n- Updated dependencies [1594315]\n  - llama-agents-appserver@0.6.3\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [508b5da]\n  - llama-agents-core@0.6.2\n  - llama-agents-appserver@0.6.2\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [1b86f90]\n  - llama-agents-core@0.6.1\n  - llama-agents-appserver@0.6.1\n\n## 0.6.0\n\n### Minor Changes\n\n- 2c94c53: Add agent core memory durability, long running task support, and idle release support.\n- 4ab011f: Rename packages from llama-deploy to llama-agents.\n\n### Patch Changes\n\n- Updated dependencies [4ab011f]\n  - llama-agents-core@0.6.0\n  - llama-agents-appserver@0.6.0\n\n## 0.5.3\n\n### Patch Changes\n\n- Updated dependencies [eee29c1]\n  - llama-deploy-appserver@0.5.3\n\n## 0.5.2\n\n### Patch Changes\n\n- e11ad55: Fix version ranges\n- Updated dependencies [e11ad55]\n  - llama-deploy-appserver@0.5.2\n\n## 0.5.1\n\n### Patch Changes\n\n- llama-deploy-appserver@0.5.1\n\n## 0.5.0\n\n### Patch Changes\n\n- Updated dependencies [ac74af4]\n- Updated dependencies [4ba0d9d]\n  - llama-deploy-appserver@0.5.0\n  - llama-deploy-core@0.5.0\n"
  },
  {
    "path": "packages/llama-agents-agentcore/README.md",
    "content": "# LlamaAgents AgentCore\n\nLlamaAgents x Bedrock AgentCore deployment utilities.\n\n## Usage\n\n```bash\nllamactl agentcore run # runs the server locally\nllamactl agentcore export # exports code to a .agentcore/ directory\n```\n"
  },
  {
    "path": "packages/llama-agents-agentcore/package.json",
    "content": "{\n  \"name\": \"llama-agents-agentcore\",\n  \"version\": \"0.9.3\",\n  \"private\": false,\n  \"dependencies\": {\n    \"llama-agents-core\": \"workspace:*\",\n    \"llama-agents-appserver\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/llama-agents-agentcore/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.10.8,<0.11\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"pytest>=8.4.2\",\n  \"pytest-asyncio>=0.25.3\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"ruff>=0.15.0\",\n  \"ty>=0.0.15\"\n]\n\n[project]\nname = \"llama-agents-agentcore\"\nversion = \"0.9.3\"\ndescription = \"LlamaAgents x Bedrock AgentCore deployment utilities\"\nreadme = \"README.md\"\nrequires-python = \">=3.10,<4\"\ndependencies = [\n  \"bedrock-agentcore>=1.2.0\",\n  \"boto3>=1.42.75\",\n  \"llama-agents-core>=0.5.0\",\n  \"llama-agents-appserver>=0.5.0\",\n  \"llama-agents-server>=0.2.0\",\n  \"click>=8.3.1\"\n]\n\n[tool.uv.build-backend]\nmodule-name = \"llama_agents.agentcore\"\n\n[tool.uv.sources]\nllama-agents-core = {workspace = true}\nllama-agents-appserver = {workspace = true}\n"
  },
  {
    "path": "packages/llama-agents-agentcore/src/llama_agents/agentcore/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-agentcore/src/llama_agents/agentcore/_runtime_decorator.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Awaitable, Callable, ParamSpec, TypeVar\n\nfrom bedrock_agentcore.runtime import BedrockAgentCoreApp\nfrom llama_agents.server._runtime.server_runtime import ServerRuntimeDecorator\nfrom llama_agents.server._store.abstract_workflow_store import AbstractWorkflowStore\nfrom workflows import Workflow\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    RegisteredWorkflow,\n    Runtime,\n    WorkflowRunFunction,\n)\nfrom workflows.runtime.types.step_function import (\n    StepWorkerFunction,\n    as_step_worker_functions,\n    create_workflow_run_function,\n)\n\n_P = ParamSpec(\"_P\")\n_R = TypeVar(\"_R\")\n\n\ndef as_agentcore_async_task(\n    app: BedrockAgentCoreApp,\n    name: str,\n    fn: Callable[_P, Awaitable[_R]],\n) -> Callable[_P, Awaitable[_R]]:\n    # Opaque pass-through typed via ParamSpec so the wrapper matches whatever\n    # signature the underlying step worker exposes.\n    async def step_worker(*args: _P.args, **kwargs: _P.kwargs) -> _R:\n        task_id = app.add_async_task(name)\n        try:\n            return await fn(*args, **kwargs)\n        finally:\n            app.complete_async_task(task_id)\n\n    return step_worker\n\n\ndef as_agentcore_workflow_run(\n    app: BedrockAgentCoreApp, name: str, fn: WorkflowRunFunction\n) -> WorkflowRunFunction:\n    \"\"\"Wrap the entire workflow run as an async task.\n\n    Keeps the container alive for the full workflow duration, not just\n    individual steps.\n    \"\"\"\n\n    async def wrapper(\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        tags: dict[str, Any] | None = None,\n    ) -> StopEvent:\n        task_id = app.add_async_task(f\"{name}.run\")\n        try:\n            return await fn(init_state, start_event, tags)\n        finally:\n            app.complete_async_task(task_id)\n\n    return wrapper\n\n\nclass AgentCoreRuntimeDecorator(ServerRuntimeDecorator):\n    def __init__(\n        self,\n        decorated: Runtime,\n        store: AbstractWorkflowStore,\n        app: BedrockAgentCoreApp,\n        *,\n        persistence_backoff: list[float] | None = None,\n    ) -> None:\n        super().__init__(decorated, store, persistence_backoff=persistence_backoff)\n        self.app = app\n        self._tracked: dict[str, Workflow] = {}\n        self._registered: dict[int, RegisteredWorkflow] = {}\n\n    def register(self, workflow: Workflow) -> RegisteredWorkflow:\n        name = workflow.workflow_name\n        if name not in self._tracked:\n            self._tracked[name] = workflow\n        wrapped_steps: dict[str, StepWorkerFunction] = {\n            step_name: as_agentcore_async_task(self.app, f\"{name}.{step_name}\", step)\n            for step_name, step in as_step_worker_functions(workflow).items()\n        }\n        run_fn = create_workflow_run_function(workflow)\n        registered = RegisteredWorkflow(\n            steps=wrapped_steps,\n            workflow_run_fn=as_agentcore_workflow_run(self.app, name, run_fn),\n            workflow=workflow,\n        )\n        id_ = id(workflow)\n        self._registered[id_] = registered\n        return registered\n\n    def track_workflow(self, workflow: Workflow) -> None:\n        self._tracked[workflow.workflow_name] = workflow\n        super().track_workflow(workflow)\n\n    def untrack_workflow(self, workflow: Workflow) -> None:\n        self._tracked.pop(workflow.workflow_name, None)\n        super().untrack_workflow(workflow)\n\n    def get_registered(self, workflow: Workflow) -> RegisteredWorkflow | None:\n        return self._registered.get(id(workflow))\n\n    def get_workflow(self, name: str) -> Workflow | None:\n        return self._tracked.get(name)\n\n    def get_workflow_names(self) -> list[str]:\n        return list(self._tracked.keys())\n"
  },
  {
    "path": "packages/llama-agents-agentcore/src/llama_agents/agentcore/_service.py",
    "content": "from bedrock_agentcore.runtime import BedrockAgentCoreApp\nfrom llama_agents.client import HandlerData\nfrom llama_agents.server._runtime.idle_release_runtime import IdleReleaseDecorator\nfrom llama_agents.server._runtime.persistence_runtime import PersistenceDecorator\nfrom llama_agents.server._service import _WorkflowService as WorkflowService\nfrom llama_agents.server._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    StoredEvent,\n)\nfrom workflows import Workflow\nfrom workflows.events import Event, StartEvent\nfrom workflows.plugins.basic import basic_runtime\nfrom workflows.runtime.types.plugin import Runtime\nfrom workflows.utils import _nanoid\n\nfrom ._runtime_decorator import AgentCoreRuntimeDecorator\n\nIDLE_TIMEOUT = 60.0\nPERSISTENCE_BACKOFF = [0.5, 3]\nPOLLING_TIMEOUT = 600\n\n\nclass WorkflowNotFoundError(Exception):\n    \"\"\"\n    Raise when a workflow with a given name cannot be found within the registered workflows.\n    \"\"\"\n\n    def __init__(self, workflow_name: str) -> None:\n        self.workflow_name = workflow_name\n\n    def __repr__(self) -> str:\n        return f\"Could not find {self.workflow_name} among the registered workflows\"\n\n    def __str__(self) -> str:\n        return f\"Could not find {self.workflow_name} among the registered workflows\"\n\n\nclass AgentCoreService:\n    def __init__(\n        self,\n        app: BedrockAgentCoreApp,\n        store: AbstractWorkflowStore,\n        idle_timeout: float = IDLE_TIMEOUT,\n        persistence_backoff: list[float] = PERSISTENCE_BACKOFF,\n    ) -> None:\n        self._workflow_store = store\n        inner: Runtime = IdleReleaseDecorator(\n            PersistenceDecorator(basic_runtime, store=self._workflow_store),\n            store=self._workflow_store,\n            idle_timeout=idle_timeout,\n        )\n        self._runtime: AgentCoreRuntimeDecorator = AgentCoreRuntimeDecorator(\n            decorated=inner,\n            store=self._workflow_store,\n            app=app,\n            persistence_backoff=persistence_backoff,\n        )\n        self._service = WorkflowService(\n            runtime=self._runtime, store=self._workflow_store\n        )\n\n    def add_workflow(self, workflow_name: str, workflow: Workflow) -> None:\n        workflow._switch_workflow_name(workflow_name)\n        workflow._switch_runtime(self._runtime)\n\n    def get_workflow(self, workflow_name: str) -> Workflow | None:\n        \"\"\"Get a registered workflow by name.\"\"\"\n        return self._runtime.get_workflow(workflow_name)\n\n    def get_workflow_names(self) -> list[str]:\n        \"\"\"Get all registered workflow names.\"\"\"\n        return self._runtime.get_workflow_names()\n\n    async def run_workflow(\n        self, workflow_name: str, start_event: StartEvent\n    ) -> HandlerData:\n        wf = self._service.get_workflow(workflow_name)\n        if wf is not None:\n            handler_data = await self._service.start_workflow(\n                wf, start_event=start_event, handler_id=_nanoid()\n            )\n            return await self._service.await_workflow(handler_data)\n\n        raise WorkflowNotFoundError(workflow_name)\n\n    async def run_workflow_with_session(\n        self,\n        workflow_name: str,\n        start_event: StartEvent,\n        handler_id: str,\n        *,\n        nowait: bool = False,\n    ) -> HandlerData:\n        \"\"\"Run a workflow using a specific handler_id (typically the session ID).\n\n        If a handler already exists for this ID:\n        - completed → return cached result\n        - running   → await it (sync) or return current status (nowait)\n        - failed/cancelled → start fresh\n\n        This makes invocations idempotent within a session.\n        \"\"\"\n        # Check for existing handler\n        existing = await self._service.load_handler(handler_id)\n        if existing is not None:\n            status = existing.status\n            if status == \"completed\":\n                return existing\n            if status == \"running\":\n                if nowait:\n                    return existing\n                return await self._service.await_workflow(existing)\n            # failed/cancelled → fall through to start fresh\n\n        wf = self._service.get_workflow(workflow_name)\n        if wf is None:\n            raise WorkflowNotFoundError(workflow_name)\n\n        handler_data = await self._service.start_workflow(\n            wf, start_event=start_event, handler_id=handler_id\n        )\n        if nowait:\n            return handler_data\n        return await self._service.await_workflow(handler_data)\n\n    async def get_handler(self, handler_id: str) -> HandlerData | None:\n        \"\"\"Get handler status and result by ID.\"\"\"\n        return await self._service.load_handler(handler_id)\n\n    async def query_handlers(self, query: HandlerQuery) -> list:\n        \"\"\"Query handlers with optional filters.\"\"\"\n        return await self._service.query_handlers(query)\n\n    async def get_events(\n        self,\n        handler_id: str,\n        after_sequence: int | None = None,\n        limit: int | None = None,\n    ) -> list[StoredEvent]:\n        \"\"\"Get recorded events for a handler.\"\"\"\n        handler = await self._service.load_handler(handler_id)\n        if handler is None:\n            return []\n        run_id = getattr(handler, \"run_id\", None)\n        if not run_id:\n            return []\n        return await self._workflow_store.query_events(\n            run_id, after_sequence=after_sequence, limit=limit\n        )\n\n    async def send_event(\n        self, handler_id: str, event: Event, step: str | None = None\n    ) -> None:\n        \"\"\"Send an event into a running workflow (human-in-the-loop).\"\"\"\n        await self._service.send_event(handler_id, event, step=step)\n\n    async def cancel_handler(self, handler_id: str, purge: bool = False) -> str | None:\n        \"\"\"Cancel a running workflow handler.\"\"\"\n        return await self._service.cancel_handler(handler_id, purge=purge)\n"
  },
  {
    "path": "packages/llama-agents-agentcore/src/llama_agents/agentcore/deploy.py",
    "content": "\"\"\"Deploy LlamaIndex Workflows to AWS Bedrock AgentCore Runtime.\n\nProvides a high-level ``AgentCoreDeployer`` that handles the full lifecycle:\nbuild a container via CodeBuild, push to ECR, create/update an AgentCore\nRuntime, invoke it, and tear it down.\n\nExample::\n\n    import boto3\n    from llama_agents.agentcore.deploy import AgentCoreDeployer\n\n    deployer = AgentCoreDeployer(\n        session=boto3.Session(region_name=\"us-east-1\"),\n        deployment_role=\"arn:aws:iam::123456789012:role/AgentCoreDeployRole\",\n        execution_role=\"arn:aws:iam::123456789012:role/AgentCoreExecutionRole\",\n    )\n\n    # Build, push, and deploy\n    runtime = deployer.deploy(project_dir=\".\")\n\n    # Invoke\n    result = deployer.invoke(runtime.arn, {\"input\": \"Hello!\"})\n\n    # Clean up\n    deployer.destroy(runtime.name)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nimport random\nimport re\nimport string\nimport tempfile\nimport time\nimport uuid\nimport zipfile\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Any\n\nlogger = logging.getLogger(__name__)\n\ntry:\n    import boto3\n    from botocore.config import Config as BotoConfig\n    from botocore.exceptions import ClientError\nexcept ImportError as e:\n    raise ImportError(\n        \"boto3 is required for deployment. Install with: \"\n        \"pip install 'llama-agents-agentcore[deploy]'\"\n    ) from e\n\n\n# ── Data classes ──────────────────────────────────────────────────────────\n\n\n@dataclass\nclass DeployedRuntime:\n    \"\"\"Metadata for a deployed AgentCore Runtime.\"\"\"\n\n    name: str\n    arn: str\n    runtime_id: str\n    ecr_image: str\n    region: str\n    account_id: str\n    repository_name: str\n\n    def to_dict(self) -> dict[str, str]:\n        \"\"\"Serialize to a dict for JSON persistence.\"\"\"\n        return {\n            \"name\": self.name,\n            \"arn\": self.arn,\n            \"runtime_id\": self.runtime_id,\n            \"ecr_image\": self.ecr_image,\n            \"region\": self.region,\n            \"account_id\": self.account_id,\n            \"repository_name\": self.repository_name,\n        }\n\n    @classmethod\n    def from_dict(cls, data: dict[str, str]) -> DeployedRuntime:\n        \"\"\"Deserialize from a dict.\"\"\"\n        return cls(**data)\n\n\n@dataclass\nclass DeployConfig:\n    \"\"\"Configuration for a deployment.\"\"\"\n\n    runtime_name: str | None = None\n    env_vars: dict[str, str] = field(default_factory=dict)\n    max_lifetime: int = 3600\n    build_compute_type: str = \"BUILD_GENERAL1_SMALL\"\n    source_files: list[str] | None = None\n\n\n# ── Deployer ──────────────────────────────────────────────────────────────\n\n\nclass AgentCoreDeployer:\n    \"\"\"High-level deployer for LlamaIndex Workflows on AWS Bedrock AgentCore.\n\n    Handles the full lifecycle: container build (CodeBuild), image push (ECR),\n    runtime create/update, invocation, and teardown.\n\n    Args:\n        session: A configured ``boto3.Session``.\n        deployment_role: IAM role ARN for CodeBuild to build/push images.\n        execution_role: IAM role ARN for the AgentCore Runtime at runtime.\n    \"\"\"\n\n    def __init__(\n        self,\n        session: boto3.Session,\n        deployment_role: str,\n        execution_role: str,\n    ) -> None:\n        self._session = session\n        self._deployment_role = deployment_role\n        self._execution_role = execution_role\n        self._region = session.region_name or \"us-east-1\"\n        self._account_id: str | None = None\n\n    @property\n    def account_id(self) -> str:\n        \"\"\"AWS account ID (resolved lazily).\"\"\"\n        if self._account_id is None:\n            self._account_id = str(\n                self._session.client(\"sts\").get_caller_identity()[\"Account\"]\n            )\n        return self._account_id\n\n    # ── Public API ────────────────────────────────────────────────────────\n\n    def deploy(\n        self,\n        project_dir: str | Path = \".\",\n        config: DeployConfig | None = None,\n    ) -> DeployedRuntime:\n        \"\"\"Build, push, and deploy a workflow project to AgentCore.\n\n        Args:\n            project_dir: Path to the project root (must contain pyproject.toml).\n            config: Optional deployment configuration overrides.\n\n        Returns:\n            A ``DeployedRuntime`` with all metadata needed for invoke/destroy.\n        \"\"\"\n        project_dir = Path(project_dir).resolve()\n        config = config or DeployConfig()\n\n        if not (project_dir / \"pyproject.toml\").exists():\n            raise FileNotFoundError(\n                f\"No pyproject.toml found in {project_dir}. \"\n                \"This file is required for workflow discovery.\"\n            )\n\n        # Derive names\n        project_name = config.runtime_name or _project_name_from_pyproject(project_dir)\n        safe_name = _sanitize_name(project_name)\n        repository_name = f\"bedrock-agentcore-{safe_name}\"\n        runtime_name = f\"{safe_name}_runtime\"\n\n        logger.info(\"Deploying '%s' to AgentCore in %s\", project_name, self._region)\n\n        # Step 1: Build and push container image\n        s3_bucket = f\"llamactl-agentcore-{self.account_id}-{self._region}\"\n        self._ensure_s3_bucket(s3_bucket)\n\n        ecr_image = self._build_and_push(\n            project_dir=project_dir,\n            repository_name=repository_name,\n            s3_bucket=s3_bucket,\n            source_files=config.source_files,\n            build_compute_type=config.build_compute_type,\n        )\n\n        # Step 2: Merge env vars from DeploymentConfig (pyproject.toml) with\n        # explicit overrides from DeployConfig so they're set at container level.\n        deployment_env = _parse_deployment_env_vars(project_dir)\n        env_vars = {\n            \"AWS_DEFAULT_REGION\": self._region,\n            **deployment_env,\n            **config.env_vars,\n        }\n        runtime_id, runtime_arn = self._deploy_runtime(\n            runtime_name=runtime_name,\n            ecr_uri=ecr_image,\n            env_vars=env_vars,\n            max_lifetime=config.max_lifetime,\n        )\n\n        result = DeployedRuntime(\n            name=runtime_name,\n            arn=runtime_arn,\n            runtime_id=runtime_id,\n            ecr_image=ecr_image,\n            region=self._region,\n            account_id=self.account_id,\n            repository_name=repository_name,\n        )\n\n        logger.info(\"Deployment complete: %s\", runtime_arn)\n        return result\n\n    def invoke(\n        self,\n        runtime_arn: str,\n        payload: dict[str, Any],\n        session_id: str | None = None,\n    ) -> dict[str, Any]:\n        \"\"\"Invoke a deployed AgentCore Runtime.\n\n        Args:\n            runtime_arn: The Runtime ARN from ``deploy()``.\n            payload: JSON-serializable payload. Typically includes\n                ``{\"input\": \"...\"}`` and optionally ``{\"workflow\": \"name\"}``.\n            session_id: Optional session ID for continuing a previous session.\n                If not provided, a new random session ID is generated.\n\n        Returns:\n            The workflow result as a dict.\n        \"\"\"\n        if session_id is None:\n            session_id = str(uuid.uuid4())\n\n        client = self._session.client(\"bedrock-agentcore\")\n\n        response = client.invoke_agent_runtime(\n            agentRuntimeArn=runtime_arn,\n            qualifier=\"DEFAULT\",\n            runtimeSessionId=session_id,\n            payload=json.dumps(payload).encode(),\n        )\n\n        chunks: list[str] = []\n        for chunk in response.get(\"response\", []):\n            chunks.append(chunk.decode(\"utf-8\"))\n\n        return json.loads(\"\".join(chunks))\n\n    def destroy(\n        self,\n        runtime_name: str,\n        repository_name: str | None = None,\n    ) -> None:\n        \"\"\"Delete an AgentCore Runtime and optionally its ECR repository.\n\n        Args:\n            runtime_name: The runtime name from ``deploy()``.\n            repository_name: ECR repository to delete. If None, only the\n                runtime is deleted.\n        \"\"\"\n        control = self._session.client(\"bedrock-agentcore-control\")\n\n        runtime = self._find_runtime(control, runtime_name)\n        if runtime:\n            logger.info(\"Deleting runtime: %s\", runtime_name)\n            control.delete_agent_runtime(agentRuntimeId=runtime[\"agentRuntimeId\"])\n            self._wait_for_deletion(control, runtime[\"agentRuntimeId\"])\n        else:\n            logger.info(\"Runtime '%s' not found (may already be deleted)\", runtime_name)\n\n        if repository_name:\n            ecr = self._session.client(\"ecr\")\n            try:\n                ecr.delete_repository(repositoryName=repository_name, force=True)\n                logger.info(\"Deleted ECR repository: %s\", repository_name)\n            except ecr.exceptions.RepositoryNotFoundException:\n                logger.info(\"ECR repository '%s' already deleted\", repository_name)\n\n    def destroy_from_metadata(self, runtime: DeployedRuntime) -> None:\n        \"\"\"Convenience: destroy using a ``DeployedRuntime`` object.\"\"\"\n        self.destroy(runtime.name, runtime.repository_name)\n\n    # ── Container Build (CodeBuild + ECR) ─────────────────────────────────\n\n    def _build_and_push(\n        self,\n        project_dir: Path,\n        repository_name: str,\n        s3_bucket: str,\n        source_files: list[str] | None = None,\n        build_compute_type: str = \"BUILD_GENERAL1_SMALL\",\n    ) -> str:\n        \"\"\"Build an ARM64 container via CodeBuild and push to ECR.\n\n        Returns the ECR image URI.\n        \"\"\"\n        ecr = self._session.client(\"ecr\")\n        ecr_registry = f\"{self.account_id}.dkr.ecr.{self._region}.amazonaws.com\"\n        ecr_image = f\"{ecr_registry}/{repository_name}:latest\"\n\n        # Ensure ECR repo\n        try:\n            ecr.create_repository(repositoryName=repository_name)\n            logger.info(\"Created ECR repository: %s\", repository_name)\n        except ecr.exceptions.RepositoryAlreadyExistsException:\n            logger.info(\"Using existing ECR repository: %s\", repository_name)\n\n        # Create source zip\n        suffix = \"\".join(random.choices(string.ascii_letters, k=16))\n        s3_key = f\"codebuild-{suffix}.zip\"\n\n        with tempfile.TemporaryFile() as tmp:\n            self._create_source_zip(\n                tmp,\n                project_dir,\n                ecr_image,\n                repository_name,\n                ecr_registry,\n                source_files,\n            )\n            tmp.seek(0)\n            self._session.client(\"s3\").upload_fileobj(tmp, s3_bucket, s3_key)\n\n        logger.info(\"Uploaded source to s3://%s/%s\", s3_bucket, s3_key)\n\n        # Create and run CodeBuild project\n        project_name = f\"llama-build-{suffix}\"\n        codebuild = self._session.client(\"codebuild\")\n\n        codebuild.create_project(\n            name=project_name,\n            source={\"type\": \"S3\", \"location\": f\"{s3_bucket}/{s3_key}\"},\n            artifacts={\"type\": \"NO_ARTIFACTS\"},\n            environment={\n                \"type\": \"ARM_CONTAINER\",\n                \"image\": \"aws/codebuild/amazonlinux2-aarch64-standard:3.0\",\n                \"computeType\": build_compute_type,\n                \"privilegedMode\": True,\n            },\n            serviceRole=self._deployment_role,\n        )\n\n        build_id = codebuild.start_build(projectName=project_name)[\"build\"][\"id\"]\n        logger.info(\"Started CodeBuild: %s\", project_name)\n\n        status = self._wait_for_build(build_id)\n\n        # Clean up temporary build resources\n        codebuild.delete_project(name=project_name)\n        self._session.client(\"s3\").delete_object(Bucket=s3_bucket, Key=s3_key)\n\n        if status != \"SUCCEEDED\":\n            raise RuntimeError(f\"CodeBuild failed with status: {status}\")\n\n        logger.info(\"Image pushed: %s\", ecr_image)\n        return ecr_image\n\n    def _create_source_zip(\n        self,\n        fileobj: Any,\n        project_dir: Path,\n        ecr_image: str,\n        repository_name: str,\n        ecr_registry: str,\n        source_files: list[str] | None,\n    ) -> None:\n        \"\"\"Create a zip with source code, Dockerfile, and buildspec.\"\"\"\n        with zipfile.ZipFile(fileobj, \"w\") as zf:\n            # Dockerfile\n            zf.writestr(\"Dockerfile\", _generate_dockerfile())\n\n            # requirements.txt from pyproject.toml\n            zf.writestr(\"requirements.txt\", _generate_requirements(project_dir))\n\n            # Source files — include everything except venv, __pycache__, .git\n            if source_files:\n                for name in source_files:\n                    path = project_dir / name\n                    if path.exists():\n                        zf.write(path, name)\n            else:\n                for path in project_dir.rglob(\"*\"):\n                    if not path.is_file():\n                        continue\n                    rel = path.relative_to(project_dir)\n                    parts = rel.parts\n                    if any(\n                        p\n                        in (\n                            \"__pycache__\",\n                            \".venv\",\n                            \".git\",\n                            \".agentcore\",\n                            \"node_modules\",\n                        )\n                        for p in parts\n                    ):\n                        continue\n                    zf.write(path, str(rel))\n\n            # buildspec.yml\n            zf.writestr(\n                \"buildspec.yml\",\n                _generate_buildspec(\n                    ecr_registry,\n                    repository_name,\n                    ecr_image,\n                    self._region,\n                ),\n            )\n\n    def _wait_for_build(self, build_id: str, poll_interval: int = 10) -> str:\n        \"\"\"Wait for CodeBuild to complete, streaming log output.\"\"\"\n        codebuild = self._session.client(\"codebuild\")\n        logs_client = self._session.client(\n            \"logs\", config=BotoConfig(retries={\"max_attempts\": 15})\n        )\n\n        next_token: str | None = None\n\n        while True:\n            info = codebuild.batch_get_builds(ids=[build_id])[\"builds\"][0]\n            status = info[\"buildStatus\"]\n            log_group = info[\"logs\"].get(\"groupName\")\n            stream_name = info[\"logs\"].get(\"streamName\")\n\n            # Stream available logs\n            if log_group and stream_name:\n                try:\n                    kwargs: dict[str, Any] = {\n                        \"logGroupName\": log_group,\n                        \"logStreamName\": stream_name,\n                        \"startFromHead\": True,\n                    }\n                    if next_token:\n                        kwargs[\"nextToken\"] = next_token\n                    resp = logs_client.get_log_events(**kwargs)\n                    for event in resp[\"events\"]:\n                        logger.info(\"[build] %s\", event[\"message\"].rstrip())\n                    next_token = resp[\"nextForwardToken\"]\n                except Exception:\n                    pass\n\n            if status != \"IN_PROGRESS\":\n                return status\n\n            time.sleep(poll_interval)\n\n    # ── AgentCore Runtime CRUD ────────────────────────────────────────────\n\n    def _deploy_runtime(\n        self,\n        runtime_name: str,\n        ecr_uri: str,\n        env_vars: dict[str, str],\n        max_lifetime: int = 3600,\n    ) -> tuple[str, str]:\n        \"\"\"Create or update an AgentCore Runtime. Returns (runtime_id, runtime_arn).\"\"\"\n        client = self._session.client(\"bedrock-agentcore-control\")\n\n        runtime_config: dict[str, Any] = {\n            \"roleArn\": self._execution_role,\n            \"agentRuntimeArtifact\": {\n                \"containerConfiguration\": {\"containerUri\": ecr_uri}\n            },\n            \"networkConfiguration\": {\"networkMode\": \"PUBLIC\"},\n            \"environmentVariables\": env_vars,\n            \"lifecycleConfiguration\": {\"maxLifetime\": max_lifetime},\n            \"filesystemConfigurations\": [\n                {\"sessionStorage\": {\"mountPath\": \"/mnt/workspace\"}}\n            ],\n        }\n\n        existing = self._find_runtime(client, runtime_name)\n\n        if existing:\n            logger.info(\"Updating existing runtime: %s\", runtime_name)\n            client.update_agent_runtime(\n                agentRuntimeId=existing[\"agentRuntimeId\"],\n                **runtime_config,\n            )\n            runtime_id = existing[\"agentRuntimeId\"]\n            runtime_arn = existing[\"agentRuntimeArn\"]\n        else:\n            logger.info(\"Creating new runtime: %s\", runtime_name)\n            resp = client.create_agent_runtime(\n                agentRuntimeName=runtime_name,\n                **runtime_config,\n            )\n            runtime_id = resp[\"agentRuntimeId\"]\n            runtime_arn = resp[\"agentRuntimeArn\"]\n\n        self._wait_for_ready(client, runtime_id)\n        return runtime_id, runtime_arn\n\n    @staticmethod\n    def _find_runtime(client: Any, runtime_name: str) -> dict[str, Any] | None:\n        \"\"\"Find an existing runtime by name.\"\"\"\n        try:\n            for rt in client.list_agent_runtimes().get(\"agentRuntimes\", []):\n                if rt[\"agentRuntimeName\"] == runtime_name:\n                    return rt\n        except Exception as e:\n            logger.warning(\"Could not list runtimes: %s\", e)\n        return None\n\n    @staticmethod\n    def _wait_for_ready(client: Any, runtime_id: str) -> None:\n        \"\"\"Poll until the runtime reaches READY status.\"\"\"\n        logger.info(\"Waiting for runtime to be ready...\")\n        while True:\n            resp = client.get_agent_runtime(agentRuntimeId=runtime_id)\n            status = resp[\"status\"]\n            if status == \"READY\":\n                logger.info(\"Runtime is ready!\")\n                return\n            if status in (\"CREATE_FAILED\", \"UPDATE_FAILED\", \"DELETE_FAILED\"):\n                raise RuntimeError(f\"Runtime failed with status: {status}\")\n            logger.info(\"  Status: %s\", status)\n            time.sleep(10)\n\n    @staticmethod\n    def _wait_for_deletion(client: Any, runtime_id: str) -> None:\n        \"\"\"Poll until the runtime is deleted.\"\"\"\n        while True:\n            try:\n                resp = client.get_agent_runtime(agentRuntimeId=runtime_id)\n                status = resp[\"status\"]\n                if status == \"DELETE_FAILED\":\n                    logger.warning(\"Deletion failed with status: %s\", status)\n                    return\n                logger.info(\"  Status: %s\", status)\n                time.sleep(5)\n            except client.exceptions.ResourceNotFoundException:\n                logger.info(\"Runtime deleted.\")\n                return\n\n    # ── S3 bucket ─────────────────────────────────────────────────────────\n\n    def _ensure_s3_bucket(self, bucket_name: str) -> None:\n        \"\"\"Create S3 bucket for CodeBuild artifacts if it doesn't exist.\"\"\"\n        s3 = self._session.client(\"s3\")\n        try:\n            s3.head_bucket(Bucket=bucket_name)\n        except ClientError:\n            kwargs: dict[str, Any] = {\"Bucket\": bucket_name}\n            if self._region != \"us-east-1\":\n                kwargs[\"CreateBucketConfiguration\"] = {\n                    \"LocationConstraint\": self._region\n                }\n            s3.create_bucket(**kwargs)\n            logger.info(\"Created S3 bucket: %s\", bucket_name)\n\n\n# ── Helpers ───────────────────────────────────────────────────────────────\n\n\ndef _sanitize_name(name: str) -> str:\n    \"\"\"Sanitize for AgentCore (underscores only, no hyphens).\"\"\"\n    return re.sub(r\"[^a-zA-Z0-9]\", \"_\", name)\n\n\ndef _project_name_from_pyproject(project_dir: Path) -> str:\n    \"\"\"Read the project name from pyproject.toml.\"\"\"\n    try:\n        import tomllib  # type: ignore\n    except ModuleNotFoundError:\n        import tomli as tomllib\n\n    text = (project_dir / \"pyproject.toml\").read_text()\n    data = tomllib.loads(text)\n\n    # Try [tool.llamadeploy].name first, then [project].name\n    name = data.get(\"tool\", {}).get(\"llamadeploy\", {}).get(\"name\") or data.get(\n        \"project\", {}\n    ).get(\"name\")\n    if not name:\n        raise ValueError(\"Could not determine project name from pyproject.toml\")\n    return name\n\n\ndef _generate_dockerfile() -> str:\n    \"\"\"Generate a minimal Dockerfile for AgentCore (ARM64).\"\"\"\n    return \"\"\"\\\nFROM public.ecr.aws/docker/library/python:3.12-slim-bookworm\n\nWORKDIR /app\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\n\nENV PYTHONUNBUFFERED=1\nENV PYTHONPATH=/app/src:/app\n\n# llama-agents-agentcore discovers workflows from pyproject.toml at startup\nCMD [\"python\", \"-m\", \"llama_agents.agentcore.main\", \"--run\"]\n\"\"\"\n\n\ndef _generate_requirements(project_dir: Path) -> str:\n    \"\"\"Extract dependencies from pyproject.toml for the container.\"\"\"\n    try:\n        import tomllib  # type: ignore\n    except ModuleNotFoundError:\n        import tomli as tomllib\n\n    text = (project_dir / \"pyproject.toml\").read_text()\n    data = tomllib.loads(text)\n    deps = data.get(\"project\", {}).get(\"dependencies\", [])\n\n    # Always include the agentcore package itself\n    lines = list(deps)\n    if not any(\"llama-agents-agentcore\" in d for d in lines):\n        lines.append(\"llama-agents-agentcore>=0.7.0\")\n\n    return \"\\n\".join(lines) + \"\\n\"\n\n\ndef _parse_deployment_env_vars(project_dir: Path) -> dict[str, str]:\n    \"\"\"Read env / env_files from the project's DeploymentConfig (pyproject.toml).\"\"\"\n    try:\n        from llama_agents.appserver.workflow_loader import parse_environment_variables\n        from llama_agents.core.deployment_config import (\n            read_deployment_config_from_git_root_or_cwd,\n        )\n\n        config = read_deployment_config_from_git_root_or_cwd(project_dir, project_dir)\n        return parse_environment_variables(config, project_dir)\n    except Exception:\n        return {}\n\n\ndef _generate_buildspec(\n    ecr_registry: str,\n    repository_name: str,\n    ecr_image: str,\n    region: str,\n) -> str:\n    \"\"\"Generate CodeBuild buildspec.yml.\"\"\"\n    return f\"\"\"\\\nversion: 0.2\nphases:\n  pre_build:\n    commands:\n      - echo Logging in to Amazon ECR...\n      - aws ecr get-login-password --region {region} | docker login --username AWS --password-stdin {ecr_registry}\n  build:\n    commands:\n      - echo Building the Docker image...\n      - docker build -t {repository_name}:latest .\n      - docker tag {repository_name}:latest {ecr_image}\n  post_build:\n    commands:\n      - echo Pushing the Docker image...\n      - docker push {ecr_image}\n\"\"\"\n"
  },
  {
    "path": "packages/llama-agents-agentcore/src/llama_agents/agentcore/entrypoint.py",
    "content": "# SPDX-License-Identifier: MIT\n\"\"\"AgentCore entrypoint for LlamaIndex Workflows.\n\nWraps workflows for AWS Bedrock AgentCore Runtime using BedrockAgentCoreApp.\n\nSession Integration\n-------------------\n``context.session_id`` is used as the **default handler_id** for all workflow\noperations.  This gives a natural 1:1 mapping between AgentCore sessions and\nworkflow handlers — re-invoking the same session returns the cached result\n(completed), awaits (running), or starts fresh (failed/cancelled).\n\nAction-Based Routing\n--------------------\nThe entrypoint supports an ``\"action\"`` key in the payload to expose the full\nWorkflowServer capabilities over a single AgentCore invoke channel:\n\n    run          — run synchronously (default when action is omitted)\n    run_nowait   — start without waiting, return handler_id\n    get_result   — poll handler status and result\n    get_events   — retrieve recorded workflow events\n    send_event   — inject event into running workflow (human-in-the-loop)\n    cancel       — cancel a running handler\n    list_workflows — list registered workflow names\n    list_handlers  — list handlers (filter by workflow/status)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport logging\nimport traceback\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Any, Literal\n\nfrom bedrock_agentcore import BedrockAgentCoreApp\nfrom llama_agents.appserver.workflow_loader import (\n    load_environment_variables,\n    load_workflows,\n    validate_required_env_vars,\n)\nfrom llama_agents.client import HandlerData\nfrom llama_agents.core.deployment_config import (\n    read_deployment_config_from_git_root_or_cwd,\n)\nfrom llama_agents.server._store.abstract_workflow_store import (\n    HandlerQuery,\n)\nfrom llama_agents.server._store.sqlite.sqlite_workflow_store import (\n    SqliteWorkflowStore,\n)\nfrom pydantic import BaseModel, ValidationError\nfrom workflows import Workflow\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.events import Event, StartEvent\n\nfrom ._service import AgentCoreService\n\nlogger = logging.getLogger(__name__)\napp = BedrockAgentCoreApp()\n\nAGENTCORE_HOST = \"0.0.0.0\"\nAGENTCORE_PORT = 8080\n\n\n# ---------------------------------------------------------------------------\n# Response models\n# ---------------------------------------------------------------------------\n\n\nclass WorkflowResult(BaseModel):\n    \"\"\"Response for a single workflow run.\"\"\"\n\n    workflow: str\n    status: Literal[\"completed\", \"failed\"]\n    result: str | None = None\n    error: str | None = None\n    session_id: str | None = None\n\n\nclass HandlerResult(BaseModel):\n    \"\"\"Response for handler-level operations.\"\"\"\n\n    handler_id: str\n    session_id: str | None = None\n    workflow_name: str | None = None\n    run_id: str | None = None\n    status: str | None = None\n    result: str | None = None\n    error: str | None = None\n    started_at: str | None = None\n    updated_at: str | None = None\n    completed_at: str | None = None\n\n\n# ---------------------------------------------------------------------------\n# Workflow loading\n# ---------------------------------------------------------------------------\n\n\n@functools.lru_cache(maxsize=1)\ndef _load_workflows() -> tuple[dict[str, Workflow], str, str | None]:\n    config_dir = Path.cwd()\n    if not (config_dir / \"pyproject.toml\").exists():\n        raise FileNotFoundError(\n            \"No pyproject.toml found at \"\n            f\"{config_dir}.\\n\"\n            \"Add a pyproject.toml to your project and re-run.\"\n        )\n    config = read_deployment_config_from_git_root_or_cwd(\n        Path.cwd(), config_dir\n    )  # let errors bubble up if misconfigured\n\n    # Load env vars from the deployment config (env, env_files) before\n    # importing workflow modules — mirrors appserver behaviour.\n    load_environment_variables(config, config_dir)\n    validate_required_env_vars(config)\n\n    workflows = load_workflows(config)\n    has_default = any(key == \"default\" for key in list(workflows.keys()))\n    default_workflow = \"default\" if has_default else next(iter(workflows))\n    file_workflow = None\n    for name, wf in workflows.items():\n        if wf.start_event_class.__name__ == \"FileEvent\":\n            file_workflow = name\n            break\n    return workflows, default_workflow, file_workflow\n\n\nAGENTCORE_WORKSPACE = \"/mnt/workspace\"\nSQLITE_DB_NAME = \"workflows.db\"\n\n\ndef _get_sqlite_db_path() -> str:\n    \"\"\"Return the SQLite DB path, preferring AgentCore session storage.\"\"\"\n    workspace = Path(AGENTCORE_WORKSPACE)\n    if workspace.exists() and workspace.is_dir():\n        return str(workspace / SQLITE_DB_NAME)\n    return SQLITE_DB_NAME\n\n\n@functools.lru_cache(maxsize=1)\ndef get_agentcore_service() -> AgentCoreService:\n    workflows, _, _ = _load_workflows()\n    db_path = _get_sqlite_db_path()\n    store = SqliteWorkflowStore(db_path=db_path, single_connection=True)\n    logger.info(\"Using SQLite workflow store at %s\", db_path)\n    service = AgentCoreService(app=app, store=store)\n    for k in workflows:\n        service.add_workflow(k, workflows[k])\n    return service\n\n\n# ---------------------------------------------------------------------------\n# Serialisation helpers\n# ---------------------------------------------------------------------------\n\n_json_serializer = JsonSerializer()\n\n\ndef _dt_str(dt: Any) -> str | None:\n    if dt is None:\n        return None\n    if isinstance(dt, datetime):\n        return dt.isoformat()\n    return str(dt)\n\n\ndef _handler_to_result(\n    handler: HandlerData, session_id: str | None = None\n) -> dict[str, Any]:\n    \"\"\"Convert HandlerData to a JSON-safe response dict.\"\"\"\n    result = handler.result\n    if result is not None:\n        result = _json_serializer.serialize(result)\n\n    return HandlerResult(\n        handler_id=handler.handler_id,\n        session_id=session_id,\n        workflow_name=getattr(handler, \"workflow_name\", None),\n        run_id=getattr(handler, \"run_id\", None),\n        status=handler.status,\n        result=result,\n        error=getattr(handler, \"error\", None),\n        started_at=_dt_str(getattr(handler, \"started_at\", None)),\n        updated_at=_dt_str(getattr(handler, \"updated_at\", None)),\n        completed_at=_dt_str(getattr(handler, \"completed_at\", None)),\n    ).model_dump()\n\n\ndef _serialize_event(stored_event: Any) -> dict[str, Any]:\n    \"\"\"Convert a StoredEvent to a JSON-safe dict.\"\"\"\n    envelope = stored_event.event\n    return {\n        \"sequence\": stored_event.sequence,\n        \"timestamp\": _dt_str(stored_event.timestamp),\n        \"value\": envelope.value if hasattr(envelope, \"value\") else None,\n        \"type\": getattr(envelope, \"type\", None),\n        \"qualified_name\": getattr(envelope, \"qualified_name\", None),\n    }\n\n\n# ---------------------------------------------------------------------------\n# Session / handler ID resolution\n# ---------------------------------------------------------------------------\n\n\ndef _resolve_handler_id(payload: dict[str, Any], session_id: str) -> str:\n    \"\"\"Determine the handler_id to use.\n\n    Priority:\n    1. Explicit ``handler_id`` in payload (for multi-workflow sessions)\n    2. ``context.session_id`` (default — one handler per session)\n    \"\"\"\n    return payload.get(\"handler_id\") or session_id\n\n\n# ---------------------------------------------------------------------------\n# Payload parsing (unchanged from original for backwards compat)\n# ---------------------------------------------------------------------------\n\n\ndef _parse_and_validate_payload(\n    workflows: dict[str, Any],\n    default_workflow: str,\n    file_workflow: str | None,\n    payload: dict[str, Any],\n) -> tuple[str, StartEvent] | tuple[str, str]:\n    \"\"\"Parse incoming payload to determine workflow and event data.\n\n    Supports:\n        - Explicit: {\"workflow\": \"process-file\", \"start_event\": {\"file_id\": \"123\"}}\n        - Shorthand: {\"file_id\": \"123\"} -> routes to file-processing workflow\n        - Default: {} -> routes to default workflow\n    \"\"\"\n    workflow_name = payload.get(\"workflow\")\n    event_data = payload.get(\"start_event\", {})\n\n    if not workflow_name:\n        if \"file_id\" in payload and file_workflow is not None:\n            workflow_name = file_workflow\n            event_data = {\"file_id\": payload[\"file_id\"]}\n        else:\n            workflow_name = default_workflow\n    if workflow_name not in workflows:\n        return workflow_name, f\"Workflow not found: {workflow_name}\"\n    wf = workflows[workflow_name]\n    start_cls = wf.start_event_class\n    try:\n        data = start_cls.model_validate(event_data)\n    except ValidationError as e:\n        return workflow_name, f\"Invalid input data: {e}\"\n    return workflow_name, data\n\n\n# ---------------------------------------------------------------------------\n# Action handlers\n# ---------------------------------------------------------------------------\n\n\nasync def _action_run(\n    payload: dict[str, Any], session_id: str, *, nowait: bool = False\n) -> dict[str, Any]:\n    \"\"\"Run (or resume) a workflow with session-aware handler ID.\"\"\"\n    workflows, default_workflow, file_workflow = _load_workflows()\n    parsed = _parse_and_validate_payload(\n        workflows, default_workflow, file_workflow, payload\n    )\n\n    # Validation error\n    if len(parsed) == 2 and all(isinstance(p, str) for p in parsed):\n        workflow_name, error = parsed[0], parsed[1]\n        return WorkflowResult(\n            workflow=str(workflow_name),\n            error=str(error),\n            status=\"failed\",\n            session_id=session_id,\n        ).model_dump()\n\n    workflow_name = str(parsed[0])\n    start_event: StartEvent = parsed[1]  # type: ignore[reportAssignmentType]  # ty: ignore[invalid-assignment]\n    handler_id = _resolve_handler_id(payload, session_id)\n    service = get_agentcore_service()\n\n    try:\n        handler_data = await service.run_workflow_with_session(\n            workflow_name,\n            start_event,\n            handler_id=handler_id,\n            nowait=nowait,\n        )\n    except Exception as e:\n        logger.error(\"Workflow '%s' failed: %s\", workflow_name, e, exc_info=True)\n        return WorkflowResult(\n            workflow=workflow_name,\n            error=f\"Workflow failed: {e}\",\n            status=\"failed\",\n            session_id=session_id,\n        ).model_dump()\n\n    return _handler_to_result(handler_data, session_id)\n\n\nasync def _action_get_result(\n    payload: dict[str, Any], session_id: str\n) -> dict[str, Any]:\n    \"\"\"Get handler status and result.\"\"\"\n    handler_id = _resolve_handler_id(payload, session_id)\n    service = get_agentcore_service()\n\n    handler = await service.get_handler(handler_id)\n    if handler is None:\n        return {\"error\": f\"Handler '{handler_id}' not found\", \"session_id\": session_id}\n\n    return _handler_to_result(handler, session_id)\n\n\nasync def _action_get_events(\n    payload: dict[str, Any], session_id: str\n) -> dict[str, Any]:\n    \"\"\"Retrieve recorded events for a handler.\"\"\"\n    handler_id = _resolve_handler_id(payload, session_id)\n    service = get_agentcore_service()\n\n    after_seq = payload.get(\"after_sequence\", -1)\n    limit = payload.get(\"limit\", 200)\n    after = after_seq if after_seq >= 0 else None\n\n    events = await service.get_events(handler_id, after_sequence=after, limit=limit)\n    return {\n        \"handler_id\": handler_id,\n        \"session_id\": session_id,\n        \"events\": [_serialize_event(e) for e in events],\n    }\n\n\nasync def _action_send_event(\n    payload: dict[str, Any], session_id: str\n) -> dict[str, Any]:\n    \"\"\"Send an event into a running workflow (human-in-the-loop).\"\"\"\n    handler_id = _resolve_handler_id(payload, session_id)\n    event_data = payload.get(\"event\")\n    if not event_data:\n        return {\"error\": \"event is required\", \"session_id\": session_id}\n\n    step = payload.get(\"step\")\n    value = event_data.get(\"value\", event_data)\n    event = Event(**value) if isinstance(value, dict) else Event()\n\n    service = get_agentcore_service()\n    await service.send_event(handler_id, event, step=step)\n    return {\"status\": \"sent\", \"handler_id\": handler_id, \"session_id\": session_id}\n\n\nasync def _action_cancel(payload: dict[str, Any], session_id: str) -> dict[str, Any]:\n    \"\"\"Cancel a running workflow handler.\"\"\"\n    handler_id = _resolve_handler_id(payload, session_id)\n    purge = payload.get(\"purge\", False)\n\n    service = get_agentcore_service()\n    result = await service.cancel_handler(handler_id, purge=purge)\n    return {\n        \"status\": result or \"not_found\",\n        \"handler_id\": handler_id,\n        \"session_id\": session_id,\n    }\n\n\nasync def _action_list_workflows(\n    _payload: dict[str, Any], session_id: str\n) -> dict[str, Any]:\n    \"\"\"List registered workflow names.\"\"\"\n    service = get_agentcore_service()\n    return {\"workflows\": service.get_workflow_names(), \"session_id\": session_id}\n\n\nasync def _action_list_handlers(\n    payload: dict[str, Any], session_id: str\n) -> dict[str, Any]:\n    \"\"\"List handlers, optionally filtered by workflow and/or status.\"\"\"\n    query_kwargs: dict[str, Any] = {}\n    if payload.get(\"workflow\"):\n        query_kwargs[\"workflow_name_in\"] = [payload[\"workflow\"]]\n    if payload.get(\"status\"):\n        query_kwargs[\"status_in\"] = [payload[\"status\"]]\n\n    service = get_agentcore_service()\n    handlers = await service.query_handlers(HandlerQuery(**query_kwargs))\n    return {\n        \"handlers\": [_handler_to_result(h, session_id) for h in handlers],\n        \"session_id\": session_id,\n    }\n\n\n# Action dispatch table\n_ACTIONS: dict[str, Any] = {\n    \"run\": lambda p, sid: _action_run(p, sid, nowait=False),\n    \"run_nowait\": lambda p, sid: _action_run(p, sid, nowait=True),\n    \"get_result\": _action_get_result,\n    \"get_events\": _action_get_events,\n    \"send_event\": _action_send_event,\n    \"cancel\": _action_cancel,\n    \"list_workflows\": _action_list_workflows,\n    \"list_handlers\": _action_list_handlers,\n}\n\n\n# ---------------------------------------------------------------------------\n# AgentCore entrypoint\n# ---------------------------------------------------------------------------\n\n\n@app.entrypoint\nasync def invoke(payload: dict, context: Any) -> dict[str, Any]:\n    \"\"\"Single entrypoint that dispatches to workflow operations.\n\n    ``context.session_id`` is used as the default handler_id, giving a\n    natural 1:1 mapping between AgentCore sessions and workflow handlers.\n\n    When ``\"action\"`` is omitted, falls back to synchronous run-and-wait\n    for backwards compatibility.\n    \"\"\"\n    session_id: str = getattr(context, \"session_id\", \"\")\n    action = payload.get(\"action\")\n\n    # Default: run-and-wait (backwards compatible)\n    if action is None:\n        return await _action_run(payload, session_id, nowait=False)\n\n    handler_fn = _ACTIONS.get(action)\n    if handler_fn is None:\n        return {\n            \"error\": f\"Unknown action '{action}'\",\n            \"available\": list(_ACTIONS.keys()),\n            \"session_id\": session_id,\n        }\n\n    try:\n        return await handler_fn(payload, session_id)\n    except Exception as e:\n        logger.exception(\"Action '%s' failed\", action)\n        return {\n            \"error\": str(e),\n            \"traceback\": traceback.format_exc(),\n            \"action\": action,\n            \"session_id\": session_id,\n        }\n"
  },
  {
    "path": "packages/llama-agents-agentcore/src/llama_agents/agentcore/export.py",
    "content": "from pathlib import Path\n\nagentcore_dir = Path(\".agentcore\")\n\n\ndef export_generated_entrypoint_code() -> None:\n    entrypoint = Path(__file__).parent / \"entrypoint.py\"\n    content = entrypoint.read_text()\n    agentcore_dir.mkdir(exist_ok=True)\n    with open(agentcore_dir / \"entrypoint.py\", \"w\") as f:\n        f.write(content)\n    return None\n"
  },
  {
    "path": "packages/llama-agents-agentcore/src/llama_agents/agentcore/main.py",
    "content": "import os\nfrom argparse import ArgumentParser\n\nfrom .entrypoint import AGENTCORE_HOST, AGENTCORE_PORT, app\n\nif __name__ == \"__main__\":\n    parser = ArgumentParser()\n\n    parser.add_argument(\n        \"--run\",\n        help=\"Run the application in the target environment\",\n        action=\"store_true\",\n        default=False,\n    )\n    parser.add_argument(\n        \"--local\",\n        help=\"Run in local mode with a local SQLite store (no AWS required)\",\n        action=\"store_true\",\n        default=False,\n    )\n\n    args = parser.parse_args()\n\n    if args.local:\n        os.environ[\"LLAMA_AGENTCORE_LOCAL\"] = \"1\"\n\n    if args.run or args.local:\n        print(f\"Starting app on {AGENTCORE_HOST}:{AGENTCORE_PORT}\")  # noqa\n        if args.local:\n            print(  # noqa\n                \"Local mode: using a local SQLite store, no AWS credentials needed.\\n\"\n                \"Send requests to POST http://localhost:8080/invocations\"\n            )\n        app.run(port=AGENTCORE_PORT, host=AGENTCORE_HOST)\n"
  },
  {
    "path": "packages/llama-agents-agentcore/tests/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-agentcore/tests/conftest.py",
    "content": "from dataclasses import dataclass\nfrom typing import Any\n\nfrom pydantic import BaseModel\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\n\nclass FileEvent(StartEvent):\n    file_id: str\n\n\nclass Metadata(BaseModel):\n    name: str\n\n\nclass DummyWorkflow(Workflow):\n    @step\n    async def take_step(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"hello\")\n\n\nclass DummyFileWorkflow(Workflow):\n    @step\n    async def take_step(self, ev: FileEvent) -> StopEvent:\n        return StopEvent(result=ev.file_id)\n\n\nclass DummyMetadataWorkflow(Workflow):\n    @step\n    async def take_step(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=Metadata(name=\"nemo\"))\n\n\nclass DummyWorkflowWithError(Workflow):\n    @step\n    async def take_step(self, ev: StartEvent) -> StopEvent:\n        raise ValueError(\"You shall not pass!\")\n\n\n@dataclass\nclass MockContext:\n    session_id: str = \"session-1\"\n\n\n@dataclass\nclass MockHandlerData:\n    handler_id: str = \"handler-1\"\n    workflow_name: str = \"default\"\n    run_id: str = \"run-1\"\n    status: str = \"completed\"\n    result: dict | None = None\n    error: str | None = None\n    started_at: str | None = None\n    updated_at: str | None = None\n    completed_at: str | None = None\n\n\nclass MockAgentCoreService:\n    def __init__(self, with_error: bool = False) -> None:\n        self.return_handler_data = MockHandlerData(result={\"hello\": \"world\"})\n        self.call_args: list[dict[str, Any]] = []\n        self.with_error = with_error\n\n    async def run_workflow(\n        self,\n        workflow_name: str,\n        *,\n        start_event: StartEvent,\n    ) -> MockHandlerData:\n        self.call_args.append(\n            {\"workflow_name\": workflow_name, \"start_event\": start_event}\n        )\n        if workflow_name == \"with_error\":\n            raise ValueError(\"You shall not pass!\")\n        if not self.with_error:\n            return self.return_handler_data\n        return MockHandlerData(result={}, error=\"Some fancy error\")\n\n    async def run_workflow_with_session(\n        self,\n        workflow_name: str,\n        start_event: StartEvent,\n        handler_id: str,\n        *,\n        nowait: bool = False,\n    ) -> MockHandlerData:\n        self.call_args.append(\n            {\n                \"workflow_name\": workflow_name,\n                \"start_event\": start_event,\n                \"handler_id\": handler_id,\n                \"nowait\": nowait,\n            }\n        )\n        if workflow_name == \"with_error\":\n            raise ValueError(\"You shall not pass!\")\n        if not self.with_error:\n            data = MockHandlerData(\n                handler_id=handler_id,\n                workflow_name=workflow_name,\n                result={\"hello\": \"world\"},\n            )\n            return data\n        return MockHandlerData(\n            handler_id=handler_id,\n            workflow_name=workflow_name,\n            result={},\n            error=\"Some fancy error\",\n        )\n\n    def get_workflow_names(self) -> list[str]:\n        return [\"default\", \"metadata\", \"process-file\"]\n\n    async def get_handler(self, handler_id: str) -> MockHandlerData | None:\n        return None\n\n    async def query_handlers(self, query: Any) -> list:\n        return []\n\n    async def get_events(\n        self,\n        handler_id: str,\n        after_sequence: int | None = None,\n        limit: int | None = None,\n    ) -> list:\n        return []\n\n    async def send_event(\n        self, handler_id: str, event: Any, step: str | None = None\n    ) -> None:\n        pass\n\n    async def cancel_handler(self, handler_id: str, purge: bool = False) -> str | None:\n        return \"cancelled\"\n\n\nclass MockBedrockApp:\n    def __init__(self) -> None:\n        self.tasks: dict[int, str] = {}\n        self.added = 0\n        self.completed = 0\n\n    def add_async_task(self, name: str, metadata: dict[str, Any] | None = None) -> int:\n        self.added += 1\n        self.tasks[id(name)] = name\n        return id(name)\n\n    def complete_async_task(self, task_id: int) -> bool:\n        self.completed += 1\n        if task_id in self.tasks:\n            self.tasks.pop(task_id)\n            return True\n        return False\n"
  },
  {
    "path": "packages/llama-agents-agentcore/tests/test_entrypoint.py",
    "content": "from pathlib import Path\nfrom unittest.mock import Mock, call, patch\n\nimport pytest\nfrom llama_agents.agentcore.entrypoint import (\n    WorkflowResult,\n    _load_workflows,\n    _parse_and_validate_payload,\n    invoke,\n)\nfrom workflows import Workflow\nfrom workflows.events import StartEvent\n\nfrom .conftest import (\n    DummyFileWorkflow,\n    DummyMetadataWorkflow,\n    DummyWorkflow,\n    DummyWorkflowWithError,\n    FileEvent,\n    MockAgentCoreService,\n    MockContext,\n)\n\n\n@pytest.fixture(scope=\"module\")\ndef loaded_workflows() -> dict[str, Workflow]:\n    return {\n        \"default\": DummyWorkflow(),\n        \"metadata\": DummyMetadataWorkflow(),\n        \"process-file\": DummyFileWorkflow(),\n    }\n\n\ndef test_load_workflows_with_meta(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint.read_deployment_config_from_git_root_or_cwd\",\n            new_callable=Mock,\n        ) as mock_read,\n        patch(\n            \"llama_agents.agentcore.entrypoint.load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\"llama_agents.agentcore.entrypoint.load_environment_variables\"),\n        patch(\"llama_agents.agentcore.entrypoint.validate_required_env_vars\"),\n    ):\n        monkeypatch.chdir(tmp_path)\n\n        (tmp_path / \"pyproject.toml\").touch()\n\n        mock_read.return_value = {\"config\": {}}\n        mock_load.return_value = loaded_workflows\n\n        workflows, default_workflow, file_workflow = _load_workflows()\n        _load_workflows.cache_clear()\n        for key in workflows:\n            assert key in loaded_workflows\n            assert isinstance(workflows[key], type(loaded_workflows[key]))\n        assert default_workflow == \"default\"\n        assert file_workflow == \"process-file\"\n        mock_read.assert_has_calls([call(Path.cwd(), Path.cwd())])\n        mock_load.assert_called_once_with({\"config\": {}})\n\n\ndef test_load_workflows_without_meta(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint.read_deployment_config_from_git_root_or_cwd\",\n            new_callable=Mock,\n        ) as mock_read,\n        patch(\n            \"llama_agents.agentcore.entrypoint.load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\"llama_agents.agentcore.entrypoint.load_environment_variables\"),\n        patch(\"llama_agents.agentcore.entrypoint.validate_required_env_vars\"),\n    ):\n        loaded_workflows_cp = loaded_workflows.copy()\n\n        loaded_workflows_cp.pop(\"default\")\n\n        monkeypatch.chdir(tmp_path)\n\n        (tmp_path / \"pyproject.toml\").touch()\n\n        mock_read.return_value = {\"config\": {}}\n        mock_load.return_value = loaded_workflows_cp\n\n        workflows, default_workflow, file_workflow = _load_workflows()\n        _load_workflows.cache_clear()\n        for key in workflows:\n            assert key in loaded_workflows_cp\n            assert isinstance(workflows[key], type(loaded_workflows_cp[key]))\n        assert default_workflow == \"metadata\"\n        assert file_workflow == \"process-file\"\n        mock_read.assert_has_calls([call(Path.cwd(), Path.cwd())])\n        mock_load.assert_called_once_with({\"config\": {}})\n\n\ndef test_load_workflows_without_file(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint.read_deployment_config_from_git_root_or_cwd\",\n            new_callable=Mock,\n        ) as mock_read,\n        patch(\n            \"llama_agents.agentcore.entrypoint.load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\"llama_agents.agentcore.entrypoint.load_environment_variables\"),\n        patch(\"llama_agents.agentcore.entrypoint.validate_required_env_vars\"),\n    ):\n        loaded_workflows_cp = loaded_workflows.copy()\n\n        loaded_workflows_cp.pop(\"process-file\")\n\n        monkeypatch.chdir(tmp_path)\n\n        (tmp_path / \"pyproject.toml\").touch()\n\n        mock_read.return_value = {\"config\": {}}\n        mock_load.return_value = loaded_workflows_cp\n\n        workflows, default_workflow, file_workflow = _load_workflows()\n        _load_workflows.cache_clear()\n        for key in workflows:\n            assert key in loaded_workflows_cp\n            assert isinstance(workflows[key], type(loaded_workflows_cp[key]))\n        assert default_workflow == \"default\"\n        assert file_workflow is None\n        mock_read.assert_has_calls([call(Path.cwd(), Path.cwd())])\n        mock_load.assert_called_once_with({\"config\": {}})\n\n\ndef test_parse_and_validate_payload_success(\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    parsed = _parse_and_validate_payload(\n        workflows=loaded_workflows,\n        default_workflow=\"default\",\n        file_workflow=\"process-file\",\n        payload={\"workflow\": \"default\", \"start_event\": {}},\n    )\n    assert len(parsed) == 2\n    assert isinstance(parsed[0], str)\n    assert isinstance(parsed[1], StartEvent)\n    assert parsed[0] == \"default\"\n    assert parsed[1].model_dump() == {}\n\n\ndef test_parse_and_validate_payload_file_id(\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    parsed = _parse_and_validate_payload(\n        workflows=loaded_workflows,\n        default_workflow=\"default\",\n        file_workflow=\"process-file\",\n        payload={\"file_id\": \"1\"},\n    )\n    assert len(parsed) == 2\n    assert isinstance(parsed[0], str)\n    assert isinstance(parsed[1], FileEvent)\n    assert parsed[0] == \"process-file\"\n    assert parsed[1].file_id == \"1\"\n\n\ndef test_parse_and_validate_payload_defaults(\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    parsed = _parse_and_validate_payload(\n        workflows=loaded_workflows,\n        default_workflow=\"default\",\n        file_workflow=\"process-file\",\n        payload={},\n    )\n    assert len(parsed) == 2\n    assert isinstance(parsed[0], str)\n    assert isinstance(parsed[1], StartEvent)\n    assert parsed[0] == \"default\"\n    assert parsed[1].model_dump() == {}\n\n\ndef test_parse_and_validate_payload_workflow_not_found(\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    parsed = _parse_and_validate_payload(\n        workflows=loaded_workflows,\n        default_workflow=\"default\",\n        file_workflow=\"process-file\",\n        payload={\"workflow\": \"notfound\", \"start_event\": {}},\n    )\n    assert len(parsed) == 2\n    assert isinstance(parsed[0], str)\n    assert isinstance(parsed[1], str)\n    assert parsed[0] == \"notfound\"\n    assert parsed[1] == \"Workflow not found: notfound\"\n\n\ndef test_parse_and_validate_payload_invalid_event(\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    parsed = _parse_and_validate_payload(\n        workflows=loaded_workflows,\n        default_workflow=\"default\",\n        file_workflow=\"process-file\",\n        payload={\"workflow\": \"process-file\", \"start_event\": {\"file_id\": 1}},\n    )\n    assert len(parsed) == 2\n    assert isinstance(parsed[0], str)\n    assert isinstance(parsed[1], str)\n    assert parsed[0] == \"process-file\"\n    assert parsed[1].startswith(\"Invalid input data: \")\n\n\n@pytest.mark.asyncio\nasync def test_invoke_success_default_action(\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    \"\"\"When no action is specified, invoke runs the workflow synchronously.\"\"\"\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService()\n        mock_get_service.return_value = mock_service\n        mock_load.return_value = (loaded_workflows, \"default\", \"process-file\")\n        result = await invoke(\n            {\"workflow\": \"process-file\", \"start_event\": {\"file_id\": \"1\"}},\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        assert result[\"session_id\"] == \"session-1\"\n        assert result[\"handler_id\"] == \"session-1\"  # session_id used as handler_id\n        assert result[\"workflow_name\"] == \"process-file\"\n        assert result[\"status\"] == \"completed\"\n        # run_workflow_with_session should have been called\n        assert len(mock_service.call_args) == 1\n        assert mock_service.call_args[0][\"workflow_name\"] == \"process-file\"\n        assert mock_service.call_args[0][\"handler_id\"] == \"session-1\"\n        assert mock_service.call_args[0][\"start_event\"] == FileEvent(file_id=\"1\")\n\n\n@pytest.mark.asyncio\nasync def test_invoke_with_explicit_handler_id(\n    loaded_workflows: dict[str, Workflow],\n) -> None:\n    \"\"\"Explicit handler_id in payload overrides session_id.\"\"\"\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService()\n        mock_get_service.return_value = mock_service\n        mock_load.return_value = (loaded_workflows, \"default\", \"process-file\")\n        result = await invoke(\n            {\n                \"workflow\": \"default\",\n                \"start_event\": {},\n                \"handler_id\": \"custom-handler-id\",\n            },\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        assert result[\"handler_id\"] == \"custom-handler-id\"\n        assert result[\"session_id\"] == \"session-1\"\n        assert mock_service.call_args[0][\"handler_id\"] == \"custom-handler-id\"\n\n\n@pytest.mark.asyncio\nasync def test_invoke_error(loaded_workflows: dict[str, Workflow]) -> None:\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService()\n        mock_get_service.return_value = mock_service\n        loaded_workflows_cp = loaded_workflows.copy()\n        loaded_workflows_cp[\"with_error\"] = DummyWorkflowWithError()\n        mock_load.return_value = (loaded_workflows_cp, \"default\", \"process-file\")\n        result = await invoke(\n            {\"workflow\": \"with_error\", \"start_event\": {}},\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        validated = WorkflowResult.model_validate(result)\n        assert validated.error is not None\n        assert (\n            \"Workflow failed: \" in validated.error\n            and \"You shall not pass!\" in validated.error\n        )\n        assert validated.result is None\n        assert validated.status == \"failed\"\n        assert validated.session_id == \"session-1\"\n        assert validated.workflow == \"with_error\"\n\n\n@pytest.mark.asyncio\nasync def test_invoke_handler_with_error(loaded_workflows: dict[str, Workflow]) -> None:\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService(with_error=True)\n        mock_get_service.return_value = mock_service\n        mock_load.return_value = (loaded_workflows, \"default\", \"process-file\")\n        result = await invoke(\n            {\"workflow\": \"default\", \"start_event\": {}},\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        assert result[\"error\"] == \"Some fancy error\"\n        assert result[\"status\"] == \"completed\"  # handler completed but with error\n        assert result[\"session_id\"] == \"session-1\"\n\n\n@pytest.mark.asyncio\nasync def test_invoke_validation_error(loaded_workflows: dict[str, Workflow]) -> None:\n    \"\"\"Workflow not found returns a WorkflowResult error.\"\"\"\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService()\n        mock_get_service.return_value = mock_service\n        mock_load.return_value = (loaded_workflows, \"default\", \"process-file\")\n        result = await invoke(\n            {\"workflow\": \"notfound\", \"start_event\": {}},\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        validated = WorkflowResult.model_validate(result)\n        assert validated.error == \"Workflow not found: notfound\"\n        assert validated.status == \"failed\"\n        assert validated.session_id == \"session-1\"\n\n\n@pytest.mark.asyncio\nasync def test_invoke_run_nowait(loaded_workflows: dict[str, Workflow]) -> None:\n    \"\"\"action=run_nowait starts workflow without waiting.\"\"\"\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService()\n        mock_get_service.return_value = mock_service\n        mock_load.return_value = (loaded_workflows, \"default\", \"process-file\")\n        result = await invoke(\n            {\"action\": \"run_nowait\", \"workflow\": \"default\", \"start_event\": {}},\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        assert result[\"session_id\"] == \"session-1\"\n        assert mock_service.call_args[0][\"nowait\"] is True\n\n\n@pytest.mark.asyncio\nasync def test_invoke_list_workflows(loaded_workflows: dict[str, Workflow]) -> None:\n    \"\"\"action=list_workflows returns workflow names.\"\"\"\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService()\n        mock_get_service.return_value = mock_service\n        mock_load.return_value = (loaded_workflows, \"default\", \"process-file\")\n        result = await invoke(\n            {\"action\": \"list_workflows\"},\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        assert \"workflows\" in result\n        assert \"session_id\" in result\n\n\n@pytest.mark.asyncio\nasync def test_invoke_unknown_action(loaded_workflows: dict[str, Workflow]) -> None:\n    \"\"\"Unknown action returns error with available actions.\"\"\"\n    with (\n        patch(\n            \"llama_agents.agentcore.entrypoint._load_workflows\", new_callable=Mock\n        ) as mock_load,\n        patch(\n            \"llama_agents.agentcore.entrypoint.get_agentcore_service\",\n            new_callable=Mock,\n        ) as mock_get_service,\n    ):\n        mock_service = MockAgentCoreService()\n        mock_get_service.return_value = mock_service\n        mock_load.return_value = (loaded_workflows, \"default\", \"process-file\")\n        result = await invoke(\n            {\"action\": \"invalid_action\"},\n            MockContext(),\n        )\n        assert isinstance(result, dict)\n        assert \"error\" in result\n        assert \"available\" in result\n        assert \"run\" in result[\"available\"]\n"
  },
  {
    "path": "packages/llama-agents-agentcore/tests/test_export.py",
    "content": "from pathlib import Path\n\nimport pytest\nfrom llama_agents.agentcore.export import (\n    agentcore_dir,\n    export_generated_entrypoint_code,\n)\n\n\ndef test_export(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.chdir(tmp_path)\n    export_generated_entrypoint_code()\n    assert (tmp_path / agentcore_dir).is_dir()\n    assert (tmp_path / agentcore_dir / \"entrypoint.py\").is_file()\n    content = (tmp_path / agentcore_dir / \"entrypoint.py\").read_text()\n    assert content.startswith(\"# SPDX-License-Identifier: MIT\")\n    assert content.endswith('\"session_id\": session_id,\\n        }\\n')\n"
  },
  {
    "path": "packages/llama-agents-agentcore/tests/test_service.py",
    "content": "from typing import cast\n\nimport pytest\nfrom llama_agents.agentcore._runtime_decorator import AgentCoreRuntimeDecorator\nfrom llama_agents.agentcore._service import AgentCoreService, WorkflowNotFoundError\nfrom llama_agents.server._service import _WorkflowService\nfrom llama_agents.server._store.memory_workflow_store import MemoryWorkflowStore\nfrom workflows import Workflow\nfrom workflows.events import StartEvent, StopEvent\n\nfrom .conftest import (\n    DummyFileWorkflow,\n    DummyMetadataWorkflow,\n    DummyWorkflow,\n    DummyWorkflowWithError,\n    FileEvent,\n    MockBedrockApp,\n)\n\n\n@pytest.fixture()\ndef workflows() -> dict[str, Workflow]:\n    return {\n        \"default\": DummyWorkflow(),\n        \"metadata\": DummyMetadataWorkflow(),\n        \"process-file\": DummyFileWorkflow(),\n    }\n\n\n@pytest.fixture()\ndef store() -> MemoryWorkflowStore:\n    return MemoryWorkflowStore()\n\n\ndef test_init(store: MemoryWorkflowStore) -> None:\n    app = MockBedrockApp()\n    service = AgentCoreService(store=store, app=app)  # type: ignore\n    assert isinstance(service._workflow_store, MemoryWorkflowStore)\n    assert isinstance(service._runtime, AgentCoreRuntimeDecorator)\n    assert isinstance(service._service, _WorkflowService)\n\n\ndef test_add_workflows(\n    store: MemoryWorkflowStore, workflows: dict[str, Workflow]\n) -> None:\n    workflows_cp = workflows.copy()\n    app = MockBedrockApp()\n    service = AgentCoreService(store=store, app=app)  # type: ignore\n    for name in workflows_cp:\n        service.add_workflow(name, workflows_cp[name])\n        wf = service._service.get_workflow(name)\n        assert wf is not None\n        assert wf.workflow_name == name\n    names = service._service.get_workflow_names()\n    assert len(names) == len(workflows_cp)\n    assert all(name in workflows_cp for name in names)\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_simple_success(\n    store: MemoryWorkflowStore, workflows: dict[str, Workflow]\n) -> None:\n    workflows_cp = workflows.copy()\n    app = MockBedrockApp()\n    service = AgentCoreService(store=store, app=app)  # type: ignore\n    for name in workflows_cp:\n        service.add_workflow(name, workflows_cp[name])\n    result = await service.run_workflow(\n        workflow_name=\"default\", start_event=StartEvent()\n    )\n    assert result.result is not None\n    event = cast(StopEvent, result.result.load_event())\n    assert event.result == \"hello\"\n    assert app.added == 1  # 1 step -> add_async_task called once\n    assert app.completed == 1  # 1 step -> complete_async_task called once\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_file_success(\n    store: MemoryWorkflowStore, workflows: dict[str, Workflow]\n) -> None:\n    workflows_cp = workflows.copy()\n    app = MockBedrockApp()\n    service = AgentCoreService(store=store, app=app)  # type: ignore\n    for name in workflows_cp:\n        service.add_workflow(name, workflows_cp[name])\n    result = await service.run_workflow(\n        workflow_name=\"process-file\", start_event=FileEvent(file_id=\"1\")\n    )\n    assert result.result is not None\n    event = cast(StopEvent, result.result.load_event())\n    assert event.result == \"1\"\n    assert app.added == 1  # 1 step -> add_async_task called once\n    assert app.completed == 1  # 1 step -> complete_async_task called once\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_retains_error(\n    store: MemoryWorkflowStore, workflows: dict[str, Workflow]\n) -> None:\n    app = MockBedrockApp()\n    service = AgentCoreService(store=store, app=app)  # type: ignore\n    workflows_cp = workflows.copy()\n    workflows_cp[\"with-error\"] = DummyWorkflowWithError()\n    for name in workflows_cp:\n        service.add_workflow(name, workflows_cp[name])\n    data = await service.run_workflow(\n        workflow_name=\"with-error\", start_event=StartEvent()\n    )\n    assert data.error is not None\n    assert \"You shall not pass!\" == data.error\n    assert app.added == 1  # 1 step -> add_async_task called once\n    assert (\n        app.completed == 1\n    )  # Step raises, but should be completed thanks to the `try ... finally` block\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_not_found_error(\n    store: MemoryWorkflowStore, workflows: dict[str, Workflow]\n) -> None:\n    workflows_cp = workflows.copy()\n    app = MockBedrockApp()\n    service = AgentCoreService(store=store, app=app)  # type: ignore\n    for name in workflows_cp:\n        service.add_workflow(name, workflows_cp[name])\n    with pytest.raises(WorkflowNotFoundError):\n        await service.run_workflow(workflow_name=\"notexist\", start_event=StartEvent())\n"
  },
  {
    "path": "packages/llama-agents-appserver/CHANGELOG.md",
    "content": "# llama-agents-appserver\n\n## 0.11.4\n\n### Patch Changes\n\n- Updated dependencies [c3fac21]\n  - llama-agents-core@0.10.2\n\n## 0.11.3\n\n### Patch Changes\n\n- Updated dependencies [463c79d]\n  - llama-agents-core@0.10.1\n\n## 0.11.2\n\n### Patch Changes\n\n- Updated dependencies [2280e04]\n  - llama-agents-core@0.10.0\n\n## 0.11.1\n\n### Patch Changes\n\n- 916b157: Fix appserver install when the target template is a uv workspace member. Install now targets whichever venv `uv run` resolves to, instead of a hard-coded `<template>/.venv`, so `llamactl dev validate` and `llamactl serve` work in workspace layouts.\n\n## 0.11.0\n\n### Minor Changes\n\n- facbac4: `PUBLIC_*` env var overlay for UI builds: `PUBLIC_X` overrides `X` in the build env so backend and frontend can use different URLs for the same service. Removes dead `VITE_`/`NEXT_PUBLIC_` injection from `llamactl serve`. Helm network policy gains `extraEgressRules`, DNS selector overrides, and `blockPrivateRanges` toggle.\n\n## 0.10.5\n\n### Patch Changes\n\n- Updated dependencies [e8b8f47]\n  - llama-agents-core@0.9.0\n\n## 0.10.4\n\n### Patch Changes\n\n- 7ad3049: Reduce full clones from github for config, repo validation, and sha discovery. Reduce dependencies on system git, preferring dulwich\n- Updated dependencies [7ad3049]\n  - llama-agents-core@0.8.5\n\n## 0.10.3\n\n### Patch Changes\n\n- 286c91a: Loosen appserver deps on llama-agents-server\n\n## 0.10.2\n\n### Patch Changes\n\n- Updated dependencies [f27d98f]\n  - llama-agents-core@0.8.4\n\n## 0.10.1\n\n### Patch Changes\n\n- Updated dependencies [3f12660]\n  - llama-agents-core@0.8.3\n\n## 0.10.0\n\n### Minor Changes\n\n- 3e2e7b8: Run all containers as non-root with hardened security contexts\n\n## 0.9.1\n\n### Patch Changes\n\n- Updated dependencies [46f2675]\n  - llama-agents-core@0.8.2\n\n## 0.9.0\n\n### Minor Changes\n\n- 58e7942: Rename Docker image repos to per-component names (llama-agents-<component>) with plain version tags\n\n### Patch Changes\n\n- Updated dependencies [58e7942]\n  - llama-agents-core@0.8.1\n\n## 0.8.1\n\n### Patch Changes\n\n- Updated dependencies [e2f3abd]\n  - llama-agents-core@0.8.0\n\n## 0.8.0\n\n## 0.7.2\n\n## 0.7.1\n\n### Patch Changes\n\n- Updated dependencies [7bb9a90]\n  - llama-agents-core@0.7.0\n\n## 0.7.0\n\n## 0.6.5\n\n## 0.6.4\n\n## 0.6.3\n\n### Patch Changes\n\n- 4127101: Exclude .pnpm-store directory from build tarballs\n- 1594315: Skip auto-upgrade of dependencies (e.g. llama-index-workflows) during container bootstrap to avoid modifying the target project's pyproject.toml\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [508b5da]\n  - llama-agents-core@0.6.2\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [1b86f90]\n  - llama-agents-core@0.6.1\n\n## 0.6.0\n\n### Minor Changes\n\n- 4ab011f: Rename packages from llama-deploy to llama-agents.\n\n### Patch Changes\n\n- Updated dependencies [4ab011f]\n  - llama-agents-core@0.6.0\n\n## 0.5.3\n\n### Patch Changes\n\n- eee29c1: Warn and upgrade workflows version to avoid obscure import errors\n\n## 0.5.2\n\n### Patch Changes\n\n- e11ad55: Fix version ranges\n\n## 0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- ac74af4: Run build separately as a 1x time process per deployment update. Build stored in s3. Allows for fast unsuspend, and better future support for replication\n- 4ba0d9d: Switch out agent data workflow store for new journal based workflow store\n\n### Patch Changes\n\n- Updated dependencies [ac74af4]\n  - llama-deploy-core@0.5.0\n"
  },
  {
    "path": "packages/llama-agents-appserver/README.md",
    "content": "# llama-agents-appserver\n\nApplication server components for LlamaAgents.\n\nFor an end-to-end introduction, see [Getting started with LlamaAgents](https://developers.llamaindex.ai/python/cloud/llamaagents/getting-started).\n"
  },
  {
    "path": "packages/llama-agents-appserver/package.json",
    "content": "{\n  \"name\": \"llama-agents-appserver\",\n  \"version\": \"0.11.4\",\n  \"private\": false,\n  \"dependencies\": {\n    \"llama-agents-core\": \"workspace:*\"\n  },\n  \"docker\": {\n    \"dockerfile\": \"docker/Dockerfile\",\n    \"target\": \"appserver\",\n    \"imageName\": \"llamaindex/llama-agents-appserver\",\n    \"platforms\": [\n      \"linux/amd64\",\n      \"linux/arm64\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/llama-agents-appserver/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.7.20,<0.8.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"pytest>=8.4.1\",\n  \"pytest-asyncio>=0.25.3\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"respx>=0.22.0\",\n  \"ty>=0.0.15\",\n  \"ruff>=0.12.9\"\n]\n\n[project]\nname = \"llama-agents-appserver\"\nversion = \"0.11.4\"\ndescription = \"Application server components for LlamaDeploy\"\nreadme = \"README.md\"\nlicense = {text = \"MIT\"}\nauthors = [\n  {name = \"Massimiliano Pippi\", email = \"mpippi@gmail.com\"},\n  {name = \"Adrian Lyjak\", email = \"adrianlyjak@gmail.com\"}\n]\nrequires-python = \">=3.10, <4\"\ndependencies = [\n  \"llama-index-workflows>=2.16.0,<3.0.0\",\n  \"llama-agents-server>=0.2.2\",\n  \"pydantic-settings>=2.10.1\",\n  \"fastapi>=0.100.0\",\n  \"websockets>=12.0\",\n  \"llama-agents-core>=0.5.0\",\n  \"httpx>=0.24.0,<1.0.0\",\n  \"prometheus-fastapi-instrumentator>=7.1.0\",\n  \"packaging>=25.0\",\n  \"structlog>=25.4.0\",\n  \"rich>=14.1.0\",\n  \"pyyaml>=6.0.2\",\n  \"watchfiles>=1.1.0\",\n  \"uvicorn>=0.35.0\",\n  \"typing-extensions>=4.15.0 ; python_full_version < '3.12'\"\n]\n\n[tool.uv.build-backend]\nmodule-name = [\"llama_agents.appserver\", \"llama_deploy.appserver\"]\nnamespace = true\n\n[tool.uv.sources]\nllama-agents-core = {workspace = true}\n"
  },
  {
    "path": "packages/llama-agents-appserver/pytest.ini",
    "content": "[tool:pytest]\nmarkers =\n    e2e: marks tests as end-to-end tests (deselect with '-m \"not e2e\"')\n\ntestpaths = tests\npython_files = test_*.py\npython_classes = Test*\npython_functions = test_*\n\n# Async configuration\nasyncio_mode = auto\nasyncio_default_fixture_loop_scope = function\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/app.py",
    "content": "import argparse\nimport json\nimport logging\nimport os\nimport threading\nimport time\nimport webbrowser\nfrom contextlib import asynccontextmanager\nfrom importlib.metadata import version\nfrom pathlib import Path\nfrom typing import Any, AsyncGenerator, Literal, cast\n\nimport uvicorn\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import RedirectResponse\nfrom llama_agents.appserver.configure_logging import (\n    add_log_middleware,\n    setup_logging,\n)\nfrom llama_agents.appserver.deployment_config_parser import (\n    get_deployment_config,\n)\nfrom llama_agents.appserver.routers.deployments import (\n    create_base_router,\n    create_deployments_router,\n)\nfrom llama_agents.appserver.routers.ui_proxy import (\n    create_ui_proxy_router,\n    mount_static_files,\n)\nfrom llama_agents.appserver.settings import configure_settings, settings\nfrom llama_agents.appserver.workflow_loader import (\n    _exclude_venv_warning,\n    build_ui,\n    inject_appserver_into_target,\n    install_ui,\n    load_environment_variables,\n    load_workflows,\n    start_dev_ui_process,\n    validate_required_env_vars,\n)\nfrom llama_agents.core.config import DEFAULT_DEPLOYMENT_FILE_PATH\nfrom llama_agents.server import WorkflowServer\nfrom prometheus_fastapi_instrumentator import Instrumentator\n\nfrom .deployment import Deployment\nfrom .interrupts import shutdown_event\nfrom .process_utils import run_process\nfrom .routers import health_router\nfrom .stats import apiserver_state\n\nlogger = logging.getLogger(\"uvicorn.info\")\n\n# Auto-configure logging on import when requested (e.g., uvicorn reload workers)\nif os.getenv(\"LLAMA_DEPLOY_AUTO_LOGGING\", \"0\") == \"1\":\n    setup_logging(os.getenv(\"LOG_LEVEL\", \"INFO\"))\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:\n    shutdown_event.clear()\n    apiserver_state.state(\"starting\")\n    config = get_deployment_config()\n\n    workflows = load_workflows(config)\n    deployment = Deployment(workflows)\n    base_router = create_base_router(config.name)\n    deploy_router = create_deployments_router(config.name, deployment)\n    server = deployment.mount_workflow_server(app)\n\n    app.include_router(base_router)\n    app.include_router(deploy_router)\n\n    _setup_openapi(config.name, app, server)\n\n    if config.ui is not None:\n        if settings.proxy_ui:\n            ui_router = create_ui_proxy_router(config.name, settings.proxy_ui_port)\n            app.include_router(ui_router)\n        else:\n            # otherwise serve the pre-built if available\n            mount_static_files(app, config, settings)\n\n        @app.get(f\"/deployments/{config.name}\", include_in_schema=False)\n        @app.get(f\"/deployments/{config.name}/\", include_in_schema=False)\n        @app.get(f\"/deployments/{config.name}/ui\", include_in_schema=False)\n        def redirect_to_ui() -> RedirectResponse:\n            return RedirectResponse(f\"/deployments/{config.name}/ui/\")\n    else:\n\n        @app.get(f\"/deployments/{config.name}\", include_in_schema=False)\n        @app.get(f\"/deployments/{config.name}/\", include_in_schema=False)\n        def redirect_to_docs() -> RedirectResponse:\n            return RedirectResponse(f\"/deployments/{config.name}/docs\")\n\n    apiserver_state.state(\"running\")\n    # terrible sad cludge\n    async with server.contextmanager():\n        yield\n\n    apiserver_state.state(\"stopped\")\n\n\ndef _setup_openapi(name: str, app: FastAPI, server: WorkflowServer) -> None:\n    \"\"\"\n    extends the fastapi based openapi schema with starlette generated schema\n    \"\"\"\n    schema_title = \"Llama Deploy App Server\"\n    app_version = version(\"llama-agents-appserver\")\n\n    prefix = f\"/deployments/{name}\"\n\n    schema = server.openapi_schema()\n    schema[\"info\"][\"title\"] = schema_title\n    schema[\"info\"][\"version\"] = app_version\n    paths = cast(dict, schema[\"paths\"])\n    new_paths = {}\n    for path, methods in list(paths.items()):\n        if \"head\" in methods:\n            methods.pop(\"head\")\n        new_paths[prefix + path] = methods\n\n    schema[\"paths\"] = new_paths\n\n    def custom_openapi() -> dict[str, object]:\n        return schema\n\n    app.openapi = custom_openapi  # ty: ignore[invalid-assignment] - doesn't like us overwriting the method\n\n\n_config = get_deployment_config()\n_prefix = f\"/deployments/{_config.name}\"\napp = FastAPI(\n    lifespan=lifespan,\n    docs_url=_prefix + \"/docs\",\n    redoc_url=_prefix + \"/redoc\",\n    openapi_url=_prefix + \"/openapi.json\",\n)\nInstrumentator(\n    excluded_handlers=[\n        \"/health.*\",\n        \"/livez\",\n        \"/readyz\",\n        \"/metrics\",\n        \"^/$\",\n        \"/deployments/.+/ui\",\n        \"/deployments/[^/]+/?$\",\n    ],\n).instrument(\n    app,\n    latency_highr_buckets=(0.01, 0.05, 0.1, 0.5, 1, 5, 10, 30),\n    latency_lowr_buckets=(0.1, 0.5, 1),\n).expose(app, include_in_schema=False)\n\n\ndef _configure_cors(app: FastAPI) -> None:\n    \"\"\"Attach CORS middleware in a way that keeps type-checkers happy.\"\"\"\n    # Use a cast here because ty's view of Starlette's middleware factory\n    # protocol is stricter than FastAPI's runtime expectations.\n    app.add_middleware(\n        cast(Any, CORSMiddleware),\n        allow_origins=[\"*\"],  # Allows all origins\n        allow_credentials=True,\n        allow_methods=[\"GET\", \"POST\"],\n        allow_headers=[\"Content-Type\", \"Authorization\"],\n    )\n\n\nif not os.environ.get(\"DISABLE_CORS\", False):\n    _configure_cors(app)\n\napp.include_router(health_router)\nadd_log_middleware(app)\n\n\ndef open_browser_async(host: str, port: int) -> None:\n    def _open_with_delay() -> None:\n        time.sleep(1)\n        webbrowser.open(f\"http://{host}:{port}\")\n\n    threading.Thread(target=_open_with_delay).start()\n\n\ndef prepare_server(\n    deployment_file: Path | None = None,\n    install: bool = False,\n    build: bool = False,\n    install_ui_deps: bool = True,\n    skip_env_validation: bool = False,\n) -> None:\n    configure_settings(\n        deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH)\n    )\n    cfg = get_deployment_config()\n    load_environment_variables(cfg, settings.resolved_config_parent)\n    validate_required_env_vars(cfg, fill_missing=skip_env_validation)\n    if install:\n        config = get_deployment_config()\n        inject_appserver_into_target(config, settings.resolved_config_parent)\n        if install_ui_deps:\n            install_ui(config, settings.resolved_config_parent)\n    if build:\n        build_ui(settings.resolved_config_parent, get_deployment_config(), settings)\n\n\ndef start_server(\n    proxy_ui: bool = False,\n    reload: bool = False,\n    cwd: Path | None = None,\n    deployment_file: Path | None = None,\n    open_browser: bool = False,\n    configure_logging: bool = True,\n) -> None:\n    # Configure via environment so uvicorn reload workers inherit the values\n    configure_settings(\n        proxy_ui=proxy_ui,\n        app_root=cwd,\n        deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),\n        reload=reload,\n    )\n    cfg = get_deployment_config()\n    load_environment_variables(cfg, settings.resolved_config_parent)\n    validate_required_env_vars(cfg)\n\n    ui_process = None\n    if proxy_ui:\n        ui_process = start_dev_ui_process(\n            settings.resolved_config_parent, settings, get_deployment_config()\n        )\n    try:\n        if open_browser:\n            open_browser_async(settings.host, settings.port)\n        # Ensure reload workers configure logging on import\n        os.environ[\"LLAMA_DEPLOY_AUTO_LOGGING\"] = \"1\"\n        # Configure logging for the launcher process as well\n        if configure_logging:\n            setup_logging(os.getenv(\"LOG_LEVEL\", \"INFO\"))\n\n        uvicorn.run(\n            \"llama_agents.appserver.app:app\",\n            host=settings.host,\n            port=settings.port,\n            reload=reload,\n            reload_dirs=[\"src\"] if reload else None,\n            timeout_graceful_shutdown=1,\n            access_log=False,\n            log_config=None,\n        )\n    finally:\n        if ui_process is not None:\n            ui_process.terminate()\n\n\ndef start_server_in_target_venv(\n    proxy_ui: bool = False,\n    reload: bool = False,\n    cwd: Path | None = None,\n    deployment_file: Path | None = None,\n    open_browser: bool = False,\n    port: int | None = None,\n    ui_port: int | None = None,\n    log_level: str | None = None,\n    log_format: str | None = None,\n    persistence: Literal[\"memory\", \"local\", \"cloud\"] | None = None,\n    local_persistence_path: str | None = None,\n    cloud_persistence_name: str | None = None,\n    host: str | None = None,\n) -> None:\n    # Ensure settings reflect the intended working directory before computing paths\n\n    configure_settings(\n        app_root=cwd,\n        deployment_file_path=deployment_file,\n        reload=reload,\n        proxy_ui=proxy_ui,\n        persistence=persistence,\n        local_persistence_path=local_persistence_path,\n        cloud_persistence_name=cloud_persistence_name,\n        host=host,\n    )\n    base_dir = cwd or Path.cwd()\n    path = settings.resolved_config_parent.relative_to(base_dir)\n    args = [\"uv\", \"run\", \"--no-progress\", \"python\", \"-m\", \"llama_agents.appserver.app\"]\n    if proxy_ui:\n        args.append(\"--proxy-ui\")\n    if reload:\n        args.append(\"--reload\")\n    if deployment_file:\n        args.append(\"--deployment-file\")\n        args.append(str(deployment_file))\n    if open_browser:\n        args.append(\"--open-browser\")\n\n    env = os.environ.copy()\n    if port:\n        env[\"LLAMA_DEPLOY_APISERVER_PORT\"] = str(port)\n    if ui_port:\n        env[\"LLAMA_DEPLOY_APISERVER_PROXY_UI_PORT\"] = str(ui_port)\n    if log_level:\n        env[\"LOG_LEVEL\"] = log_level\n    if log_format:\n        env[\"LOG_FORMAT\"] = log_format\n\n    run_process(\n        args,\n        cwd=path,\n        env=env,\n        line_transform=_exclude_venv_warning,\n    )\n\n\ndef start_preflight_in_target_venv(\n    cwd: Path | None = None,\n    deployment_file: Path | None = None,\n    skip_env_validation: bool = False,\n) -> None:\n    \"\"\"\n    Run preflight validation inside the target project's virtual environment using uv.\n    Mirrors the venv targeting and invocation strategy used by start_server_in_target_venv.\n\n    Args:\n        cwd: Working directory for the validation.\n        deployment_file: Path to the deployment configuration file.\n        skip_env_validation: If True, skip validation of required environment variables.\n    \"\"\"\n    configure_settings(\n        app_root=cwd,\n        deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),\n    )\n    base_dir = cwd or Path.cwd()\n    path = settings.resolved_config_parent.relative_to(base_dir)\n    args = [\n        \"uv\",\n        \"run\",\n        \"--no-progress\",\n        \"python\",\n        \"-m\",\n        \"llama_agents.appserver.app\",\n        \"--preflight\",\n    ]\n    if deployment_file:\n        args.extend([\"--deployment-file\", str(deployment_file)])\n    if skip_env_validation:\n        args.append(\"--skip-env-validation\")\n\n    run_process(\n        args,\n        cwd=path,\n        env=os.environ.copy(),\n        line_transform=_exclude_venv_warning,\n    )\n    # Note: run_process doesn't return exit code; process runs to completion or raises\n\n\ndef start_export_json_graph_in_target_venv(\n    cwd: Path | None = None,\n    deployment_file: Path | None = None,\n    output: Path | None = None,\n) -> None:\n    \"\"\"\n    Run workflow graph export inside the target project's virtual environment using uv.\n    Mirrors the venv targeting and invocation strategy used by start_preflight_in_target_venv.\n    \"\"\"\n\n    configure_settings(\n        app_root=cwd,\n        deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),\n    )\n    base_dir = cwd or Path.cwd()\n    path = settings.resolved_config_parent.relative_to(base_dir)\n    args = [\n        \"uv\",\n        \"run\",\n        \"--no-progress\",\n        \"python\",\n        \"-m\",\n        \"llama_agents.appserver.app\",\n        \"--export-json-graph\",\n    ]\n    if deployment_file:\n        args.extend([\"--deployment-file\", str(deployment_file)])\n    if output is not None:\n        args.extend([\"--export-output\", str(output)])\n\n    run_process(\n        args,\n        cwd=path,\n        env=os.environ.copy(),\n        line_transform=_exclude_venv_warning,\n    )\n\n\nclass PreflightValidationError(Exception):\n    \"\"\"Raised when workflow validations fail during preflight.\n\n    Attributes:\n        errors: List of (workflow/service name, error message)\n    \"\"\"\n\n    def __init__(self, errors: list[tuple[str, str]]):\n        self.errors = errors\n        error_messages = [f\"{name}: {msg}\" for name, msg in errors]\n        message = \"Workflow validation failed:\\n\" + \"\\n\".join(error_messages)\n        super().__init__(message)\n\n\ndef preflight_validate(\n    cwd: Path | None = None,\n    deployment_file: Path | None = None,\n    configure_logging: bool = False,\n    skip_env_validation: bool = False,\n) -> None:\n    \"\"\"\n    Perform the same initialization path as starting the server, without serving.\n    This catches import errors and runs workflow validations.\n\n    Args:\n        cwd: Working directory for the validation.\n        deployment_file: Path to the deployment configuration file.\n        configure_logging: Whether to configure logging.\n        skip_env_validation: If True, fill missing required env vars with placeholder\n            values instead of raising an error. Useful when validating workflow structure\n            without actual environment variable values.\n    \"\"\"\n    configure_settings(\n        app_root=cwd,\n        deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),\n    )\n    cfg = get_deployment_config()\n    load_environment_variables(cfg, settings.resolved_config_parent)\n    validate_required_env_vars(cfg, fill_missing=skip_env_validation)\n\n    workflows = load_workflows(cfg)\n    # Instantiate Deployment to ensure server wiring doesn't raise\n    _ = Deployment(workflows)\n    # Run workflow-level validations if present\n    errors: list[tuple[str, str]] = []\n    for service_name, workflow in workflows.items():\n        method = getattr(workflow, \"_validate\", None)\n        if callable(method):\n            try:\n                method()\n            except Exception as exc:\n                errors.append((service_name, str(exc)))\n    if errors:\n        raise PreflightValidationError(errors)\n\n\ndef export_json_graph(\n    cwd: Path | None = None,\n    deployment_file: Path | None = None,\n    output: Path | None = None,\n) -> None:\n    \"\"\"\n    Export a JSON representation of the registered workflows' graph.\n\n    This follows the same initialization path as preflight validation and writes\n    a workflows.json-style structure compatible with the CLI expectations.\n    \"\"\"\n    from workflows.representation.build import get_workflow_representation\n\n    configure_settings(\n        app_root=cwd,\n        deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),\n    )\n    cfg = get_deployment_config()\n    load_environment_variables(cfg, settings.resolved_config_parent)\n\n    workflows = load_workflows(cfg)\n\n    graph: dict[str, dict[str, Any]] = {}\n    for name, workflow in workflows.items():\n        wf_repr_dict = get_workflow_representation(workflow).model_dump()\n        graph[name] = wf_repr_dict\n\n    output_path = output or (Path.cwd() / \"workflows.json\")\n    with output_path.open(\"w\", encoding=\"utf-8\") as f:\n        json.dump(graph, f, indent=2)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--proxy-ui\", action=\"store_true\")\n    parser.add_argument(\"--reload\", action=\"store_true\")\n    parser.add_argument(\"--deployment-file\", type=Path)\n    parser.add_argument(\"--open-browser\", action=\"store_true\")\n    parser.add_argument(\"--preflight\", action=\"store_true\")\n    parser.add_argument(\"--skip-env-validation\", action=\"store_true\")\n    parser.add_argument(\"--export-json-graph\", action=\"store_true\")\n    parser.add_argument(\"--export-output\", type=Path)\n\n    args = parser.parse_args()\n    if args.preflight:\n        preflight_validate(\n            cwd=Path.cwd(),\n            deployment_file=args.deployment_file,\n            skip_env_validation=args.skip_env_validation,\n        )\n    elif args.export_json_graph:\n        export_json_graph(\n            cwd=Path.cwd(),\n            deployment_file=args.deployment_file,\n            output=args.export_output,\n        )\n    else:\n        start_server(\n            proxy_ui=args.proxy_ui,\n            reload=args.reload,\n            deployment_file=args.deployment_file,\n            open_browser=args.open_browser,\n        )\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/bootstrap.py",
    "content": "\"\"\"\nBootstraps an application from a remote github repository given environment variables.\n\nSupports two modes of operation:\n\n- **Init container**: If a pre-built artifact exists (LLAMA_DEPLOY_BUILD_ID is set),\n  downloads and extracts it into the target directory. Otherwise, runs the full\n  bootstrap (clone, install deps, build UI).\n- **Build job**: Runs the full bootstrap, then packages the result into a tarball\n  and uploads it to the Build API for S3 storage.\n\"\"\"\n\nimport logging\nimport os\nimport tarfile\nimport time\nfrom importlib.metadata import version as pkg_version\nfrom pathlib import Path\n\nimport httpx\nfrom llama_agents.appserver.configure_logging import setup_logging\nfrom llama_agents.appserver.deployment_config_parser import get_deployment_config\nfrom llama_agents.appserver.settings import (\n    BootstrapSettings,\n    configure_settings,\n    settings,\n)\nfrom llama_agents.appserver.workflow_loader import (\n    build_ui,\n    inject_appserver_into_target,\n    install_ui,\n    load_environment_variables,\n    validate_required_env_vars,\n)\nfrom llama_agents.core.git.git_util import clone_repo_sync as clone_repo\n\nlogger = logging.getLogger(__name__)\n\n\ndef _extract_filter(member: tarfile.TarInfo, dest_path: str) -> tarfile.TarInfo:\n    \"\"\"Extraction filter like ``data`` but permits symlinks with absolute targets.\n\n    Virtualenvs contain symlinks like ``.venv/bin/python -> /usr/local/bin/python3.12``.\n    The built-in ``data`` filter rejects these (AbsoluteLinkError /\n    LinkOutsideDestinationError).  We skip the link-target checks for symlinks\n    while keeping every other safety check: path-traversal prevention, special-file\n    rejection, mode clamping, and ownership reset.\n    \"\"\"\n    if not member.issym():\n        # Non-symlink entries: delegate entirely to data_filter.\n        return tarfile.data_filter(member, dest_path)\n\n    # --- Symlink-specific path: replicate data_filter sans link-target checks ---\n\n    name = member.name\n\n    # Strip leading slashes (same as data_filter step 2).\n    if name.startswith((\"/\", os.sep)):\n        name = member.path.lstrip(\"/\" + os.sep)\n\n    # Reject still-absolute names (Windows drive letters, etc.).\n    if os.path.isabs(name):\n        raise tarfile.AbsolutePathError(member)\n\n    # Ensure the *entry itself* (not its target) resolves inside dest_path.\n    dest_path = os.path.realpath(dest_path)\n    target_path = os.path.realpath(os.path.join(dest_path, name), strict=False)\n    if os.path.commonpath([target_path, dest_path]) != dest_path:\n        raise tarfile.OutsideDestinationError(member, target_path)\n\n    # Nullify ownership (same as data_filter) and normalise name if needed.\n    if name != member.name:\n        return member.replace(name=name, uid=0, gid=0, uname=\"\", gname=\"\", deep=False)\n    return member.replace(uid=0, gid=0, uname=\"\", gname=\"\", deep=False)\n\n\ndef _download_and_extract_artifact(\n    build_api_host: str,\n    deployment_name: str,\n    build_id: str,\n    auth_token: str,\n    target_dir: str,\n) -> None:\n    \"\"\"Download a pre-built artifact from the Build API and extract into target_dir.\"\"\"\n    url = f\"http://{build_api_host}/deployments/{deployment_name}/builds/{build_id}\"\n    logger.info(\"Downloading build artifact from %s\", url)\n\n    tarball_path = \"/tmp/build-artifact-download.tar.gz\"\n    with httpx.stream(\n        \"GET\",\n        url,\n        headers={\"Authorization\": f\"Bearer {auth_token}\"},\n        timeout=600.0,\n    ) as response:\n        if response.status_code == 503:\n            raise RuntimeError(\n                \"Build artifact storage not configured on the control plane. \"\n                \"Cannot download build artifact. Ensure S3_BUCKET is set.\"\n            )\n        response.raise_for_status()\n        with open(tarball_path, \"wb\") as f:\n            for chunk in response.iter_bytes(chunk_size=65536):\n                f.write(chunk)\n\n    tarball_size = os.path.getsize(tarball_path)\n    logger.info(\"Downloaded artifact: %.1f MB\", tarball_size / (1024 * 1024))\n\n    os.makedirs(target_dir, exist_ok=True)\n    logger.info(\"Extracting artifact...\")\n    extract_start = time.monotonic()\n    with tarfile.open(tarball_path, \"r:*\") as tf:\n        tf.extractall(path=target_dir, filter=_extract_filter)\n    extract_elapsed = time.monotonic() - extract_start\n    logger.info(\"Extracted artifact into %s (%.1fs)\", target_dir, extract_elapsed)\n    os.remove(tarball_path)\n\n\n_TARBALL_EXCLUDE_DIRS = {\".git\", \"node_modules\", \"__pycache__\", \".pnpm-store\"}\n\n\ndef _create_tarball(source_dir: str, output_path: str) -> None:\n    \"\"\"Create an uncompressed tarball of the source directory.\n\n    Excludes .git, node_modules, and __pycache__ directories.\n    \"\"\"\n    logger.info(\"Creating tarball of %s -> %s\", source_dir, output_path)\n\n    def _tar_filter(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo | None:\n        parts = Path(tarinfo.name).parts\n        if any(part in _TARBALL_EXCLUDE_DIRS for part in parts):\n            return None\n        return tarinfo\n\n    with tarfile.open(output_path, \"w:\") as tf:\n        tf.add(source_dir, arcname=\".\", filter=_tar_filter)\n\n\ndef _artifact_exists(\n    build_api_host: str,\n    deployment_name: str,\n    build_id: str,\n    auth_token: str,\n) -> bool:\n    \"\"\"Check if a build artifact already exists in S3 via the Build API.\"\"\"\n    url = f\"http://{build_api_host}/deployments/{deployment_name}/builds/{build_id}\"\n    try:\n        response = httpx.head(\n            url,\n            headers={\"Authorization\": f\"Bearer {auth_token}\"},\n            timeout=30.0,\n        )\n        if response.status_code == 503:\n            raise RuntimeError(\n                \"Build artifact storage not configured on the control plane. \"\n                \"Ensure S3_BUCKET is set.\"\n            )\n        if response.status_code == 404:\n            return False\n        if response.status_code == 200:\n            return True\n        # Unexpected status — treat as an error\n        raise RuntimeError(\n            f\"Unexpected status {response.status_code} checking artifact existence\"\n        )\n    except httpx.HTTPError:\n        logger.debug(\"Artifact existence check failed, will proceed with build\")\n        return False\n\n\ndef _upload_artifact(\n    build_api_host: str,\n    deployment_name: str,\n    build_id: str,\n    auth_token: str,\n    tarball_path: str,\n) -> None:\n    \"\"\"Upload a build artifact tarball to the Build API (streamed from disk).\"\"\"\n    url = f\"http://{build_api_host}/deployments/{deployment_name}/builds/{build_id}\"\n    logger.info(\"Uploading artifact to %s\", url)\n\n    size_bytes = os.path.getsize(tarball_path)\n    size_mb = size_bytes / (1024 * 1024)\n    logger.info(\"Artifact size: %.1f MB\", size_mb)\n\n    with open(tarball_path, \"rb\") as f:\n        response = httpx.put(\n            url,\n            content=f,\n            headers={\n                \"Authorization\": f\"Bearer {auth_token}\",\n                \"Content-Type\": \"application/x-tar\",\n                \"Content-Length\": str(size_bytes),\n            },\n            timeout=600.0,  # 10 minute timeout for large artifacts\n        )\n    if response.status_code == 503:\n        raise RuntimeError(\n            \"Build artifact storage not configured on the control plane. \"\n            \"Ensure S3_BUCKET is set.\"\n        )\n    response.raise_for_status()\n    logger.info(\"Artifact uploaded successfully\")\n\n\ndef bootstrap_app_from_repo(\n    target_dir: str = \"/opt/app\",\n) -> None:\n    bootstrap_settings = BootstrapSettings()\n\n    # Download mode: if BUILD_ID is set, download pre-built artifact instead of building\n    if bootstrap_settings.build_id:\n        if not bootstrap_settings.build_api_host:\n            raise ValueError(\n                \"LLAMA_DEPLOY_BUILD_API_HOST is required when BUILD_ID is set\"\n            )\n        if not bootstrap_settings.auth_token:\n            raise ValueError(\"LLAMA_DEPLOY_AUTH_TOKEN is required when BUILD_ID is set\")\n        if not bootstrap_settings.deployment_name:\n            raise ValueError(\n                \"LLAMA_DEPLOY_DEPLOYMENT_NAME is required when BUILD_ID is set\"\n            )\n        logger.info(\n            \"Download mode: build_id=%s deployment=%s\",\n            bootstrap_settings.build_id,\n            bootstrap_settings.deployment_name,\n        )\n        _download_and_extract_artifact(\n            build_api_host=bootstrap_settings.build_api_host,\n            deployment_name=bootstrap_settings.deployment_name,\n            build_id=bootstrap_settings.build_id,\n            auth_token=bootstrap_settings.auth_token,\n            target_dir=target_dir,\n        )\n        return\n\n    # Full bootstrap: clone and build in-place\n    repo_url = bootstrap_settings.repo_url\n    if repo_url is None:\n        raise ValueError(\"repo_url is required to bootstrap\")\n    clone_repo(\n        repository_url=repo_url,\n        git_ref=bootstrap_settings.git_ref,\n        git_sha=bootstrap_settings.git_sha,\n        basic_auth=bootstrap_settings.auth_token,\n        dest_dir=target_dir,\n        depth=1\n        if bootstrap_settings.git_sha is None and bootstrap_settings.git_ref is not None\n        else None,\n    )\n    # Ensure target_dir exists locally when running tests outside a container\n    os.makedirs(target_dir, exist_ok=True)\n    os.chdir(target_dir)\n    configure_settings(\n        app_root=Path(target_dir),\n        deployment_file_path=Path(bootstrap_settings.deployment_file_path),\n    )\n    config = get_deployment_config()\n    load_environment_variables(config, settings.resolved_config_parent)\n    # Fail fast if required env vars are missing\n    validate_required_env_vars(config)\n\n    sdists = None\n    if bootstrap_settings.bootstrap_sdists:\n        sdists = [\n            Path(bootstrap_settings.bootstrap_sdists) / f\n            for f in os.listdir(bootstrap_settings.bootstrap_sdists)\n        ]\n        sdists = [f for f in sdists if f.is_file() and f.name.endswith(\".tar.gz\")]\n        if not sdists:\n            sdists = None\n\n    # If a specific appserver version is requested and it differs from the\n    # version bundled in this image, discard baked-in sdists so the install\n    # step fetches the correct version from PyPI instead.\n    target_version: str | None = bootstrap_settings.appserver_version\n    if target_version and sdists:\n        bundled_version = pkg_version(\"llama-agents-appserver\")\n        if bundled_version != target_version:\n            logger.info(\n                \"Pinned appserver version %s differs from bundled %s; \"\n                \"will fetch from PyPI instead of using sdists\",\n                target_version,\n                bundled_version,\n            )\n            sdists = None\n\n    # Use the explicit base path rather than relying on global settings so tests\n    # can safely mock configure_settings without affecting call arguments.\n    inject_appserver_into_target(\n        config,\n        settings.resolved_config_parent,\n        sdists,\n        target_version=target_version,\n        auto_upgrade=False,\n    )\n    install_ui(config, settings.resolved_config_parent)\n    build_ui(settings.resolved_config_parent, config, settings)\n\n\ndef run_build(target_dir: str = \"/opt/app\") -> None:\n    \"\"\"Run the full build process: bootstrap + package + upload.\"\"\"\n    setup_logging()\n\n    settings = BootstrapSettings()\n    build_id = settings.build_id\n    build_api_host = settings.build_api_host\n    auth_token = settings.auth_token\n    deployment_name = settings.deployment_name\n\n    if not build_id:\n        raise ValueError(\"LLAMA_DEPLOY_BUILD_ID is required for build mode\")\n    if not build_api_host:\n        raise ValueError(\"LLAMA_DEPLOY_BUILD_API_HOST is required for build mode\")\n    if not auth_token:\n        raise ValueError(\"LLAMA_DEPLOY_AUTH_TOKEN is required for build mode\")\n    if not deployment_name:\n        raise ValueError(\"LLAMA_DEPLOY_DEPLOYMENT_NAME is required for build mode\")\n\n    logger.info(\"Starting build: deployment=%s build_id=%s\", deployment_name, build_id)\n\n    # Step 0: Check if the artifact already exists in S3 — skip the build if so\n    if _artifact_exists(build_api_host, deployment_name, build_id, auth_token):\n        logger.info(\"Artifact already exists, skipping build: build_id=%s\", build_id)\n        return\n\n    # Step 1: Run the standard bootstrap (clone, install deps, build UI).\n    # Temporarily clear BUILD_ID so bootstrap_app_from_repo takes the full\n    # clone-and-build path instead of the download-artifact shortcut.\n    saved_build_id = os.environ.pop(\"LLAMA_DEPLOY_BUILD_ID\", None)\n    try:\n        bootstrap_app_from_repo(target_dir)\n    finally:\n        if saved_build_id is not None:\n            os.environ[\"LLAMA_DEPLOY_BUILD_ID\"] = saved_build_id\n\n    # Step 2: Package into tarball\n    tarball_path = \"/tmp/build-artifact.tar.gz\"\n    _create_tarball(target_dir, tarball_path)\n\n    # Step 3: Upload to Build API\n    _upload_artifact(\n        build_api_host=build_api_host,\n        deployment_name=deployment_name,\n        build_id=build_id,\n        auth_token=auth_token,\n        tarball_path=tarball_path,\n    )\n\n    logger.info(\"Build completed successfully: build_id=%s\", build_id)\n\n\nif __name__ == \"__main__\":\n    setup_logging()\n    bootstrap_app_from_repo()\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/build.py",
    "content": "\"\"\"\nBuild entry point for creating deployment artifacts.\n\nUsage: python -m llama_deploy.appserver.build\n\"\"\"\n\nfrom llama_agents.appserver.bootstrap import run_build\n\nif __name__ == \"__main__\":\n    run_build()\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/configure_logging.py",
    "content": "import logging\nimport logging.config\nimport os\nimport time\nfrom contextlib import asynccontextmanager\nfrom contextvars import ContextVar\nfrom typing import Any, AsyncGenerator, Awaitable, Callable\n\nimport structlog\nfrom fastapi import FastAPI, Request, Response\nfrom llama_agents.appserver.correlation_id import (\n    create_correlation_id,\n    get_correlation_id,\n    set_correlation_id,\n)\nfrom llama_agents.appserver.process_utils import should_use_color\nfrom structlog.dev import RichTracebackFormatter\n\naccess_logger = logging.getLogger(\"app.access\")\n\n\ndef _get_or_create_correlation_id(request: Request) -> str:\n    return request.headers.get(\"X-Request-ID\", create_correlation_id())\n\n\ndef add_log_middleware(app: FastAPI) -> None:\n    @app.middleware(\"http\")\n    async def add_log_id(\n        request: Request, call_next: Callable[[Request], Awaitable[Response]]\n    ) -> Response:\n        set_correlation_id(_get_or_create_correlation_id(request))\n        return await call_next(request)\n\n    @app.middleware(\"http\")\n    async def access_log_middleware(\n        request: Request, call_next: Callable[[Request], Awaitable[Response]]\n    ) -> Response:\n        if _is_proxy_request(request) or _is_health_request(request):\n            return await call_next(request)\n        start = time.perf_counter()\n        response = await call_next(request)\n        dur_ms = (time.perf_counter() - start) * 1000\n        qp = str(request.query_params)\n        if qp:\n            qp = f\"?{qp}\"\n        access_logger.info(\n            f\"{request.method} {request.url.path}{qp}\",\n            extra={\n                \"duration_ms\": round(dur_ms, 2),\n                \"status_code\": response.status_code,\n            },\n        )\n        return response\n\n\ndef _add_request_id(_: Any, __: str, event_dict: dict[str, Any]) -> dict[str, Any]:\n    req_id = get_correlation_id()\n    if req_id and \"request_id\" not in event_dict:\n        event_dict[\"request_id\"] = req_id\n    return event_dict\n\n\ndef _drop_uvicorn_color_message(\n    _: Any, __: str, event_dict: dict[str, Any]\n) -> dict[str, Any]:\n    # Uvicorn injects an ANSI-colored duplicate of the message under this key\n    event_dict.pop(\"color_message\", None)\n    return event_dict\n\n\ndef setup_logging(level: str = \"INFO\") -> None:\n    \"\"\"\n    Configure console logging via structlog with a compact, dev-friendly format.\n    Includes request_id and respects logging.extra.\n    \"\"\"\n    # Choose renderer and timestamp format based on LOG_FORMAT\n    log_format = os.getenv(\"LOG_FORMAT\", \"console\").lower()\n    is_console = log_format == \"console\"\n\n    if log_format == \"json\":\n        renderer = structlog.processors.JSONRenderer()\n        timestamper = structlog.processors.TimeStamper(fmt=\"iso\", key=\"timestamp\")\n    else:\n        renderer = structlog.dev.ConsoleRenderer(\n            colors=should_use_color(),\n            exception_formatter=RichTracebackFormatter(\n                show_locals=False,\n                width=120,\n            ),\n        )\n        timestamper = structlog.processors.TimeStamper(fmt=\"%H:%M:%S\", key=\"timestamp\")\n\n    pre_chain: list[Any] = [\n        structlog.contextvars.merge_contextvars,\n        structlog.stdlib.add_logger_name,\n        structlog.stdlib.add_log_level,\n        timestamper,\n        _add_request_id,\n    ]\n\n    # Ensure stdlib logs (foreign to structlog) also include `extra={...}` fields\n    # and that exceptions/stack info are rendered nicely (esp. for JSON format)\n    foreign_pre_chain = [\n        *pre_chain,\n        structlog.stdlib.ExtraAdder(),\n        *(  # otherwise ConsoleRenderer will render nice rich stack traces\n            [\n                structlog.processors.StackInfoRenderer(),\n                structlog.processors.format_exc_info,\n            ]\n            if not is_console\n            else []\n        ),\n        _drop_uvicorn_color_message,\n    ]\n\n    structlog.configure(\n        processors=[\n            *pre_chain,\n            structlog.stdlib.PositionalArgumentsFormatter(),\n            structlog.stdlib.ExtraAdder(),\n            structlog.processors.StackInfoRenderer(),\n            structlog.processors.format_exc_info,\n            structlog.stdlib.ProcessorFormatter.wrap_for_formatter,\n        ],\n        logger_factory=structlog.stdlib.LoggerFactory(),\n        cache_logger_on_first_use=True,\n    )\n\n    handler = {\n        \"class\": \"logging.StreamHandler\",\n        \"level\": level,\n        \"formatter\": \"console\",\n        \"stream\": \"ext://sys.stdout\",\n    }\n\n    logging.config.dictConfig(\n        {\n            \"version\": 1,\n            \"disable_existing_loggers\": False,\n            \"formatters\": {\n                \"console\": {\n                    \"()\": structlog.stdlib.ProcessorFormatter,\n                    # With Rich, let it handle the final formatting; otherwise use our renderer\n                    \"processor\": renderer,\n                    \"foreign_pre_chain\": foreign_pre_chain,\n                }\n            },\n            \"handlers\": {\"console\": handler, \"default\": handler},\n            \"root\": {\n                \"handlers\": [\"console\"],\n                \"level\": level,\n            },\n            \"loggers\": {\n                \"uvicorn.access\": {  # disable access logging, we have our own access log\n                    \"level\": \"WARNING\",\n                    \"handlers\": [\"console\"],\n                    \"propagate\": False,\n                },\n            },\n        }\n    )\n\n    # Reduce noise from httpx globally, with fine-grained suppression controlled per-request\n    logging.getLogger(\"httpx\").addFilter(_HttpxProxyNoiseFilter())\n\n\n#####################################################################################\n### Proxying through the fastapi server in dev mode is noisy, various suppressions\n###\ndef _is_proxy_request(request: Request) -> bool:\n    parts = request.url.path.split(\"/\")\n    return len(parts) >= 4 and parts[1] == \"deployments\" and parts[3] == \"ui\"\n\n\n_HEALTH_PATHS = {\"/health\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"}\n\n\ndef _is_health_request(request: Request) -> bool:\n    return request.url.path.rstrip(\"/\") in _HEALTH_PATHS\n\n\n_suppress_httpx_logging: ContextVar[bool] = ContextVar(\n    \"suppress_httpx_logging\", default=False\n)\n\n\nclass _HttpxProxyNoiseFilter(logging.Filter):\n    def filter(self, record: logging.LogRecord) -> bool:\n        \"\"\"Return False to drop httpx info/debug logs when suppression is active.\"\"\"\n        try:\n            if record.name.startswith(\"httpx\") and record.levelno <= logging.INFO:\n                return not _suppress_httpx_logging.get()\n        except Exception:\n            return True\n        return True\n\n\n@asynccontextmanager\nasync def suppress_httpx_logs() -> AsyncGenerator[None, None]:\n    _suppress_httpx_logging.set(True)\n    yield\n    _suppress_httpx_logging.set(False)\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/correlation_id.py",
    "content": "import random\nimport string\nfrom contextvars import ContextVar\n\ncorrelation_id_var: ContextVar[str] = ContextVar(\"correlation_id\", default=\"\")\n\n\ndef get_correlation_id() -> str:\n    return correlation_id_var.get()\n\n\ndef set_correlation_id(correlation_id: str) -> None:\n    correlation_id_var.set(correlation_id)\n\n\ndef create_correlation_id() -> str:\n    return random_alphanumeric_string(8)\n\n\n_alphanumeric_chars = string.ascii_letters + string.digits\n\n\ndef random_alphanumeric_string(length: int) -> str:\n    return \"\".join(random.choices(_alphanumeric_chars, k=length))\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/deployment.py",
    "content": "import asyncio\nimport json\nimport logging\nimport os\nfrom typing import Any, Tuple\nfrom urllib.parse import quote_plus\n\nfrom fastapi import FastAPI\nfrom fastapi.responses import RedirectResponse\nfrom llama_agents.appserver.deployment_config_parser import get_deployment_config\nfrom llama_agents.appserver.settings import ApiserverSettings, settings\nfrom llama_agents.appserver.types import generate_id\nfrom llama_agents.appserver.workflow_loader import DEFAULT_SERVICE_ID\nfrom llama_agents.core.deployment_config import DeploymentConfig\nfrom llama_agents.server import (\n    AbstractWorkflowStore,\n    AgentDataStore,\n    MemoryWorkflowStore,\n    SqliteWorkflowStore,\n    WorkflowServer,\n)\nfrom starlette.requests import Request\nfrom starlette.responses import HTMLResponse\nfrom starlette.routing import Route\nfrom workflows import Context, Workflow\nfrom workflows.handler import WorkflowHandler\n\nlogger = logging.getLogger()\n\n\nclass DeploymentError(Exception): ...\n\n\nclass Deployment:\n    def __init__(\n        self,\n        workflows: dict[str, Workflow],\n    ) -> None:\n        \"\"\"Creates a Deployment instance.\n\n        Args:\n            config: The configuration object defining this deployment\n            root_path: The path on the filesystem used to store deployment data\n            local: Whether the deployment is local. If true, sources won't be synced\n        \"\"\"\n\n        self._default_service: Workflow | None = workflows.get(DEFAULT_SERVICE_ID)\n        self._service_tasks: list[asyncio.Task] = []\n        # Ready to load services\n        self._workflow_services: dict[str, Workflow] = workflows\n        self._contexts: dict[str, Context] = {}\n        self._handlers: dict[str, WorkflowHandler] = {}\n        self._handler_inputs: dict[str, str] = {}\n\n    @property\n    def default_service(self) -> Workflow | None:\n        \"\"\"Return the default workflow, if any.\"\"\"\n        return self._default_service\n\n    @property\n    def service_names(self) -> list[str]:\n        \"\"\"Returns the list of service names in this deployment.\"\"\"\n        return list(self._workflow_services.keys())\n\n    async def run_workflow(\n        self, service_id: str, session_id: str | None = None, **run_kwargs: Any\n    ) -> Any:\n        workflow = self._workflow_services[service_id]\n        if session_id is not None:\n            context = self._contexts[session_id]\n            return await workflow.run(context=context, **run_kwargs)\n\n        if run_kwargs:\n            return await workflow.run(**run_kwargs)\n\n        return await workflow.run()\n\n    def run_workflow_no_wait(\n        self, service_id: str, session_id: str | None = None, **run_kwargs: Any\n    ) -> Tuple[str, str]:\n        workflow = self._workflow_services[service_id]\n        if session_id is not None:\n            context = self._contexts[session_id]\n            handler = workflow.run(context=context, **run_kwargs)\n        else:\n            handler = workflow.run(**run_kwargs)\n            session_id = generate_id()\n            self._contexts[session_id] = handler.ctx or Context(workflow)\n\n        handler_id = generate_id()\n        self._handlers[handler_id] = handler\n        self._handler_inputs[handler_id] = json.dumps(run_kwargs)\n        assert session_id is not None\n        return handler_id, session_id\n\n    def create_workflow_server(\n        self, deployment_config: DeploymentConfig, settings: ApiserverSettings\n    ) -> WorkflowServer:\n        persistence: AbstractWorkflowStore = MemoryWorkflowStore()\n        if settings.persistence == \"local\":\n            logger.info(\"Using local sqlite persistence for workflows\")\n            persistence = SqliteWorkflowStore(\n                settings.local_persistence_path or \"workflows.db\"\n            )\n        elif settings.persistence == \"cloud\" or (\n            # default to cloud if api key is present to use\n            settings.persistence is None and os.getenv(\"LLAMA_CLOUD_API_KEY\")\n        ):\n            logger.info(\"Using agent data cloud persistence for workflows\")\n            cloud_name = settings.cloud_persistence_name\n            collection = \"workflow_contexts\"\n            if cloud_name is not None:\n                parts = cloud_name.split(\":\")\n                if len(parts) > 1:\n                    collection = parts[1]\n                deployment_name = parts[0]\n            else:\n                deployment_name = deployment_config.name\n            persistence = AgentDataStore(\n                base_url=os.getenv(\n                    \"LLAMA_CLOUD_BASE_URL\",\n                    \"https://api.cloud.llamaindex.ai\",\n                ),\n                api_key=os.getenv(\"LLAMA_CLOUD_API_KEY\", \"\"),\n                project_id=os.getenv(\"LLAMA_DEPLOY_PROJECT_ID\", \"\"),\n                deployment_name=deployment_name,\n                collection=collection,\n            )\n        else:\n            logger.info(\"Not persisting workflows\")\n        server = WorkflowServer(workflow_store=persistence)\n        for service_id, workflow in self._workflow_services.items():\n            server.add_workflow(service_id, workflow)\n        return server\n\n    def mount_workflow_server(self, app: FastAPI) -> WorkflowServer:\n        config = get_deployment_config()\n        server = self.create_workflow_server(config, settings)\n\n        for route in server.app.routes:\n            # add routes directly rather than mounting, so that we can share a root (only one ASGI app can be mounted at a path)\n            if isinstance(route, Route):\n                app.add_api_route(\n                    f\"/deployments/{{deployment_name}}{route.path}\",\n                    route.endpoint,\n                    name=f\"{config.name}_{route.name}\",\n                    methods=list(route.methods) if route.methods else None,\n                    include_in_schema=True,  # change to false when schemas are added to workflow server\n                    tags=[\"workflows\"],\n                )\n\n        @app.get(\"/debugger\", include_in_schema=False)\n        @app.get(\"/debugger/\", include_in_schema=False)\n        def redirect_to_debugger(request: Request) -> RedirectResponse:\n            return RedirectResponse(\n                \"/debugger/index.html?api=\" + quote_plus(\"/deployments/\" + config.name)\n            )\n\n        @app.get(\"/debugger/index.html\", include_in_schema=False, response_model=None)\n        def serve_debugger(api: str | None = None) -> RedirectResponse | HTMLResponse:\n            if not api:\n                return RedirectResponse(\n                    \"/debugger/index.html?api=\"\n                    + quote_plus(\"/deployments/\" + config.name)\n                )\n            else:\n                return HTMLResponse(\"\"\"<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>workflow-debugger</title>\n    <script type=\"module\" crossorigin src=\"https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@latest/dist/app.js\"></script>\n    <link rel=\"stylesheet\" crossorigin href=\"https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@latest/dist/app.css\">\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n\"\"\")\n\n        return server\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/deployment_config_parser.py",
    "content": "import functools\n\nfrom llama_agents.appserver.settings import BootstrapSettings, settings\nfrom llama_agents.core.deployment_config import DeploymentConfig, read_deployment_config\n\n\n@functools.cache\ndef get_deployment_config() -> DeploymentConfig:\n    base_settings = BootstrapSettings()\n    base = settings.app_root.resolve()\n    name = base_settings.deployment_name\n    parsed = read_deployment_config(base, settings.deployment_file_path)\n    if name is not None:\n        parsed.name = name\n    return parsed\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/interrupts.py",
    "content": "import asyncio\nimport signal\nfrom asyncio import Event\nfrom contextlib import suppress\nfrom typing import Any, Coroutine, TypeVar\n\nshutdown_event = Event()\n\n\ndef setup_interrupts() -> None:\n    loop = asyncio.get_running_loop()\n    for sig in (signal.SIGINT, signal.SIGTERM):\n        loop.add_signal_handler(sig, shutdown_event.set)\n\n\nclass OperationAborted(Exception):\n    \"\"\"Raised when an operation is aborted due to shutdown/interrupt.\"\"\"\n\n\nT = TypeVar(\"T\")\n\n\nasync def wait_or_abort(\n    awaitable: Coroutine[Any, Any, T], shutdown_event: asyncio.Event = shutdown_event\n) -> T:\n    \"\"\"Await an operation, aborting early if shutdown is requested.\n\n    If the shutdown event is set before the awaitable completes, cancel the\n    awaitable and raise OperationAborted. Otherwise, return the awaitable's result.\n    \"\"\"\n    event = shutdown_event\n    if event.is_set():\n        raise OperationAborted()\n\n    op_task: asyncio.Task[T] = asyncio.create_task(awaitable)\n    stop_task = asyncio.create_task(event.wait())\n    try:\n        done, _ = await asyncio.wait(\n            {op_task, stop_task}, return_when=asyncio.FIRST_COMPLETED\n        )\n        if stop_task in done:\n            op_task.cancel()\n            with suppress(asyncio.CancelledError):\n                await op_task\n            raise OperationAborted()\n        # Operation finished first\n        stop_task.cancel()\n        with suppress(asyncio.CancelledError):\n            await stop_task\n        return await op_task\n    finally:\n        # Ensure no leaked tasks if an exception propagates\n        for t in (op_task, stop_task):\n            if not t.done():\n                t.cancel()\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/process_utils.py",
    "content": "import functools\nimport logging\nimport os\nimport platform\nimport subprocess\nimport sys\nimport threading\nfrom dataclasses import dataclass\nfrom typing import Callable, TextIO, Tuple, cast\n\n\ndef run_process(\n    cmd: list[str],\n    *,\n    cwd: os.PathLike | None = None,\n    env: dict[str, str] | None = None,\n    prefix: str | None = None,\n    color_code: str = \"36\",\n    line_transform: Callable[[str], str | None] | None = None,\n    use_tty: bool | None = None,\n) -> None:\n    \"\"\"Run a process and stream its output with optional TTY semantics.\n\n    If use_tty is None, a PTY will be used only when the parent's stdout is a TTY\n    and the platform supports PTYs. When a PTY is used, stdout/stderr are merged.\n    \"\"\"\n    use_pty = _should_use_pty(use_tty)\n    prefixer = _make_prefixer(prefix, color_code, line_transform)\n\n    spawned = _spawn_process(cmd, cwd=cwd, env=env, use_pty=use_pty)\n    threads: list[threading.Thread] = []\n    try:\n        spawned.cleanup()\n        _log_command(cmd, prefixer)\n        threads = _start_stream_threads(spawned.sources, prefixer)\n        ret = spawned.process.wait()\n        if ret != 0:\n            raise subprocess.CalledProcessError(ret, cmd)\n    finally:\n        for t in threads:\n            t.join()\n\n\ndef spawn_process(\n    cmd: list[str],\n    *,\n    cwd: os.PathLike | None = None,\n    env: dict[str, str] | None = None,\n    prefix: str | None = None,\n    color_code: str = \"36\",\n    line_transform: Callable[[str], str | None] | None = None,\n    use_tty: bool | None = None,\n) -> subprocess.Popen:\n    \"\"\"Spawn a process and stream its output in background threads.\n\n    Returns immediately with the Popen object. Streaming threads are daemons.\n    \"\"\"\n    use_pty = _should_use_pty(use_tty)\n    prefixer = _make_prefixer(prefix, color_code, line_transform)\n\n    spawned = _spawn_process(cmd, cwd=cwd, env=env, use_pty=use_pty)\n    spawned.cleanup()\n    _log_command(cmd, prefixer)\n    _start_stream_threads(spawned.sources, prefixer)\n    return spawned.process\n\n\n@functools.cache\ndef _use_color() -> bool:\n    \"\"\"Return True if ANSI colors should be emitted to stdout.\n\n    Respects common environment variables and falls back to TTY detection.\n    \"\"\"\n    force_color = os.environ.get(\"FORCE_COLOR\")\n\n    return sys.stdout.isatty() or force_color is not None and force_color != \"0\"\n\n\ndef _colored_prefix(prefix: str, color_code: str) -> str:\n    return f\"\\x1b[{color_code}m{prefix}\\x1b[0m \" if _use_color() else f\"{prefix} \"\n\n\ndef _make_prefixer(\n    prefix: str | None,\n    color_code: str,\n    line_transform: Callable[[str], str | None] | None = None,\n) -> Callable[[str], str | None]:\n    colored = _colored_prefix(prefix, color_code) if prefix else \"\"\n\n    def _prefixer(line: str) -> str | None:\n        transformed = line_transform(line) if line_transform else line\n        if transformed is None:\n            return None\n        return f\"{colored}{transformed}\"\n\n    return _prefixer\n\n\n# Unified PTY/Pipe strategy helpers\n\n\ndef _should_use_pty(use_tty: bool | None) -> bool:\n    if platform.system() == \"Windows\":\n        return False\n    if use_tty is None:\n        return sys.stdout.isatty()\n    return use_tty and sys.stdout.isatty() and not os.environ.get(\"NO_COLOR\")\n\n\ndef should_use_color() -> bool:\n    return _should_use_pty(None)\n\n\n@dataclass\nclass SpawnProcessResult:\n    process: subprocess.Popen[str] | subprocess.Popen[bytes]\n    sources: list[Tuple[int | TextIO, TextIO]]\n    cleanup: Callable[[], None]\n\n\ndef _spawn_process(\n    cmd: list[str],\n    *,\n    cwd: os.PathLike | None,\n    env: dict[str, str] | None,\n    use_pty: bool,\n) -> SpawnProcessResult:\n    process: subprocess.Popen[str] | subprocess.Popen[bytes]\n    if use_pty:\n        import pty\n\n        master_fd, slave_fd = pty.openpty()\n        process = subprocess.Popen(\n            cmd,\n            env=env,\n            cwd=cwd,\n            stdin=slave_fd,\n            stdout=slave_fd,\n            stderr=slave_fd,\n            close_fds=True,\n        )\n\n        def cleanup() -> None:\n            try:\n                os.close(slave_fd)\n            except OSError:\n                pass\n\n        sources: list[tuple[int | TextIO, TextIO]] = [\n            (master_fd, cast(TextIO, sys.stdout)),\n        ]\n        return SpawnProcessResult(process, sources, cleanup)\n\n    use_shell = False\n    if platform.system() == \"Windows\":\n        use_shell = True\n    process = subprocess.Popen(\n        cmd,\n        env=env,\n        cwd=cwd,\n        stdin=None,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        text=True,\n        encoding=\"utf-8\",\n        shell=use_shell,\n    )\n\n    def cleanup_non_pty() -> None:\n        return None\n\n    assert process.stdout is not None and process.stderr is not None\n    sources = [\n        (cast(int | TextIO, process.stdout), cast(TextIO, sys.stdout)),\n        (cast(int | TextIO, process.stderr), cast(TextIO, sys.stderr)),\n    ]\n    return SpawnProcessResult(process, sources, cleanup_non_pty)\n\n\ndef _stream_source(\n    source: int | TextIO,\n    writer: TextIO,\n    transform: Callable[[str], str | None] | None,\n) -> None:\n    if isinstance(source, int):\n        try:\n            with os.fdopen(\n                source, \"r\", encoding=\"utf-8\", errors=\"replace\", buffering=1\n            ) as f:\n                for line in f:\n                    out = transform(line) if transform else line\n                    if out is not None:\n                        try:\n                            writer.write(out)\n                            writer.flush()\n                        except UnicodeEncodeError:\n                            pass\n        except OSError:\n            # PTY EOF may raise EIO; ignore\n            pass\n    else:\n        for line in iter(source.readline, \"\"):\n            out = transform(line) if transform else line\n            if out is None:\n                continue\n            writer.write(out)\n            writer.flush()\n        try:\n            source.close()\n        except Exception:\n            pass\n\n\ndef _log_command(cmd: list[str], transform: Callable[[str], str | None] | None) -> None:\n    cmd_str = \"> \" + \" \".join(cmd)\n    if transform:\n        transformed = transform(cmd_str)\n        if transformed is not None:\n            cmd_str = transformed\n    sys.stderr.write(cmd_str + \"\\n\")\n\n\ndef _start_stream_threads(\n    sources: list[tuple[int | TextIO, TextIO]],\n    transform: Callable[[str], str | None] | None,\n) -> list[threading.Thread]:\n    threads: list[threading.Thread] = []\n    for src, dst in sources:\n        t = threading.Thread(\n            target=_stream_source, args=(src, dst, transform), daemon=True\n        )\n        t.start()\n        threads.append(t)\n    return threads\n\n\nclass BootstrapHandler(logging.Handler):\n    \"\"\"A logging handler that prints colored-prefixed lines to stderr.\n\n    Matches the visual style of ``run_process`` output so bootstrap messages\n    appear inline with subprocess output.  Once ``setup_logging`` configures\n    structlog, this handler can be removed and the same ``logger.info()``\n    calls will be routed through structlog instead.\n    \"\"\"\n\n    def __init__(\n        self,\n        prefix: str = \"[bootstrap]\",\n        color_code: str = \"33\",\n        level: int = logging.DEBUG,\n    ) -> None:\n        super().__init__(level)\n        self._colored = _colored_prefix(prefix, color_code)\n\n    def emit(self, record: logging.LogRecord) -> None:\n        msg = self.format(record)\n        if record.levelno >= logging.WARNING and _use_color():\n            msg = f\"\\x1b[1m{msg}\\x1b[0m\"\n        sys.stderr.write(f\"{self._colored}{msg}\\n\")\n        sys.stderr.flush()\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/py.typed",
    "content": ""
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/routers/__init__.py",
    "content": "from .deployments import create_deployments_router\nfrom .status import health_router\nfrom .ui_proxy import create_ui_proxy_router\n\n__all__ = [\"create_deployments_router\", \"create_ui_proxy_router\", \"health_router\"]\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/routers/deployments.py",
    "content": "import asyncio\nimport json\nfrom typing import AsyncGenerator\n\nfrom fastapi import (\n    APIRouter,\n    HTTPException,\n)\nfrom fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse\nfrom llama_agents.appserver.deployment import Deployment\nfrom llama_agents.appserver.types import (\n    EventDefinition,\n    SessionDefinition,\n    TaskDefinition,\n    TaskResult,\n    generate_id,\n)\nfrom llama_agents.appserver.workflow_loader import DEFAULT_SERVICE_ID\nfrom workflows import Context\nfrom workflows.context import JsonSerializer\nfrom workflows.handler import WorkflowHandler\n\n\ndef create_base_router(name: str) -> APIRouter:\n    base_router = APIRouter(\n        prefix=\"\",\n    )\n\n    @base_router.get(\"/\", response_model=None, include_in_schema=False)\n    async def root() -> RedirectResponse:\n        return RedirectResponse(f\"/deployments/{name}/\")\n\n    return base_router\n\n\ndef create_deployments_router(name: str, deployment: Deployment) -> APIRouter:\n    deployments_router = APIRouter(\n        prefix=\"/deployments/{name}\",\n    )\n\n    @deployments_router.post(\"/tasks/run\", include_in_schema=False)\n    async def create_deployment_task(\n        name: str,\n        task_definition: TaskDefinition,\n        session_id: str | None = None,\n    ) -> JSONResponse:\n        \"\"\"Create a task for the deployment, wait for result and delete associated session.\"\"\"\n\n        service_id = task_definition.service_id or DEFAULT_SERVICE_ID\n\n        if service_id not in deployment.service_names:\n            raise HTTPException(\n                status_code=404,\n                detail=(\n                    \"There is no default service for this deployment. service_id is required\"\n                    if not task_definition.service_id\n                    else f\"Service '{service_id}' not found in deployment 'deployment_name'\"\n                ),\n            )\n\n        run_kwargs = json.loads(task_definition.input) if task_definition.input else {}\n        result = await deployment.run_workflow(\n            service_id=service_id, session_id=session_id, **run_kwargs\n        )\n        return JSONResponse(result)\n\n    @deployments_router.post(\"/tasks/create\", include_in_schema=False)\n    async def create_deployment_task_nowait(\n        name: str,\n        task_definition: TaskDefinition,\n        session_id: str | None = None,\n    ) -> TaskDefinition:\n        \"\"\"Create a task for the deployment but don't wait for result.\"\"\"\n        service_id = task_definition.service_id or DEFAULT_SERVICE_ID\n        if service_id not in deployment.service_names:\n            raise HTTPException(\n                status_code=404,\n                detail=(\n                    \"There is no default service for this deployment. service_id is required\"\n                    if not task_definition.service_id\n                    else f\"Service '{service_id}' not found in deployment 'deployment_name'\"\n                ),\n            )\n\n        run_kwargs = json.loads(task_definition.input) if task_definition.input else {}\n        handler_id, session_id = deployment.run_workflow_no_wait(\n            service_id=service_id, session_id=session_id, **run_kwargs\n        )\n\n        task_definition.session_id = session_id\n        task_definition.task_id = handler_id\n\n        return task_definition\n\n    @deployments_router.post(\"/tasks/{task_id}/events\", include_in_schema=False)\n    async def send_event(\n        name: str,\n        task_id: str,\n        session_id: str,\n        event_def: EventDefinition,\n    ) -> EventDefinition:\n        \"\"\"Send a human response event to a service for a specific task and session.\"\"\"\n        ctx = deployment._contexts[session_id]\n        serializer = JsonSerializer()\n        event = serializer.deserialize(event_def.event_obj_str)\n        ctx.send_event(event)\n\n        return event_def\n\n    @deployments_router.get(\"/tasks/{task_id}/events\", include_in_schema=False)\n    async def get_events(\n        name: str,\n        session_id: str,\n        task_id: str,\n        raw_event: bool = False,\n    ) -> StreamingResponse:\n        \"\"\"\n        Get the stream of events from a given task and session.\n\n        Args:\n            raw_event (bool, default=False): Whether to return the raw event object\n                or just the event data.\n        \"\"\"\n\n        async def event_stream(handler: WorkflowHandler) -> AsyncGenerator[str, None]:\n            serializer = JsonSerializer()\n            # this will hang indefinitely if done and queue is empty. Bail\n            if (\n                handler.is_done()\n                and handler.ctx is not None\n                and handler.ctx.streaming_queue.empty()\n            ):\n                return\n            async for event in handler.stream_events():\n                data = json.loads(serializer.serialize(event))\n                if raw_event:\n                    yield json.dumps(data) + \"\\n\"\n                else:\n                    yield json.dumps(data.get(\"value\")) + \"\\n\"\n                await asyncio.sleep(0.01)\n            await handler\n\n        return StreamingResponse(\n            event_stream(deployment._handlers[task_id]),\n            media_type=\"application/x-ndjson\",\n        )\n\n    @deployments_router.get(\"/tasks/{task_id}/results\", include_in_schema=False)\n    async def get_task_result(\n        name: str,\n        session_id: str,\n        task_id: str,\n    ) -> TaskResult | None:\n        \"\"\"Get the task result associated with a task and session.\"\"\"\n\n        handler = deployment._handlers[task_id]\n        return TaskResult(task_id=task_id, history=[], result=await handler)\n\n    @deployments_router.get(\"/tasks\", include_in_schema=False)\n    async def get_tasks(name: str) -> list[TaskDefinition]:\n        \"\"\"Get all the tasks from all the sessions in a given deployment.\"\"\"\n\n        tasks: list[TaskDefinition] = []\n        for task_id, handler in deployment._handlers.items():\n            if handler.is_done():\n                continue\n            tasks.append(\n                TaskDefinition(\n                    task_id=task_id,\n                    input=deployment._handler_inputs[task_id],\n                )\n            )\n\n        return tasks\n\n    @deployments_router.get(\"/sessions\", include_in_schema=False)\n    async def get_sessions(name: str) -> list[SessionDefinition]:\n        \"\"\"Get the active sessions in a deployment and service.\"\"\"\n\n        return [SessionDefinition(session_id=k) for k in deployment._contexts.keys()]\n\n    @deployments_router.get(\"/sessions/{session_id}\", include_in_schema=False)\n    async def get_session(\n        name: str,\n        session_id: str,\n    ) -> SessionDefinition:\n        \"\"\"Get the definition of a session by ID.\"\"\"\n\n        return SessionDefinition(session_id=session_id)\n\n    @deployments_router.post(\"/sessions/create\", include_in_schema=False)\n    async def create_session(name: str) -> SessionDefinition:\n        \"\"\"Create a new session for a deployment.\"\"\"\n\n        workflow = deployment.default_service\n        if workflow is None:\n            raise HTTPException(\n                status_code=400,\n                detail=\"There is no default service for this deployment\",\n            )\n        session_id = generate_id()\n        deployment._contexts[session_id] = Context(workflow)\n\n        return SessionDefinition(session_id=session_id)\n\n    @deployments_router.post(\"/sessions/delete\", include_in_schema=False)\n    async def delete_session(\n        name: str,\n        session_id: str,\n    ) -> None:\n        \"\"\"Get the active sessions in a deployment and service.\"\"\"\n\n        deployment._contexts.pop(session_id)\n\n    return deployments_router\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/routers/status.py",
    "content": "from fastapi import APIRouter\nfrom llama_agents.appserver.types import Status, StatusEnum\n\nhealth_router = APIRouter()\n\n\n@health_router.get(\"/health\", include_in_schema=False)\nasync def health() -> Status:\n    return Status(\n        status=StatusEnum.HEALTHY,\n    )\n\n\n@health_router.get(\"/healthz\", include_in_schema=False)\nasync def healthz() -> Status:\n    return Status(status=StatusEnum.HEALTHY)\n\n\n@health_router.get(\"/livez\", include_in_schema=False)\nasync def livez() -> Status:\n    return Status(status=StatusEnum.HEALTHY)\n\n\n@health_router.get(\"/readyz\", include_in_schema=False)\nasync def readyz() -> Status:\n    return Status(status=StatusEnum.HEALTHY)\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/routers/ui_proxy.py",
    "content": "import asyncio\nimport logging\nfrom collections.abc import AsyncGenerator, Sequence\nfrom contextlib import suppress\n\nimport httpx\nimport websockets\nfrom fastapi import (\n    APIRouter,\n    FastAPI,\n    HTTPException,\n    Request,\n    WebSocket,\n)\nfrom fastapi.responses import StreamingResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom llama_agents.appserver.configure_logging import suppress_httpx_logs\nfrom llama_agents.appserver.interrupts import (\n    OperationAborted,\n    shutdown_event,\n    wait_or_abort,\n)\nfrom llama_agents.appserver.settings import ApiserverSettings\nfrom llama_agents.core.client.ssl_util import get_httpx_verify_param\nfrom llama_agents.core.deployment_config import DeploymentConfig\nfrom websockets.typing import Subprotocol\n\nlogger = logging.getLogger(__name__)\n\n\nasync def _ws_proxy(ws: WebSocket, upstream_url: str) -> None:\n    \"\"\"Proxy WebSocket connection to upstream server.\"\"\"\n    if shutdown_event.is_set():\n        await ws.close()\n        return\n\n    # Defer accept until after upstream connects so we can mirror the selected subprotocol\n\n    # Forward most headers except WebSocket-specific ones\n    header_prefix_blacklist = [\"sec-websocket-\"]\n    header_blacklist = {\n        \"host\",\n        \"connection\",\n        \"upgrade\",\n    }\n    hdrs = []\n    for k, v in ws.headers.items():\n        if k.lower() not in header_blacklist:\n            for prefix in header_prefix_blacklist:\n                if k.lower().startswith(prefix):\n                    break\n            else:\n                hdrs.append((k, v))\n\n    try:\n        # Parse subprotocols if present\n        subprotocols: Sequence[Subprotocol] | None = None\n        requested = ws.headers.get(\"sec-websocket-protocol\")\n        if requested:\n            # Parse comma-separated subprotocols (as plain strings)\n            parsed = [p.strip() for p in requested.split(\",\")]\n            subprotocols = [Subprotocol(p) for p in parsed if p]\n\n        # Open upstream WebSocket connection, offering the same subprotocols\n        async with websockets.connect(\n            upstream_url,\n            additional_headers=hdrs,\n            subprotocols=subprotocols,\n            open_timeout=5,\n        ) as upstream:\n            await ws.accept(subprotocol=upstream.subprotocol)\n\n            async def client_to_upstream() -> None:\n                try:\n                    while True:\n                        msg = await wait_or_abort(ws.receive(), shutdown_event)\n                        if msg[\"type\"] == \"websocket.receive\":\n                            if \"text\" in msg:\n                                await upstream.send(msg[\"text\"])\n                            elif \"bytes\" in msg:\n                                await upstream.send(msg[\"bytes\"])\n                        elif msg[\"type\"] == \"websocket.disconnect\":\n                            break\n                except OperationAborted:\n                    pass\n                except Exception:\n                    pass\n\n            async def upstream_to_client() -> None:\n                try:\n                    while True:\n                        message = await wait_or_abort(upstream.recv(), shutdown_event)\n                        if isinstance(message, str):\n                            await ws.send_text(message)\n                        else:\n                            await ws.send_bytes(message)\n                except OperationAborted:\n                    pass\n                except Exception:\n                    pass\n\n            # Pump both directions concurrently, cancel the peer when one side closes\n            t1 = asyncio.create_task(client_to_upstream())\n            t2 = asyncio.create_task(upstream_to_client())\n            _, pending = await asyncio.wait(\n                {t1, t2}, return_when=asyncio.FIRST_COMPLETED\n            )\n            for task in pending:\n                task.cancel()\n                with suppress(asyncio.CancelledError):\n                    await task\n\n            # On shutdown, proactively close both sides to break any remaining waits\n            if shutdown_event.is_set():\n                with suppress(Exception):\n                    await ws.close()\n                with suppress(Exception):\n                    await upstream.close()\n\n    except Exception as e:\n        logger.error(f\"WebSocket proxy error: {e}\")\n        # Accept then close so clients (and TestClient) don't error on enter\n        with suppress(Exception):\n            await ws.accept()\n        with suppress(Exception):\n            await ws.close()\n    finally:\n        try:\n            await ws.close()\n        except Exception as e:\n            logger.debug(f\"Error closing client connection: {e}\")\n\n\ndef create_ui_proxy_router(name: str, port: int) -> APIRouter:\n    deployment_router = APIRouter(\n        prefix=f\"/deployments/{name}\",\n        tags=[\"deployments\"],\n    )\n\n    @deployment_router.websocket(\"/ui/{path:path}\")\n    @deployment_router.websocket(\"/ui\")\n    async def websocket_proxy(\n        websocket: WebSocket,\n        path: str | None = None,\n    ) -> None:\n        # Build the upstream WebSocket URL using FastAPI's extracted path parameter\n        slash_path = f\"/{path}\" if path is not None else \"\"\n        upstream_path = f\"/deployments/{name}/ui{slash_path}\"\n\n        # Convert to WebSocket URL\n        upstream_url = f\"ws://localhost:{port}{upstream_path}\"\n        if websocket.url.query:\n            upstream_url += f\"?{websocket.url.query}\"\n\n        await _ws_proxy(websocket, upstream_url)\n\n    @deployment_router.api_route(\n        \"/ui/{path:path}\",\n        methods=[\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\", \"HEAD\", \"PATCH\"],\n        include_in_schema=False,\n    )\n    @deployment_router.api_route(\n        \"/ui\",\n        methods=[\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\", \"HEAD\", \"PATCH\"],\n        include_in_schema=False,\n    )\n    async def proxy(\n        request: Request,\n        path: str | None = None,\n    ) -> StreamingResponse:\n        # Build the upstream URL using FastAPI's extracted path parameter\n        slash_path = f\"/{path}\" if path else \"\"\n        upstream_path = f\"/deployments/{name}/ui{slash_path}\"\n\n        upstream_url = httpx.URL(f\"http://localhost:{port}{upstream_path}\").copy_with(\n            params=request.query_params\n        )\n\n        # Debug logging\n        logger.debug(f\"Proxying {request.method} {request.url} -> {upstream_url}\")\n\n        # Strip hop-by-hop headers + host\n        hop_by_hop = {\n            \"connection\",\n            \"keep-alive\",\n            \"proxy-authenticate\",\n            \"proxy-authorization\",\n            \"te\",  # codespell:ignore\n            \"trailers\",\n            \"transfer-encoding\",\n            \"upgrade\",\n            \"host\",\n        }\n        headers = {\n            k: v for k, v in request.headers.items() if k.lower() not in hop_by_hop\n        }\n\n        try:\n            client = httpx.AsyncClient(timeout=None, verify=get_httpx_verify_param())\n\n            req = client.build_request(\n                request.method,\n                upstream_url,\n                headers=headers,\n                content=request.stream(),  # stream uploads\n            )\n            async with suppress_httpx_logs():\n                upstream = await client.send(req, stream=True)\n\n            resp_headers = {\n                k: v for k, v in upstream.headers.items() if k.lower() not in hop_by_hop\n            }\n\n            # Stream downloads and ensure cleanup in the generator's finally block\n            async def upstream_body() -> AsyncGenerator[bytes, None]:\n                try:\n                    async for chunk in upstream.aiter_raw():\n                        yield chunk\n                finally:\n                    try:\n                        await upstream.aclose()\n                    finally:\n                        await client.aclose()\n\n            return StreamingResponse(\n                upstream_body(),\n                status_code=upstream.status_code,\n                headers=resp_headers,\n            )\n\n        except httpx.ConnectError:\n            raise HTTPException(status_code=502, detail=\"Upstream server unavailable\")\n        except httpx.TimeoutException:\n            raise HTTPException(status_code=504, detail=\"Upstream server timeout\")\n        except Exception as e:\n            logger.error(f\"Proxy error: {e}\")\n            raise HTTPException(status_code=502, detail=\"Proxy error\")\n\n    return deployment_router\n\n\ndef mount_static_files(\n    app: FastAPI, config: DeploymentConfig, settings: ApiserverSettings\n) -> None:\n    build_output = config.build_output_path()\n    if build_output is None:\n        return\n    path = settings.app_root / build_output\n\n    if not path.exists():\n        return\n\n    # Serve index.html when accessing the directory path\n    app.mount(\n        f\"/deployments/{config.name}/ui\",\n        StaticFiles(directory=str(path), html=True),\n        name=f\"ui-static-{config.name}\",\n    )\n    return None\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/settings.py",
    "content": "import os\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom llama_agents.core.config import DEFAULT_DEPLOYMENT_FILE_PATH\nfrom llama_agents.core.deployment_config import resolve_config_parent\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass BootstrapSettings(BaseSettings):\n    \"\"\"\n    Settings configurable via env vars for controlling how an application is\n    created from a git repository.\n    \"\"\"\n\n    model_config = SettingsConfigDict(env_prefix=\"LLAMA_DEPLOY_\")\n    repo_url: str | None = Field(\n        default=None, description=\"The URL of the git repository to clone\"\n    )\n    auth_token: str | None = Field(\n        default=None, description=\"The token to use to clone the git repository\"\n    )\n    git_ref: str | None = Field(\n        default=None, description=\"The git reference to checkout\"\n    )\n    git_sha: str | None = Field(default=None, description=\"The git SHA to checkout\")\n    deployment_file_path: str = Field(\n        default=\".\",\n        description=\"The path to the deployment file, relative to the root of the repository\",\n    )\n    deployment_name: str | None = Field(\n        default=None, description=\"The name of the deployment\"\n    )\n    bootstrap_sdists: str | None = Field(\n        default=None,\n        description=\"A directory containing tar.gz sdists to install instead of installing the appserver\",\n    )\n    build_id: str | None = None\n    build_api_host: str | None = None\n    appserver_version: str | None = Field(\n        default=None,\n        description=\"The pinned appserver version to install (from LLAMA_DEPLOY_APPSERVER_VERSION)\",\n    )\n\n\nclass ApiserverSettings(BaseSettings):\n    model_config = SettingsConfigDict(env_prefix=\"LLAMA_DEPLOY_APISERVER_\")\n\n    host: str = Field(\n        default=\"127.0.0.1\",\n        description=\"The host where to run the API Server\",\n    )\n    port: int = Field(\n        default=4501,\n        description=\"The TCP port where to bind the API Server\",\n    )\n\n    app_root: Path = Field(\n        default=Path(\".\"),\n        description=\"The root of the application\",\n    )\n\n    deployment_file_path: Path = Field(\n        default=Path(DEFAULT_DEPLOYMENT_FILE_PATH),\n        description=\"path, relative to the repository root, where the pyproject.toml file is located\",\n    )\n\n    proxy_ui: bool = Field(\n        default=False,\n        description=\"If true, proxy a development UI server instead of serving built assets\",\n    )\n    proxy_ui_port: int = Field(\n        default=4502,\n        description=\"The TCP port where to bind the UI proxy server\",\n    )\n\n    reload: bool = Field(\n        default=False,\n        description=\"If true, reload the workflow modules, for use in a dev server environment\",\n    )\n\n    persistence: Literal[\"memory\", \"local\", \"cloud\"] | None = Field(\n        default=None,\n        description=\"The persistence mode to use for the workflow server\",\n    )\n    local_persistence_path: str | None = Field(\n        default=None,\n        description=\"The path to the sqlite database to use for the workflow server\",\n    )\n    cloud_persistence_name: str | None = Field(\n        default=None,\n        description=\"Agent Data deployment name to use for workflow persistence. May optionally include a `:` delimited collection name, e.g. 'my_agent:my_collection'. Leave none to use the current deployment name. Recommended to override with _public if running locally, and specify a collection name\",\n    )\n\n    @property\n    def resolved_config_parent(self) -> Path:\n        return resolve_config_parent(self.app_root, self.deployment_file_path)\n\n\nsettings = ApiserverSettings()\n\n\ndef configure_settings(\n    proxy_ui: bool | None = None,\n    deployment_file_path: Path | None = None,\n    app_root: Path | None = None,\n    reload: bool | None = None,\n    persistence: Literal[\"memory\", \"local\", \"cloud\"] | None = None,\n    local_persistence_path: str | None = None,\n    cloud_persistence_name: str | None = None,\n    host: str | None = None,\n) -> None:\n    if proxy_ui is not None:\n        settings.proxy_ui = proxy_ui\n        os.environ[\"LLAMA_DEPLOY_APISERVER_PROXY_UI\"] = \"true\" if proxy_ui else \"false\"\n    if deployment_file_path is not None:\n        settings.deployment_file_path = deployment_file_path\n        os.environ[\"LLAMA_DEPLOY_APISERVER_DEPLOYMENT_FILE_PATH\"] = str(\n            deployment_file_path\n        )\n    if app_root is not None:\n        settings.app_root = app_root\n        os.environ[\"LLAMA_DEPLOY_APISERVER_APP_ROOT\"] = str(app_root)\n    if reload is not None:\n        settings.reload = reload\n        os.environ[\"LLAMA_DEPLOY_APISERVER_RELOAD\"] = \"true\" if reload else \"false\"\n    if persistence is not None:\n        settings.persistence = persistence\n        os.environ[\"LLAMA_DEPLOY_APISERVER_PERSISTENCE\"] = persistence\n    if local_persistence_path is not None:\n        settings.local_persistence_path = local_persistence_path\n        os.environ[\"LLAMA_DEPLOY_APISERVER_LOCAL_PERSISTENCE_PATH\"] = (\n            local_persistence_path\n        )\n    if cloud_persistence_name is not None:\n        settings.cloud_persistence_name = cloud_persistence_name\n        os.environ[\"LLAMA_DEPLOY_APISERVER_CLOUD_PERSISTENCE_NAME\"] = (\n            cloud_persistence_name\n        )\n    if host is not None:\n        settings.host = host\n        os.environ[\"LLAMA_DEPLOY_APISERVER_HOST\"] = host\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/stats.py",
    "content": "from prometheus_client import Enum\n\napiserver_state = Enum(\n    \"apiserver_state\",\n    \"Current state of the API server\",\n    states=[\n        \"starting\",\n        \"running\",\n        \"stopped\",\n    ],\n)\n\ndeployment_state = Enum(\n    \"deployment_state\",\n    \"Current state of a deployment\",\n    [\"deployment_name\"],\n    states=[\n        \"loading_services\",\n        \"ready\",\n        \"starting_services\",\n        \"running\",\n        \"stopped\",\n    ],\n)\n\nservice_state = Enum(\n    \"service_state\",\n    \"Current state of a service attached to a deployment\",\n    [\"deployment_name\", \"service_name\"],\n    states=[\n        \"loading\",\n        \"syncing\",\n        \"installing\",\n        \"ready\",\n    ],\n)\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/types.py",
    "content": "import uuid\nfrom enum import Enum\n\nfrom pydantic import BaseModel, Field\n\n\ndef generate_id() -> str:\n    return str(uuid.uuid4())\n\n\nclass TaskDefinition(BaseModel):\n    \"\"\"\n    The definition and state of a task.\n\n    Attributes:\n        input (str):\n            The task input.\n        session_id (str):\n            The session ID that the task belongs to.\n        task_id (str):\n            The task ID. Defaults to a random UUID.\n        service_id (str):\n            The service ID that the task should be sent to.\n            If blank, the orchestrator decides.\n    \"\"\"\n\n    input: str\n    task_id: str = Field(default_factory=generate_id)\n    session_id: str | None = None\n    service_id: str | None = None\n\n\nclass SessionDefinition(BaseModel):\n    \"\"\"\n    The definition of a session.\n\n    Attributes:\n        session_id (str):\n            The session ID. Defaults to a random UUID.\n        task_definitions (list[str]):\n            The task ids in order, representing the session.\n        state (dict):\n            The current session state.\n    \"\"\"\n\n    session_id: str = Field(default_factory=generate_id)\n    task_ids: list[str] = Field(default_factory=list)\n    state: dict = Field(default_factory=dict)\n\n\nclass EventDefinition(BaseModel):\n    \"\"\"The definition of event.\n\n    To be used as payloads for service endpoints when wanting to send serialized\n    Events.\n\n    Attributes:\n        event_object_str (str): serialized string of event.\n    \"\"\"\n\n    service_id: str\n    event_obj_str: str\n\n\nclass TaskResult(BaseModel):\n    \"\"\"\n    The result of a task.\n\n    Attributes:\n        task_id (str):\n            The task ID.\n        history (list[str]):\n            The task history.\n        result (str):\n            The task result.\n        data (dict):\n            Additional data about the task or result.\n    \"\"\"\n\n    task_id: str\n    history: list[str]\n    result: str\n    data: dict = Field(default_factory=dict)\n\n\nclass StatusEnum(Enum):\n    HEALTHY = \"Healthy\"\n    UNHEALTHY = \"Unhealthy\"\n    DOWN = \"Down\"\n\n\nclass Status(BaseModel):\n    status: StatusEnum\n\n\nclass DeploymentDefinition(BaseModel):\n    name: str\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_agents/appserver/workflow_loader.py",
    "content": "import configparser\nimport functools\nimport importlib\nimport logging\nimport os\nimport socket\nimport subprocess\nimport sys\nfrom dataclasses import dataclass\nfrom importlib.metadata import requires as pkg_requires\nfrom importlib.metadata import version as pkg_version\nfrom pathlib import Path\nfrom textwrap import dedent\n\nfrom dotenv import dotenv_values\nfrom llama_agents.appserver.process_utils import (\n    BootstrapHandler,\n    run_process,\n    spawn_process,\n)\nfrom llama_agents.appserver.settings import ApiserverSettings, settings\nfrom llama_agents.core.deployment_config import DeploymentConfig\nfrom llama_agents.core.ui_build import ui_build_output_path\nfrom llama_agents.server import WorkflowServer\nfrom packaging.requirements import Requirement\nfrom packaging.specifiers import SpecifierSet\nfrom packaging.version import InvalidVersion, Version\nfrom workflows import Workflow\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(BootstrapHandler(prefix=\"[install]\", color_code=\"33\"))\nlogger.setLevel(logging.INFO)\n\nDEFAULT_SERVICE_ID = \"default\"\n\n# The last version published under the old \"llama-deploy-appserver\" dist name.\n# Versions after this are published as \"llama-agents-appserver\".\n_LAST_OLD_DIST_VERSION = Version(\"0.5.3\")\n_OLD_DIST_NAME = \"llama-deploy-appserver\"\n_NEW_DIST_NAME = \"llama-agents-appserver\"\n\n\ndef _dist_name_for_version(version: Version) -> str:\n    \"\"\"Return the correct PyPI dist name for a given appserver version.\"\"\"\n    if version <= _LAST_OLD_DIST_VERSION:\n        return _OLD_DIST_NAME\n    return _NEW_DIST_NAME\n\n\ndef load_workflows(config: DeploymentConfig) -> dict[str, Workflow]:\n    \"\"\"\n    Creates WorkflowService instances according to the configuration object.\n\n    \"\"\"\n    workflow_services: dict[str, Workflow] = {}\n\n    if config.app:\n        module_name, app_name = config.app.split(\":\", 1)\n        module = importlib.import_module(module_name)\n        if not hasattr(module, app_name):\n            raise AttributeError(\n                f\"Module '{module_name}' has no attribute '{app_name}'\"\n            )\n        workflow = getattr(module, app_name)\n        if not isinstance(workflow, WorkflowServer):\n            raise ValueError(\n                f\"Workflow {app_name} in {module_name} is not a WorkflowServer object\"\n            )\n        workflow_services = workflow.get_workflows()\n    else:\n        for service_id, workflow_name in config.workflows.items():\n            module_name, workflow_name = workflow_name.split(\":\", 1)\n            module = importlib.import_module(module_name)\n            if not hasattr(module, workflow_name):\n                raise AttributeError(\n                    f\"Module '{module_name}' has no attribute '{workflow_name}'\"\n                )\n            workflow = getattr(module, workflow_name)\n            if not isinstance(workflow, Workflow):\n                logger.warning(\n                    f\"Workflow {workflow_name} in {module_name} is not a Workflow object\",\n                )\n            workflow_services[service_id] = workflow\n\n    return workflow_services\n\n\ndef load_environment_variables(config: DeploymentConfig, source_root: Path) -> None:\n    \"\"\"\n    Load environment variables from the deployment config.\n    \"\"\"\n    for key, value in parse_environment_variables(config, source_root).items():\n        if value:\n            os.environ[key] = value\n\n\ndef validate_required_env_vars(\n    config: DeploymentConfig, *, fill_missing: bool = False\n) -> None:\n    \"\"\"\n    Validate that all required environment variables are present and non-empty.\n\n    Args:\n        config: The deployment configuration containing required_env_vars.\n        fill_missing: If True, fill missing env vars with placeholder values instead\n            of raising an error. This is useful for validation where env vars may be\n            read statically during import time.\n\n    Raises:\n        RuntimeError: If any required env vars are missing or empty and fill_missing is False.\n    \"\"\"\n    required = config.required_env_vars\n    if not required:\n        return\n    missing = [name for name in required if not os.environ.get(name)]\n    if missing:\n        if fill_missing:\n            for name in missing:\n                os.environ[name] = f\"__PLACEHOLDER_{name}__\"\n        else:\n            missing_list = \", \".join(sorted(missing))\n            raise RuntimeError(\n                (\n                    \"Missing required environment variables defined in required_env_vars: \"\n                    f\"{missing_list}. Provide them via your environment, .env files, or the deployment secrets.\"\n                )\n            )\n\n\ndef parse_environment_variables(\n    config: DeploymentConfig, source_root: Path\n) -> dict[str, str]:\n    \"\"\"\n    Parse environment variables from the deployment config.\n    \"\"\"\n    env_vars = {**config.env} if config.env else {}\n    for env_file in config.env_files or []:\n        env_file_path = source_root / env_file\n        values = dotenv_values(env_file_path)\n        str_values = {k: v for k, v in values.items() if isinstance(v, str)}\n        env_vars.update(str_values)\n    return env_vars\n\n\n@functools.cache\ndef are_we_editable_mode() -> bool:\n    \"\"\"\n    Check if we're in editable mode.\n    \"\"\"\n    # Heuristic: if the package path does not include 'site-packages', treat as editable\n    top_level_pkg = \"llama_agents.appserver\"\n    try:\n        pkg = importlib.import_module(top_level_pkg)\n        pkg_path = Path(getattr(pkg, \"__file__\", \"\")).resolve()\n        if not pkg_path.exists():\n            return False\n\n        return \"site-packages\" not in pkg_path.parts\n    except Exception:\n        return False\n\n\ndef inject_appserver_into_target(\n    config: DeploymentConfig,\n    source_root: Path,\n    sdists: list[Path] | None = None,\n    target_version: str | None = None,\n    auto_upgrade: bool = True,\n) -> None:\n    \"\"\"\n    Ensures uv, and uses it to add the appserver as a dependency to the target app.\n    - If sdists are provided, they will be installed directly for offline-ish installs (still fetches dependencies)\n    - If the appserver is currently editable, it will be installed directly from the source repo\n    - otherwise fetches the current version from pypi\n\n    Args:\n        config: The deployment config\n        source_root: The root directory of the deployment\n        sdists: A list of tar.gz sdists files to install instead of installing the appserver\n        auto_upgrade: If True, auto-upgrade dependencies (e.g. llama-index-workflows) to\n            be compatible with the appserver. Should be False during container bootstrap\n            to avoid modifying the target project's pyproject.toml.\n    \"\"\"\n    path = settings.resolved_config_parent\n    logger.info(f\"Ensuring venv at {path} and adding appserver\")\n    _ensure_uv_available()\n    _install_and_add_appserver_if_missing(\n        path,\n        source_root,\n        sdists=sdists,\n        target_version=target_version,\n        auto_upgrade=auto_upgrade,\n    )\n\n\ndef _uv_run_python(cwd: Path, snippet: str, *, stderr: int | None = None) -> str:\n    \"\"\"\n    Run a python snippet via ``uv run`` inside the project venv uv resolves for\n    ``cwd``. Returns stripped stdout. Use this anywhere we need to probe the\n    target venv (installed version, ``sys.prefix``, etc.) so all probes agree\n    with what the start-time runners see.\n    \"\"\"\n    result = subprocess.check_output(\n        [\"uv\", \"run\", \"--no-progress\", \"python\", \"-c\", snippet],\n        cwd=cwd,\n        stderr=stderr,\n    )\n    return result.decode(\"utf-8\").strip()\n\n\ndef _get_installed_version_within_target(\n    path: Path, package: str = \"llama-agents-appserver\"\n) -> Version | None:\n    packages = [package, _OLD_DIST_NAME] if package == _NEW_DIST_NAME else [package]\n    for pkg_name in packages:\n        try:\n            output = _uv_run_python(\n                path,\n                dedent(f\"\"\"\n                        from importlib.metadata import version\n                        try:\n                            print(version(\"{pkg_name}\"))\n                        except Exception:\n                            pass\n                       \"\"\"),\n                stderr=subprocess.DEVNULL,\n            )\n            try:\n                return Version(output)\n            except InvalidVersion:\n                continue\n        except subprocess.CalledProcessError:\n            continue\n    return None\n\n\ndef _get_current_version() -> Version:\n    return Version(pkg_version(\"llama-agents-appserver\"))\n\n\ndef _is_missing_or_outdated(path: Path) -> Version | None:\n    \"\"\"\n    returns the current version if the installed version is missing or outdated, otherwise None\n    \"\"\"\n    installed = _get_installed_version_within_target(path)\n    current = _get_current_version()\n    if installed is None or installed < current:\n        return current\n    return None\n\n\n@functools.cache\ndef _get_appserver_workflows_requirement() -> SpecifierSet | None:\n    \"\"\"Read the appserver's version requirement for llama-index-workflows from package metadata.\"\"\"\n    reqs = pkg_requires(\"llama-agents-appserver\") or []\n    for req_str in reqs:\n        req = Requirement(req_str)\n        if req.name == \"llama-index-workflows\":\n            return req.specifier\n    return None\n\n\ndef _ensure_compatible_workflows(\n    source_root: Path,\n    path: Path,\n) -> None:\n    \"\"\"Check if the user's llama-index-workflows version is compatible with the appserver.\n\n    If incompatible, auto-updates via ``uv add``. If the update fails (e.g. conflicting\n    constraints), raises RuntimeError with a clear message.\n    \"\"\"\n    requirement = _get_appserver_workflows_requirement()\n    if requirement is None:\n        return\n\n    installed = _get_installed_version_within_target(\n        source_root / path, package=\"llama-index-workflows\"\n    )\n    if installed is None:\n        # Not installed — the appserver install will bring it in\n        return\n\n    if installed in requirement:\n        return\n\n    # Version is incompatible — auto-update\n    req_str = str(requirement)\n    logger.warning(\n        f\"⚠️ Updating llama-index-workflows from {installed} to {req_str} \"\n        f\"(required by llama-agents-appserver). \"\n        f\"You can add llamactl as a dev dependency to resolve version conflicts: \"\n        f\"uv add llamactl --dev (then run with uv run llamactl)\"\n    )\n    try:\n        run_uv(\n            source_root,\n            path,\n            \"add\",\n            [f\"llama-index-workflows{req_str}\"],\n        )\n    except subprocess.CalledProcessError:\n        raise RuntimeError(\n            f\"Your project has llama-index-workflows=={installed} which is incompatible \"\n            f\"with this version of the appserver (requires {req_str}). \"\n            f\"Automatic update failed — your project may have conflicting constraints. \"\n            f\"Please update manually: uv add 'llama-index-workflows{req_str}'\"\n        )\n\n\ndef run_uv(\n    source_root: Path,\n    path: Path,\n    cmd: str,\n    args: list[str] = [],\n    extra_env: dict[str, str] | None = None,\n) -> None:\n    env = os.environ.copy()\n    if extra_env:\n        env.update(extra_env)\n    run_process(\n        [\"uv\", cmd] + args,\n        cwd=source_root / path,\n        prefix=f\"[uv {cmd}]\",\n        color_code=\"36\",\n        use_tty=False,\n        line_transform=_exclude_venv_warning,\n        env=env,\n    )\n\n\ndef _resolve_project_venv(source_root: Path, path: Path) -> Path:\n    \"\"\"\n    Return the venv path uv would use for this project.\n\n    Must be called after ``uv sync`` so the venv is guaranteed to exist. Asks uv\n    itself rather than reimplementing uv's workspace / project resolution, so the\n    install side stays aligned with ``start_*_in_target_venv`` (also bare ``uv run``).\n    \"\"\"\n    return Path(_uv_run_python(source_root / path, \"import sys; print(sys.prefix)\"))\n\n\ndef _install_and_add_appserver_if_missing(\n    path: Path,\n    source_root: Path,\n    save_version: bool = False,\n    sdists: list[Path] | None = None,\n    target_version: str | None = None,\n    auto_upgrade: bool = True,\n) -> None:\n    \"\"\"\n    Sync project deps (letting uv pick the venv location, so we agree with uv run\n    in workspace and non-workspace layouts) and install the appserver if missing\n    or outdated.\n    \"\"\"\n\n    if not (source_root / path / \"pyproject.toml\").exists():\n        logger.warning(\n            f\"No pyproject.toml found at {source_root / path}, skipping appserver injection. The server will likely not be able to install your workflows.\"\n        )\n        return\n\n    editable = are_we_editable_mode()\n    run_uv(\n        source_root,\n        path,\n        \"sync\",\n        [\"--no-dev\", \"--inexact\"],\n    )\n    venv_path = _resolve_project_venv(source_root, path)\n\n    if auto_upgrade:\n        _ensure_compatible_workflows(source_root, path)\n\n    if sdists:\n        run_uv(\n            source_root,\n            path,\n            \"pip\",\n            [\"install\"]\n            + [str(s.absolute()) for s in sdists]\n            + [\"--prefix\", str(venv_path)],\n        )\n    elif editable:\n        same_python_version = _same_python_version(venv_path)\n        if not same_python_version.is_same:\n            msg = (\n                f\"Python version mismatch at {venv_path}: runtime \"\n                f\"{same_python_version.current_version} != venv \"\n                f\"{same_python_version.target_version}\"\n            )\n            logger.error(\n                f\"{msg}. In editable-appserver mode the target venv must run \"\n                f\"the same Python as the appserver process, otherwise the \"\n                f\"appserver cannot be installed.\"\n            )\n            raise RuntimeError(msg)\n        pyproject = _find_development_pyproject()\n        if pyproject is None:\n            raise RuntimeError(\"No pyproject.toml found in llama-agents-appserver\")\n        base = (source_root.resolve() / path).resolve()\n        rel = Path(os.path.relpath(pyproject, start=base))\n        target = f\"file://{str(rel)}\"\n\n        run_uv(\n            source_root,\n            path,\n            \"pip\",\n            [\n                \"install\",\n                \"--reinstall-package\",\n                \"llama-agents-appserver\",\n                target,\n                \"--prefix\",\n                str(venv_path),\n            ],\n        )\n\n    else:\n        if target_version:\n            # Explicit version requested (e.g. pinned deployment) — install\n            # exactly that version from PyPI, skipping the outdated check.\n            install_version = Version(target_version)\n        else:\n            install_version = _is_missing_or_outdated(path)\n        if install_version is not None:\n            dist_name = _dist_name_for_version(install_version)\n            if save_version and not target_version:\n                run_uv(\n                    source_root,\n                    path,\n                    \"add\",\n                    [f\"{dist_name}>={install_version}\"],\n                )\n            else:\n                run_uv(\n                    source_root,\n                    path,\n                    \"pip\",\n                    [\n                        \"install\",\n                        f\"{dist_name}=={install_version}\",\n                        \"--prefix\",\n                        str(venv_path),\n                    ],\n                )\n\n\ndef _find_development_pyproject() -> Path | None:\n    dir = Path(__file__).parent.resolve()\n    while not (dir / \"pyproject.toml\").exists():\n        dir = dir.parent\n        if dir == dir.root:\n            return None\n    return dir\n\n\ndef _exclude_venv_warning(line: str) -> str | None:\n    if \"use `--active` to target the active environment instead\" in line:\n        return None\n    return line\n\n\ndef _ensure_uv_available() -> None:\n    # Check if uv is available on the path\n    uv_available = False\n    try:\n        subprocess.check_call(\n            [\"uv\", \"--version\"],\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.DEVNULL,\n        )\n        uv_available = True\n    except (subprocess.CalledProcessError, FileNotFoundError):\n        pass\n    if not uv_available:\n        # bootstrap uv with pip\n        try:\n            run_process(\n                [\n                    sys.executable,\n                    \"-m\",\n                    \"pip\",\n                    \"install\",\n                    \"uv\",\n                ],\n                prefix=\"[python -m pip]\",\n                color_code=\"31\",  # red\n            )\n        except subprocess.CalledProcessError as e:\n            msg = f\"Unable to install uv. Environment must include uv, or uv must be installed with pip: {e.stderr}\"\n            raise RuntimeError(msg)\n\n\n@dataclass\nclass SamePythonVersionResult:\n    is_same: bool\n    current_version: str\n    target_version: str | None\n\n\ndef _same_python_version(venv_path: Path) -> SamePythonVersionResult:\n    current_version = f\"{sys.version_info.major}.{sys.version_info.minor}\"\n    target_version = None\n    cfg = venv_path / \"pyvenv.cfg\"\n    if cfg.exists():\n        parser = configparser.ConfigParser()\n        parser.read_string(\"[venv]\\n\" + cfg.read_text())\n        ver_str = parser[\"venv\"].get(\"version_info\", \"\").strip()\n        if ver_str:\n            try:\n                v = Version(ver_str)\n                target_version = f\"{v.major}.{v.minor}\"\n            except InvalidVersion:\n                pass\n    return SamePythonVersionResult(\n        is_same=current_version == target_version,\n        current_version=current_version,\n        target_version=target_version,\n    )\n\n\ndef install_ui(config: DeploymentConfig, config_parent: Path) -> None:\n    if config.ui is None:\n        return\n    package_manager = config.ui.package_manager\n    try:\n        run_process(\n            [package_manager, \"install\"],\n            cwd=config_parent / config.ui.directory,\n            prefix=f\"[{package_manager} install]\",\n            color_code=\"33\",\n            # auto download the package manager\n            env={**os.environ.copy(), \"COREPACK_ENABLE_DOWNLOAD_PROMPT\": \"0\"},\n        )\n    except BaseException as e:\n        if \"No such file or directory\" in str(e):\n            raise RuntimeError(\n                f\"Package manager {package_manager} not found. Please download and enable corepack, or install the package manager manually.\"\n            )\n        raise e\n\n\ndef _ui_env(config: DeploymentConfig, settings: ApiserverSettings) -> dict[str, str]:\n    env = os.environ.copy()\n    # Set new canonical name while preserving legacy URL_ID for backwards compatibility\n    if \"LLAMA_DEPLOY_DEPLOYMENT_NAME\" not in env:\n        env[\"LLAMA_DEPLOY_DEPLOYMENT_NAME\"] = config.name\n    env[\"LLAMA_DEPLOY_DEPLOYMENT_URL_ID\"] = config.name\n    env[\"LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH\"] = f\"/deployments/{config.name}/ui\"\n    if config.ui is not None:\n        env[\"PORT\"] = str(settings.proxy_ui_port)\n    env[\"LLAMA_DEPLOY_SERVER_PORT\"] = str(settings.port)\n    # Apply PUBLIC_* overlays: PUBLIC_X overrides X in the UI build env\n    public_prefix = \"PUBLIC_\"\n    public_keys = [k for k in env if k.startswith(public_prefix)]\n    for key in public_keys:\n        base_key = key[len(public_prefix) :]\n        env[base_key] = env[key]\n        del env[key]\n    return env\n\n\ndef build_ui(\n    config_parent: Path, config: DeploymentConfig, settings: ApiserverSettings\n) -> bool:\n    \"\"\"\n    Returns True if the UI was built (and supports building), otherwise False if there's no build command\n    \"\"\"\n    if config.ui is None:\n        return False\n    path = Path(config.ui.directory)\n    env = _ui_env(config, settings)\n\n    has_build = ui_build_output_path(config_parent, config)\n    if has_build is None:\n        return False\n\n    run_process(\n        [\"npm\", \"run\", \"build\"],\n        cwd=config_parent / path,\n        env=env,\n        prefix=\"[npm run build]\",\n        color_code=\"34\",\n    )\n    return True\n\n\ndef start_dev_ui_process(\n    root: Path, settings: ApiserverSettings, config: DeploymentConfig\n) -> None | subprocess.Popen:\n    ui_port = settings.proxy_ui_port\n    ui = config.ui\n    if ui is None:\n        return None\n\n    # If a UI dev server is already listening on the configured port, do not start another\n    def _is_port_open(port: int) -> bool:\n        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:\n            sock.settimeout(0.2)\n            try:\n                return sock.connect_ex((\"127.0.0.1\", port)) == 0\n            except Exception:\n                return False\n\n    if _is_port_open(ui_port):\n        logger.info(\n            f\"Detected process already running on port {ui_port}; not starting a new one.\"\n        )\n        return None\n    # start the ui process\n    env = _ui_env(config, settings)\n    # Transform first 20 lines to replace the default UI port with the main server port\n    line_counter = 0\n\n    def _transform(line: str) -> str:\n        nonlocal line_counter\n        if line_counter < 20:\n            line = line.replace(f\":{ui_port}\", f\":{settings.port}\")\n        line_counter += 1\n        return line\n\n    return spawn_process(\n        [\"npm\", \"run\", ui.serve_command],\n        cwd=root / (ui.directory),\n        env=env,\n        prefix=f\"[npm run {ui.serve_command}]\",\n        color_code=\"35\",\n        line_transform=_transform,\n        use_tty=False,\n    )\n"
  },
  {
    "path": "packages/llama-agents-appserver/src/llama_deploy/appserver/__init__.py",
    "content": "# Backwards-compatibility shim: llama_deploy.appserver -> llama_agents.appserver\nfrom llama_agents.core._alias import install_alias_finder\n\ninstall_alias_finder()\n\nfrom llama_agents.appserver import *  # noqa: E402, F403\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/conftest.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Callable, Iterator\n\nimport pytest\nimport yaml\nfrom fastapi.testclient import TestClient\nfrom llama_agents.appserver import app as app_mod\nfrom llama_agents.appserver.app import app\nfrom llama_agents.appserver.deployment_config_parser import get_deployment_config\nfrom llama_agents.appserver.settings import settings\n\n\n@pytest.fixture(autouse=True)\ndef reset_config_cache(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> Iterator[None]:\n    # Point rc_path to a temp dir by default and clear cached config between tests\n    monkeypatch.setenv(\"LLAMA_DEPLOY_APISERVER_APP_ROOT\", str(tmp_path))\n    settings.app_root = tmp_path\n    get_deployment_config.cache_clear()\n    yield\n    # cleanup env\n    os.environ.pop(\"LLAMA_DEPLOY_APISERVER_APP_ROOT\", None)\n\n\n@pytest.fixture\ndef http_client() -> TestClient:\n    return TestClient(app)\n\n\n@pytest.fixture\ndef write_yaml(tmp_path: Path) -> Iterator[Callable[[str, str], Path]]:\n    def _write(name: str, content: str) -> Path:\n        p = tmp_path / name\n        p.write_text(content)\n        return p\n\n    yield _write\n\n\n@pytest.fixture(autouse=True)\ndef no_browser(monkeypatch: pytest.MonkeyPatch) -> None:\n    # Prevent opening a real browser window during tests by stubbing the helper\n    monkeypatch.setattr(\n        app_mod, \"open_browser_async\", lambda *a, **k: None, raising=True\n    )\n\n\n@pytest.fixture\ndef make_deployment_file(tmp_path: Path) -> Callable[[str, bool], Path]:\n    def _make(name: str = \"myapp\", with_ui: bool = False) -> Path:\n        content = {\n            \"name\": name,\n            \"services\": {},\n        }\n        if with_ui:\n            content[\"ui\"] = {\n                \"source\": {\n                    \"location\": \"ui\",\n                },\n                \"proxy_port\": 3000,\n            }\n        path = tmp_path / \"llama_deploy.yaml\"\n        path.write_text(yaml.dump(content))\n        return path\n\n    return _make\n\n\n@pytest.fixture\ndef process_stub() -> object:\n    class _Proc:\n        def __init__(self) -> None:\n            self.terminated = False\n\n        def terminate(self) -> None:\n            self.terminated = True\n\n    return _Proc()\n\n\n@pytest.fixture\ndef proc_with_poll_wait() -> object:\n    class _Proc:\n        def __init__(self) -> None:\n            self._ret = 0\n\n        def wait(self) -> int:\n            return self._ret\n\n        def poll(self) -> int:\n            return self._ret\n\n    return _Proc()\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/routers/test_deployments.py",
    "content": "from typing import Any\nfrom unittest import mock\n\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom llama_agents.appserver.deployment import Deployment\nfrom llama_agents.appserver.routers.deployments import (\n    create_base_router,\n    create_deployments_router,\n)\nfrom llama_agents.appserver.types import TaskDefinition\n\n\ndef build_app(name: str, deployment: Deployment) -> TestClient:\n    app = FastAPI()\n    app.include_router(create_base_router(name))\n    app.include_router(create_deployments_router(name, deployment))\n    return TestClient(app)\n\n\ndef test_create_and_run_task_basic() -> None:\n    d = Deployment(workflows={})\n    d._workflow_services = {\"default\": mock.MagicMock()}\n\n    async def run_workflow(\n        service_id: str, session_id: str | None = None, **run_kwargs: dict\n    ) -> dict[str, bool]:\n        return {\"ok\": True}\n\n    d.run_workflow = mock.AsyncMock(side_effect=run_workflow)  # type: ignore\n\n    client = build_app(\"dep\", d)\n    r = client.post(\"/deployments/dep/tasks/run\", json={\"input\": \"{}\"})\n    assert r.status_code == 200\n    assert r.json() == {\"ok\": True}\n\n\ndef test_create_task_nowait_and_list() -> None:\n    d = Deployment(workflows={})\n    d._workflow_services = {\"default\": mock.MagicMock()}\n    d.run_workflow_no_wait = mock.MagicMock(return_value=(\"hid\", \"sid\"))  # type: ignore\n\n    client = build_app(\"dep\", d)\n    r = client.post(\"/deployments/dep/tasks/create\", json={\"input\": \"{}\"})\n    assert r.status_code == 200\n    td = TaskDefinition.model_validate(r.json())\n    assert td.task_id == \"hid\"\n    assert td.session_id == \"sid\"\n\n    # handler still running\n    d._handlers = {\"hid\": mock.MagicMock(is_done=mock.MagicMock(return_value=False))}\n    d._handler_inputs = {\"hid\": \"{}\"}\n    r2 = client.get(\"/deployments/dep/tasks\")\n    assert r2.status_code == 200\n    assert len(r2.json()) == 1\n\n\ndef test_get_event_stream() -> None:\n    d = Deployment(workflows={})\n\n    class Ctx:\n        class Q:\n            def empty(self) -> bool:\n                return True\n\n        def __init__(self) -> None:\n            self.streaming_queue = self.Q()\n\n    class H:\n        def is_done(self) -> bool:\n            return True\n\n        def __await__(self) -> Any:\n            async def _w() -> str:\n                return \"done\"\n\n            return _w().__await__()\n\n        ctx = Ctx()\n\n    d._handlers = {\"hid\": H()}  # type: ignore[attr-defined]  # ty: ignore[invalid-assignment]\n\n    client = build_app(\"dep\", d)\n    r = client.get(\"/deployments/dep/tasks/hid/events\", params={\"session_id\": \"s\"})\n    assert r.status_code == 200\n\n\ndef test_sessions_crud() -> None:\n    d = Deployment(workflows={})\n    # have a default to allow create\n    d._default_service = mock.MagicMock()\n    d._workflow_services = {\"default\": mock.MagicMock()}\n    client = build_app(\"dep\", d)\n\n    r = client.post(\"/deployments/dep/sessions/create\")\n    assert r.status_code == 200\n    sid = r.json()[\"session_id\"]\n\n    r = client.get(f\"/deployments/dep/sessions/{sid}\")\n    assert r.status_code == 200\n\n    r = client.get(\"/deployments/dep/sessions\")\n    assert r.status_code == 200\n\n    r = client.post(f\"/deployments/dep/sessions/delete?session_id={sid}\")\n    assert r.status_code == 200\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/routers/test_ui_proxy.py",
    "content": "from __future__ import annotations\n\nimport httpx\nimport respx\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom llama_agents.appserver.routers.ui_proxy import create_ui_proxy_router\n\n\ndef build_client() -> TestClient:\n    app = FastAPI()\n    app.include_router(create_ui_proxy_router(\"dep\", 3000))\n    return TestClient(app)\n\n\n@respx.mock\ndef test_proxy_success() -> None:\n    client = build_client()\n    respx.get(\"http://localhost:3000/deployments/dep/ui/x.js\").mock(\n        return_value=httpx.Response(200, content=b\"x\")\n    )\n    r = client.get(\"/deployments/dep/ui/x.js\")\n    assert r.status_code == 200\n    assert r.content == b\"x\"\n\n\n@respx.mock\ndef test_proxy_timeout() -> None:\n    client = build_client()\n    respx.get(\"http://localhost:3000/deployments/dep/ui/x\").mock(\n        side_effect=httpx.TimeoutException(\"timeout\")\n    )\n    r = client.get(\"/deployments/dep/ui/x\")\n    assert r.status_code == 504\n\n\n@respx.mock\ndef test_proxy_connect_error_results_in_502() -> None:\n    client = build_client()\n    respx.get(\"http://localhost:3000/deployments/dep/ui/y\").mock(\n        side_effect=httpx.ConnectError(\"boom\")\n    )\n    r = client.get(\"/deployments/dep/ui/y\")\n    assert r.status_code == 502\n\n\n@respx.mock\ndef test_header_filtering_and_methods_with_body_and_headers() -> None:\n    client = build_client()\n\n    route = respx.post(\"http://localhost:3000/deployments/dep/ui/submit\").mock(\n        return_value=httpx.Response(\n            201,\n            json={\"ok\": True},\n            headers={\"X-Foo\": \"bar\", \"Transfer-Encoding\": \"chunked\"},\n        )\n    )\n\n    r = client.post(\n        \"/deployments/dep/ui/submit\",\n        content=b\"payload\",\n        headers={\n            \"Connection\": \"keep-alive\",\n            \"Transfer-Encoding\": \"chunked\",\n            \"X-Custom\": \"123\",\n        },\n    )\n\n    assert r.status_code == 201\n    # Hop-by-hop headers should be stripped from response\n    assert \"transfer-encoding\" not in {k.lower() for k in r.headers.keys()}\n    # Custom header from upstream should pass through\n    assert r.headers.get(\"X-Foo\") == \"bar\"\n\n    # Verify request seen by upstream filtered hop-by-hop headers and preserved payload\n    assert route.called\n    req = route.calls.last.request\n    assert req.read() == b\"payload\"\n    # hop-by-hop stripped where applicable (httpx may add 'connection' back)\n    assert \"transfer-encoding\" not in {k.lower() for k in req.headers.keys()}\n    # custom header retained\n    assert req.headers.get(\"X-Custom\") == \"123\"\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/routers/test_ui_proxy_ws.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nfrom typing import Any\n\nimport pytest\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom llama_agents.appserver.routers.ui_proxy import create_ui_proxy_router\n\n\nclass FakeUpstreamWebSocket:\n    def __init__(self) -> None:\n        self._recv_q: asyncio.Queue[Any] = asyncio.Queue()\n        self._send_q: asyncio.Queue[Any] = asyncio.Queue()\n        self.closed = False\n        self.subprotocol: str | None = None\n\n    def __aiter__(self) -> Any:\n        return self\n\n    async def send(self, data: Any) -> None:\n        await self._recv_q.put(data)\n\n    # API used by proxy for server->client messages\n    async def recv(self) -> Any:\n        item = await self._send_q.get()\n        if item is StopAsyncIteration:\n            raise StopAsyncIteration\n        return item\n\n    # helpers for tests\n    async def push_from_server(self, data: Any) -> None:\n        await self._send_q.put(data)\n\n    async def close(self) -> None:\n        self.closed = True\n\n\n@pytest.mark.asyncio\nasync def test_websocket_text_and_binary_and_subprotocol(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    app = FastAPI()\n    app.include_router(create_ui_proxy_router(\"dep\", 3000))\n    client = TestClient(app)\n\n    upstream = FakeUpstreamWebSocket()\n\n    def fake_connect(\n        url: str,\n        additional_headers: dict[str, Any] | None = None,\n        subprotocols: list[str] | None = None,\n        open_timeout: int | None = None,\n        ping_interval: int | None = None,\n    ) -> Any:\n        # Validate URL constructed with path and query params\n        assert url == \"ws://localhost:3000/deployments/dep/ui/chat?room=1\"\n        # Simulate server-side subprotocol selection\n        assert subprotocols is not None and subprotocols == [\"json\", \"msgpack\"]\n        upstream.subprotocol = subprotocols[0]\n\n        # Provide an async context manager that yields our upstream\n        class Ctx:\n            async def __aenter__(self) -> FakeUpstreamWebSocket:\n                return upstream\n\n            async def __aexit__(\n                self,\n                exc_type: type | None,\n                exc: Exception | None,\n                tb: Any,\n            ) -> bool:\n                return False\n\n        return Ctx()\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.routers.ui_proxy.websockets.connect\", fake_connect\n    )\n\n    with client.websocket_connect(\n        \"/deployments/dep/ui/chat?room=1\",\n        headers={\"sec-websocket-protocol\": \"json, msgpack\"},\n    ) as ws:\n        # Send text and binary to upstream\n        ws.send_text(\"hello\")\n        ws.send_bytes(b\"bytes\")\n\n        # Upstream should receive them\n        assert await asyncio.wait_for(upstream._recv_q.get(), 1) == \"hello\"\n        assert await asyncio.wait_for(upstream._recv_q.get(), 1) == b\"bytes\"\n\n        # Upstream sends messages back\n        await upstream.push_from_server(\"world\")\n        await upstream.push_from_server(b\"buf\")\n\n        # Client should receive them\n        assert ws.receive_text() == \"world\"\n        assert ws.receive_bytes() == b\"buf\"\n\n    # Ensure graceful close attempted\n    await upstream.close()\n    assert upstream.closed is True\n\n\n@pytest.mark.asyncio\nasync def test_websocket_upstream_raises_is_handled(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    app = FastAPI()\n    app.include_router(create_ui_proxy_router(\"dep\", 3000))\n    client = TestClient(app)\n\n    def fake_connect(*args: Any, **kwargs: dict[str, Any]) -> Any:\n        class Ctx:\n            async def __aenter__(self) -> Any:\n                raise RuntimeError(\"boom\")\n\n            async def __aexit__(\n                self,\n                exc_type: type | None,\n                exc: Exception | None,\n                tb: Any,\n            ) -> bool:\n                return False\n\n        return Ctx()\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.routers.ui_proxy.websockets.connect\", fake_connect\n    )\n\n    # Should not raise on connect failure; FastAPI's TestClient will raise if close not called\n    with client.websocket_connect(\"/deployments/dep/ui\"):\n        # Connection will be accepted then closed in finally\n        pass\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_app.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\nimport yaml\nfrom fastapi.testclient import TestClient\nfrom llama_agents.appserver.app import app\nfrom llama_agents.appserver.deployment_config_parser import get_deployment_config\nfrom llama_agents.appserver.settings import settings\n\n\n@pytest.fixture(autouse=True)\ndef _reset_deployment_config() -> None:\n    get_deployment_config.cache_clear()\n\n\ndef _write_deployment(\n    tmp_path: Path, name: str = \"myapp\", with_ui: bool = True\n) -> None:\n    content = {\n        \"name\": name,\n        \"workflows\": {},\n    }\n    if with_ui:\n        content[\"ui\"] = {\n            \"directory\": \"ui\",\n            \"proxy_port\": 3010,\n        }\n    (tmp_path / \"llama_deploy.yaml\").write_text(yaml.dump(content))\n\n\ndef test_root_redirect_and_metrics(tmp_path: Path) -> None:\n    settings.app_root = tmp_path\n    settings.deployment_file_path = Path(\"llama_deploy.yaml\")\n    _write_deployment(tmp_path)\n\n    with TestClient(app) as client:\n        r = client.get(\"/\", follow_redirects=False)\n        assert r.status_code in (302, 307)\n        # target should be deployments/{name}/ui\n        assert \"/deployments/myapp/\" in r.headers.get(\"location\", \"\")\n\n        # metrics exposed\n        m = client.get(\"/metrics\")\n        assert m.status_code == 200\n        assert b\"apiserver_state\" in m.content\n\n        # CORS header present when Origin is sent\n        r2 = client.get(\"/health\", headers={\"Origin\": \"http://example.com\"})\n        assert r2.headers.get(\"access-control-allow-origin\") == \"*\"\n\n\ndef test_static_ui_mount_serves_dist(tmp_path: Path) -> None:\n    # create dist assets\n    ui_dist = tmp_path / \"ui\" / \"dist\"\n    ui_dist.mkdir(parents=True)\n    (ui_dist / \"index.html\").write_text(\"<html>ok</html>\")\n\n    settings.app_root = tmp_path\n    settings.deployment_file_path = Path(\"llama_deploy.yaml\")\n    settings.proxy_ui = False\n    _write_deployment(tmp_path)\n\n    with TestClient(app) as client:\n        r = client.get(\"/deployments/myapp/ui/index.html\")\n        assert r.status_code == 200\n        assert r.text.strip() == \"<html>ok</html>\"\n\n\ndef test_workflowserver_endpoint_available(tmp_path: Path) -> None:\n    # Real mount with no workflows configured should still expose the index endpoint\n    settings.app_root = tmp_path\n    settings.deployment_file_path = Path(\"llama_deploy.yaml\")\n    (tmp_path / \"llama_deploy.yaml\").write_text(\"name: myapp\\nservices: {}\\n\")\n\n    with TestClient(app) as client:\n        # Starlette WorkflowServer index\n        r = client.get(\"/deployments/myapp/workflows\")\n        assert r.status_code == 200\n        _ = r.json()\n\n        # Legacy FastAPI sessions endpoint (should return empty list when no sessions)\n        r2 = client.get(\"/deployments/myapp/sessions\")\n        assert r2.status_code == 200\n        assert r2.json() == []\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_app_server_start.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import Any, Callable\n\nimport pytest\nfrom llama_agents.appserver import app as app_mod\nfrom llama_agents.appserver.app import start_server\nfrom llama_agents.appserver.settings import settings\n\n\ndef test_start_server_sets_env_and_runs_server(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    make_deployment_file: Callable[..., Path],\n) -> None:\n    # Arrange minimal deployment file\n    deployment_path = make_deployment_file()\n    # Ensure settings pick up our temp rc and file\n    settings.app_root = tmp_path\n    settings.deployment_file_path = Path(deployment_path.name)\n\n    # Stub external calls\n    called = {\"run\": 0}\n\n    monkeypatch.setenv(\"LLAMA_DEPLOY_APISERVER_APP_ROOT\", str(tmp_path))\n\n    monkeypatch.setenv(\"DISABLE_CORS\", \"1\")\n\n    def _fail_dev_ui(base: Any, port: Any, cfg: Any) -> None:\n        raise AssertionError(\n            \"start_dev_ui_process should not be called when proxy_ui=False\"\n        )\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.app.start_dev_ui_process\",\n        _fail_dev_ui,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.app.uvicorn.run\",\n        lambda *a, **k: called.__setitem__(\"run\", called[\"run\"] + 1),\n    )\n\n    # Act\n    start_server(\n        proxy_ui=False,\n        reload=False,\n        cwd=tmp_path,\n        deployment_file=deployment_path,\n        configure_logging=False,\n    )\n\n    # Assert env and settings updated\n    assert settings.proxy_ui is False\n    assert settings.app_root == tmp_path\n    assert settings.deployment_file_path == deployment_path\n\n    assert called[\"run\"] == 1\n\n\ndef test_start_server_proxies_ui_and_terminates(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    make_deployment_file: Callable[..., Path],\n    process_stub: Any,\n) -> None:\n    deployment_path = make_deployment_file(with_ui=True)\n    settings.app_root = tmp_path\n    settings.deployment_file_path = Path(deployment_path.name)\n\n    proc = process_stub\n\n    # Stub external calls\n    monkeypatch.setattr(\n        \"llama_agents.appserver.app.start_dev_ui_process\", lambda base, port, cfg: proc\n    )\n\n    def fake_run(*args: Any, **kwargs: dict[str, Any]) -> None:\n        # Validate expected module path and host/port\n        assert args[0] == \"llama_agents.appserver.app:app\"\n        assert kwargs.get(\"host\") == settings.host\n        assert kwargs.get(\"port\") == settings.port\n        assert kwargs.get(\"reload\") is False\n        return None\n\n    monkeypatch.setattr(\"llama_agents.appserver.app.uvicorn.run\", fake_run)\n\n    # Act\n    start_server(\n        proxy_ui=True,\n        reload=False,\n        cwd=tmp_path,\n        deployment_file=deployment_path,\n        configure_logging=False,\n    )\n\n    # Assert process termination in finally\n    assert proc.terminated is True\n\n\ndef test_prepare_server_calls_install_and_build_when_flags_set(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    make_deployment_file: Callable[..., Path],\n) -> None:\n    # target: prepare_server() path that performs install and build\n\n    called = {\"inject\": 0, \"install\": 0, \"build\": 0}\n\n    # Ensure a deployment file exists so get_deployment_config can load it\n    make_deployment_file()\n\n    monkeypatch.setattr(\n        app_mod,\n        \"inject_appserver_into_target\",\n        lambda *a, **k: called.__setitem__(\"inject\", called[\"inject\"] + 1),\n    )\n    monkeypatch.setattr(\n        app_mod,\n        \"install_ui\",\n        lambda *a, **k: called.__setitem__(\"install\", called[\"install\"] + 1),\n    )\n    monkeypatch.setattr(\n        app_mod,\n        \"build_ui\",\n        lambda *a, **k: called.__setitem__(\"build\", called[\"build\"] + 1),\n    )\n\n    app_mod.prepare_server(deployment_file=None, install=True, build=True)\n\n    assert called[\"inject\"] == 1\n    assert called[\"install\"] == 1\n    assert called[\"build\"] == 1\n\n\ndef test_prepare_server_install_only_invokes_inject_and_install(\n    monkeypatch: pytest.MonkeyPatch, make_deployment_file: Callable[..., Path]\n) -> None:\n    called = {\"inject\": 0, \"install\": 0}\n    # Ensure a deployment file exists so get_deployment_config can load it\n    make_deployment_file()\n    monkeypatch.setattr(\n        app_mod,\n        \"inject_appserver_into_target\",\n        lambda *a, **k: called.__setitem__(\"inject\", called[\"inject\"] + 1),\n    )\n    monkeypatch.setattr(\n        app_mod,\n        \"install_ui\",\n        lambda *a, **k: called.__setitem__(\"install\", called[\"install\"] + 1),\n    )\n    app_mod.prepare_server(deployment_file=None, install=True, build=False)\n    assert called[\"inject\"] == 1\n    assert called[\"install\"] == 1\n\n\ndef test_start_server_open_browser_triggers(\n    monkeypatch: pytest.MonkeyPatch, make_deployment_file: Callable[..., Path]\n) -> None:\n    opened = {\"count\": 0}\n    monkeypatch.setattr(\n        app_mod.webbrowser,\n        \"open\",\n        lambda *a, **k: opened.__setitem__(\"count\", opened[\"count\"] + 1),\n    )\n    monkeypatch.setattr(\n        app_mod, \"uvicorn\", type(\"_U\", (), {\"run\": staticmethod(lambda *a, **k: None)})\n    )\n\n    # Avoid spawning UI process\n    monkeypatch.setattr(app_mod, \"start_dev_ui_process\", lambda *a, **k: None)\n    # Ensure a deployment file exists for get_deployment_config\n    make_deployment_file()\n\n    app_mod.start_server(\n        proxy_ui=False,\n        reload=False,\n        cwd=None,\n        deployment_file=None,\n        open_browser=True,\n        configure_logging=False,\n    )\n    assert (\n        opened[\"count\"] >= 0\n    )  # timing dependent, presence of call path is what matters\n\n\ndef test_start_server_in_target_venv_invocation(\n    monkeypatch: pytest.MonkeyPatch,\n    tmp_path: Path,\n    proc_with_poll_wait: Any,\n    make_deployment_file: Callable[..., Path],\n) -> None:\n    # Avoid actually spawning processes; ensure run_process is called with expected args\n    called: dict[str, Any] = {\"args\": None, \"cwd\": None}\n\n    def fake_run_process(\n        args: list[str],\n        *,\n        cwd: Path | None = None,\n        env: dict[str, Any] | None = None,\n        prefix: str | None = None,\n        color_code: str = \"36\",\n        line_transform: Callable[[str], str] | None = None,\n        use_tty: bool | None = None,\n    ) -> int:\n        called[\"args\"] = args\n        called[\"cwd\"] = cwd\n        return 0\n\n    monkeypatch.setattr(\"llama_agents.appserver.app.run_process\", fake_run_process)\n\n    # Ensure config and pyproject exist\n    make_deployment_file()\n    (tmp_path / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    app_mod.start_server_in_target_venv(\n        proxy_ui=False,\n        reload=False,\n        cwd=tmp_path,\n        deployment_file=None,\n        open_browser=False,\n    )\n\n    assert called[\"args\"] is not None\n    assert called[\"args\"][0:6] == [\n        \"uv\",\n        \"run\",\n        \"--no-progress\",\n        \"python\",\n        \"-m\",\n        \"llama_agents.appserver.app\",\n    ]\n    assert called[\"cwd\"] == Path(\".\")\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_bootstrap.py",
    "content": "from pathlib import Path\nfrom unittest import mock\n\nimport pytest\nfrom llama_agents.appserver.bootstrap import (\n    _artifact_exists,\n    _download_and_extract_artifact,\n    _upload_artifact,\n    bootstrap_app_from_repo,\n)\n\n\ndef _set_bootstrap_env(\n    monkeypatch: pytest.MonkeyPatch,\n    *,\n    repo_url: str,\n    auth_token: str | None = None,\n    git_ref: str | None = None,\n    git_sha: str | None = None,\n    deployment_file_path: str = \"llama_deploy.yaml\",\n    bootstrap_sdists: str | None = None,\n) -> None:\n    monkeypatch.setenv(\"LLAMA_DEPLOY_REPO_URL\", repo_url)\n    if auth_token is not None:\n        monkeypatch.setenv(\"LLAMA_DEPLOY_AUTH_TOKEN\", auth_token)\n    if git_ref is not None:\n        monkeypatch.setenv(\"LLAMA_DEPLOY_GIT_REF\", git_ref)\n    if git_sha is not None:\n        monkeypatch.setenv(\"LLAMA_DEPLOY_GIT_SHA\", git_sha)\n    if deployment_file_path is not None:\n        monkeypatch.setenv(\"LLAMA_DEPLOY_DEPLOYMENT_FILE_PATH\", deployment_file_path)\n    if bootstrap_sdists is not None:\n        monkeypatch.setenv(\"LLAMA_DEPLOY_BOOTSTRAP_SDISTS\", bootstrap_sdists)\n\n\ndef _stub_bootstrap_pipeline(monkeypatch: pytest.MonkeyPatch) -> dict[str, mock.Mock]:\n    \"\"\"\n    Patch side-effectful functions used by bootstrap to no-ops so tests can\n    assert call wiring without performing real work. Returns a dict of mocks.\n    \"\"\"\n    mocks: dict[str, mock.Mock] = {}\n    for qualname in [\n        \"llama_agents.appserver.bootstrap.load_environment_variables\",\n        \"llama_agents.appserver.bootstrap.inject_appserver_into_target\",\n        \"llama_agents.appserver.bootstrap.install_ui\",\n        \"llama_agents.appserver.bootstrap.build_ui\",\n        \"llama_agents.appserver.bootstrap.configure_settings\",\n    ]:\n        m = mock.Mock()\n        monkeypatch.setattr(qualname, m)\n        mocks[qualname] = m\n    return mocks\n\n\ndef test_bootstrap_minimal_happy_path(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"End-to-end happy path with minimal config and stubs to verify key calls and wiring.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n        # No git_ref/sha on purpose to validate None behavior\n    )\n\n    pipeline_mocks = _stub_bootstrap_pipeline(monkeypatch)\n\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\") as clone:\n        # Execute\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n        # clone_repo was invoked with expected args\n        clone.assert_called_once_with(\n            repository_url=\"https://example.com/repo.git\",\n            git_ref=None,\n            git_sha=None,\n            basic_auth=\"tok\",\n            dest_dir=str(tmp_path),\n            depth=None,\n        )\n\n    # configure_settings received proper app_root and deployment_file_path\n    cfg_calls = pipeline_mocks[\n        \"llama_agents.appserver.bootstrap.configure_settings\"\n    ].call_args_list\n    assert cfg_calls, \"configure_settings should be called\"\n    _, cfg_kwargs = cfg_calls[0]\n    assert cfg_kwargs.get(\"app_root\") == Path(tmp_path)\n    assert cfg_kwargs.get(\"deployment_file_path\") == Path(\"llama_deploy.yaml\")\n\n    # build_ui now receives settings as third arg via bootstrap module\n    args, kwargs = pipeline_mocks[\"llama_agents.appserver.bootstrap.build_ui\"].call_args\n    assert args[0] == Path(tmp_path)\n\n\ndef test_bootstrap_invokes_clone_repo_with_explicit_git_sha_and_git_ref(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"\n    If both `LLAMA_DEPLOY_GIT_SHA` and `LLAMA_DEPLOY_GIT_REF` are set,\n    bootstrap should pass them separately and avoid the shallow-clone path.\n    \"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n        git_ref=\"feature/branch\",\n        git_sha=\"deadbeef\",\n    )\n\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\") as clone:\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n        clone.assert_called_once_with(\n            repository_url=\"https://example.com/repo.git\",\n            git_ref=\"feature/branch\",\n            git_sha=\"deadbeef\",\n            basic_auth=\"tok\",\n            dest_dir=str(tmp_path),\n            depth=None,\n        )\n\n\ndef test_bootstrap_invokes_clone_repo_with_git_ref_only(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"If only `LLAMA_DEPLOY_GIT_REF` is set, bootstrap should use the ref path.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n        git_ref=\"feature/branch\",\n    )\n\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\") as clone:\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n        clone.assert_called_once_with(\n            repository_url=\"https://example.com/repo.git\",\n            git_ref=\"feature/branch\",\n            git_sha=None,\n            basic_auth=\"tok\",\n            dest_dir=str(tmp_path),\n            depth=1,\n        )\n\n\ndef test_bootstrap_raises_when_repo_url_missing(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Ensure a ValueError is raised when `repo_url` is not provided via settings/env.\"\"\"\n    # Ensure repo_url is absent\n    monkeypatch.delenv(\"LLAMA_DEPLOY_REPO_URL\", raising=False)\n    with pytest.raises(ValueError):\n        bootstrap_app_from_repo(target_dir=\"/tmp/irrelevant\")\n\n\ndef test_bootstrap_invokes_clone_repo_with_auth_token(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"Verify `basic_auth` is passed to `clone_repo` when `auth_token` is set.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"secret-token\",\n    )\n    _stub_bootstrap_pipeline(monkeypatch)\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\") as clone:\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n        clone.assert_called_once()\n        kwargs = clone.call_args.kwargs\n        assert kwargs[\"basic_auth\"] == \"secret-token\"\n\n\ndef test_bootstrap_configure_settings_called_with_app_root_and_deployment_file(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"Assert `configure_settings(app_root, deployment_file_path)` is called with resolved paths from `target_dir` and settings.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    # Use a non-default deployment file path to ensure it is passed through\n    custom_path = \"configs/deploy.yaml\"\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        deployment_file_path=custom_path,\n    )\n\n    mocks = _stub_bootstrap_pipeline(monkeypatch)\n\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\"):\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n    cfg = mocks[\"llama_agents.appserver.bootstrap.configure_settings\"]\n    assert cfg.called, \"configure_settings should be called\"\n    _, kwargs = cfg.call_args\n    assert kwargs[\"app_root\"] == Path(tmp_path)\n    assert kwargs[\"deployment_file_path\"] == Path(custom_path)\n\n\ndef test_bootstrap_sdists_passed_when_tarballs_present(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"Create a directory with mixed files; only .tar.gz files should be collected and passed to `inject_appserver_into_target`.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    sd_dir = tmp_path / \"sdists\"\n    sd_dir.mkdir()\n    good1 = sd_dir / \"a-0.1.0.tar.gz\"\n    good2 = sd_dir / \"b-0.2.0.tar.gz\"\n    bad1 = sd_dir / \"c.txt\"\n    good1.write_text(\"x\")\n    good2.write_text(\"x\")\n    bad1.write_text(\"x\")\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n        bootstrap_sdists=str(sd_dir),\n    )\n\n    mocks = _stub_bootstrap_pipeline(monkeypatch)\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\"):\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n    inject_mock = mocks[\"llama_agents.appserver.bootstrap.inject_appserver_into_target\"]\n    assert inject_mock.called\n    # Extract sdists arg (third positional or named)\n    _, args, kwargs = inject_mock.mock_calls[0]\n    # monkeypatch.Mock's call objects: (name, args, kwargs) via .mock_calls entries\n    # But here we used default Mock(), so access via .call_args as safer:\n    args, kwargs = inject_mock.call_args\n    sdists = args[2] if len(args) >= 3 else kwargs.get(\"sdists\")\n    assert sdists is not None and len(sdists) == 2\n    assert {p.name for p in sdists} == {\"a-0.1.0.tar.gz\", \"b-0.2.0.tar.gz\"}\n\n\ndef test_bootstrap_sdists_none_when_empty_or_no_tarballs(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"When the sdists directory is empty or lacks .tar.gz, `inject_appserver_into_target` should receive `sdists=None`.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    sd_dir = tmp_path / \"sdists\"\n    sd_dir.mkdir()\n    # Create non-tarball files only\n    (sd_dir / \"note.txt\").write_text(\"x\")\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        bootstrap_sdists=str(sd_dir),\n    )\n    mocks = _stub_bootstrap_pipeline(monkeypatch)\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\"):\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n    inject_mock = mocks[\"llama_agents.appserver.bootstrap.inject_appserver_into_target\"]\n    args, kwargs = inject_mock.call_args\n    sdists = args[2] if len(args) >= 3 else kwargs.get(\"sdists\")\n    assert sdists is None\n\n\ndef test_bootstrap_propagates_errors_from_clone(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"If `clone_repo` raises, the exception should bubble up (no swallowing).\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n    )\n    # Do not stub pipeline; we expect to raise before any other calls\n    with mock.patch(\n        \"llama_agents.appserver.bootstrap.clone_repo\",\n        side_effect=RuntimeError(\"boom\"),\n    ):\n        with pytest.raises(RuntimeError):\n            bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n\ndef _write_minimal_deployment_config(tmp_path: Path) -> None:\n    (tmp_path / \"llama_deploy.yaml\").write_text(\"name: test\\nservices: {}\\n\")\n\n\n# ---------------------------------------------------------------------------\n# Phase 3: Build artifact 503 handling tests\n# ---------------------------------------------------------------------------\n\n\ndef test_artifact_exists_raises_on_503() -> None:\n    resp = mock.Mock(status_code=503)\n    with mock.patch(\"llama_agents.appserver.bootstrap.httpx.head\", return_value=resp):\n        with pytest.raises(RuntimeError, match=\"Build artifact storage not configured\"):\n            _artifact_exists(\"host:8000\", \"dep1\", \"build1\", \"tok\")\n\n\ndef test_artifact_exists_returns_false_on_404() -> None:\n    resp = mock.Mock(status_code=404)\n    with mock.patch(\"llama_agents.appserver.bootstrap.httpx.head\", return_value=resp):\n        assert _artifact_exists(\"host:8000\", \"dep1\", \"build1\", \"tok\") is False\n\n\ndef test_artifact_exists_returns_true_on_200() -> None:\n    resp = mock.Mock(status_code=200)\n    with mock.patch(\"llama_agents.appserver.bootstrap.httpx.head\", return_value=resp):\n        assert _artifact_exists(\"host:8000\", \"dep1\", \"build1\", \"tok\") is True\n\n\ndef test_upload_artifact_raises_on_503(tmp_path: Path) -> None:\n    tarball = tmp_path / \"artifact.tar.gz\"\n    tarball.write_bytes(b\"fake tarball content\")\n    resp = mock.Mock(status_code=503)\n    with mock.patch(\"llama_agents.appserver.bootstrap.httpx.put\", return_value=resp):\n        with pytest.raises(RuntimeError, match=\"Build artifact storage not configured\"):\n            _upload_artifact(\"host:8000\", \"dep1\", \"build1\", \"tok\", str(tarball))\n\n\ndef test_bootstrap_discards_sdists_when_version_mismatch(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"When LLAMA_DEPLOY_APPSERVER_VERSION differs from bundled version, sdists are discarded.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    sd_dir = tmp_path / \"sdists\"\n    sd_dir.mkdir()\n    (sd_dir / \"appserver-0.5.0.tar.gz\").write_text(\"x\")\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n        bootstrap_sdists=str(sd_dir),\n    )\n    monkeypatch.setenv(\"LLAMA_DEPLOY_APPSERVER_VERSION\", \"0.4.15\")\n\n    mocks = _stub_bootstrap_pipeline(monkeypatch)\n    with (\n        mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\"),\n        mock.patch(\n            \"llama_agents.appserver.bootstrap.pkg_version\", return_value=\"0.5.0\"\n        ),\n    ):\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n    inject_mock = mocks[\"llama_agents.appserver.bootstrap.inject_appserver_into_target\"]\n    args, kwargs = inject_mock.call_args\n    sdists = args[2] if len(args) >= 3 else kwargs.get(\"sdists\")\n    assert sdists is None\n    assert kwargs.get(\"target_version\") == \"0.4.15\"\n\n\ndef test_bootstrap_keeps_sdists_when_version_matches(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"When LLAMA_DEPLOY_APPSERVER_VERSION matches bundled version, sdists are kept.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    sd_dir = tmp_path / \"sdists\"\n    sd_dir.mkdir()\n    (sd_dir / \"appserver-0.5.0.tar.gz\").write_text(\"x\")\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n        bootstrap_sdists=str(sd_dir),\n    )\n    monkeypatch.setenv(\"LLAMA_DEPLOY_APPSERVER_VERSION\", \"0.5.0\")\n\n    mocks = _stub_bootstrap_pipeline(monkeypatch)\n    with (\n        mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\"),\n        mock.patch(\n            \"llama_agents.appserver.bootstrap.pkg_version\", return_value=\"0.5.0\"\n        ),\n    ):\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n    inject_mock = mocks[\"llama_agents.appserver.bootstrap.inject_appserver_into_target\"]\n    args, kwargs = inject_mock.call_args\n    sdists = args[2] if len(args) >= 3 else kwargs.get(\"sdists\")\n    assert sdists is not None\n    assert kwargs.get(\"target_version\") == \"0.5.0\"\n\n\ndef test_bootstrap_no_appserver_version_env_uses_sdists(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"When LLAMA_DEPLOY_APPSERVER_VERSION is not set, sdists are passed through and target_version is None.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    sd_dir = tmp_path / \"sdists\"\n    sd_dir.mkdir()\n    (sd_dir / \"appserver-0.5.0.tar.gz\").write_text(\"x\")\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n        bootstrap_sdists=str(sd_dir),\n    )\n    monkeypatch.delenv(\"LLAMA_DEPLOY_APPSERVER_VERSION\", raising=False)\n\n    mocks = _stub_bootstrap_pipeline(monkeypatch)\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\"):\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n    inject_mock = mocks[\"llama_agents.appserver.bootstrap.inject_appserver_into_target\"]\n    args, kwargs = inject_mock.call_args\n    sdists = args[2] if len(args) >= 3 else kwargs.get(\"sdists\")\n    assert sdists is not None\n    assert kwargs.get(\"target_version\") is None\n\n\ndef test_bootstrap_passes_auto_upgrade_false(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"Bootstrap should pass auto_upgrade=False to inject_appserver_into_target\n    so that dependencies like llama-index-workflows are not auto-upgraded\n    during the container bootstrap process.\"\"\"\n    _write_minimal_deployment_config(tmp_path)\n    _set_bootstrap_env(\n        monkeypatch,\n        repo_url=\"https://example.com/repo.git\",\n        auth_token=\"tok\",\n    )\n\n    mocks = _stub_bootstrap_pipeline(monkeypatch)\n    with mock.patch(\"llama_agents.appserver.bootstrap.clone_repo\"):\n        bootstrap_app_from_repo(target_dir=str(tmp_path))\n\n    inject_mock = mocks[\"llama_agents.appserver.bootstrap.inject_appserver_into_target\"]\n    _, kwargs = inject_mock.call_args\n    assert kwargs.get(\"auto_upgrade\") is False\n\n\ndef test_download_artifact_raises_on_503() -> None:\n    # httpx.stream returns a context manager; mock the response inside it.\n    mock_response = mock.Mock(status_code=503)\n    mock_response.__enter__ = mock.Mock(return_value=mock_response)\n    mock_response.__exit__ = mock.Mock(return_value=False)\n    with mock.patch(\n        \"llama_agents.appserver.bootstrap.httpx.stream\", return_value=mock_response\n    ):\n        with pytest.raises(RuntimeError, match=\"Build artifact storage not configured\"):\n            _download_and_extract_artifact(\n                \"host:8000\", \"dep1\", \"build1\", \"tok\", \"/tmp/test-target\"\n            )\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_configure_logging.py",
    "content": "from __future__ import annotations\n\nimport json\nimport logging\nfrom collections.abc import Generator\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom llama_agents.appserver.configure_logging import (\n    _is_health_request,\n    add_log_middleware,\n    setup_logging,\n)\n\n\n@pytest.fixture()\ndef isolated_logging() -> Generator[None, None, None]:\n    \"\"\"Minimal isolation for logging and structlog state.\n\n    - Snapshot root logger level/handlers/filters\n    - Restore all after test\n    \"\"\"\n    # Root logger snapshot\n    root_logger = logging.getLogger()\n    prev_root_level = root_logger.level\n    prev_root_handlers = list(root_logger.handlers)\n    prev_root_filters = list(root_logger.filters)\n\n    try:\n        yield\n    finally:\n        # Restore root\n        root = logging.getLogger()\n        for h in list(root.handlers):\n            root.removeHandler(h)\n        for f in list(root.filters):\n            root.removeFilter(f)\n        for h in prev_root_handlers:\n            root.addHandler(h)\n        for f in prev_root_filters:\n            root.addFilter(f)\n        root.setLevel(prev_root_level)\n\n\ndef test_setup_logging_json_filters_by_level_and_renders_json(\n    isolated_logging: None,\n    capfd: pytest.CaptureFixture[str],\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setenv(\"LOG_FORMAT\", \"json\")\n    setup_logging(level=\"WARNING\")\n\n    logger = logging.getLogger(\"test\")\n    logger.info(\"info message\", extra={\"foo\": 1})\n    logger.warning(\"warn message\", extra={\"bar\": 2})\n\n    out, err = capfd.readouterr()\n    lines = [line for line in out.splitlines() if line.strip()]\n\n    # Only the WARNING should appear\n    assert not any(\"info message\" in line for line in lines)\n    warn_lines = [line for line in lines if \"warn message\" in line]\n    assert len(warn_lines) >= 1\n\n    # Validate JSON content shape\n    record = json.loads(warn_lines[-1])\n    # Basic keys we expect from our processors\n    assert \"timestamp\" in record\n    assert record.get(\"event\") == \"warn message\"\n    # level casing depends on structlog; accept either\n    level_val = record.get(\"level\")\n    assert level_val in {\"warning\", \"WARN\", \"WARNING\", \"warn\"}\n\n\ndef test_setup_logging_console_renders_human_readable(\n    isolated_logging: None,\n    capfd: pytest.CaptureFixture[str],\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    # Default format is console; ensure explicitly\n    monkeypatch.delenv(\"LOG_FORMAT\", raising=False)\n    setup_logging(level=\"INFO\")\n\n    logger = logging.getLogger(\"console-test\")\n    logger.info(\"hello world\", extra={\"answer\": 42})\n\n    out, err = capfd.readouterr()\n    # Should not be JSON\n    assert \"hello world\" in out\n    assert not out.strip().startswith(\"{\")\n\n\n# ---------------------------------------------------------------------------\n# _is_health_request unit tests\n# ---------------------------------------------------------------------------\n\n\ndef _fake_request(path: str) -> MagicMock:\n    req = MagicMock()\n    req.url.path = path\n    return req\n\n\n@pytest.mark.parametrize(\n    \"path\",\n    [\"/health\", \"/health/\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"],\n)\ndef test_is_health_request_matches(path: str) -> None:\n    assert _is_health_request(_fake_request(path)) is True\n\n\n@pytest.mark.parametrize(\n    \"path\",\n    [\"/\", \"/deployments/myapp/workflows\", \"/health/extra\"],\n)\ndef test_is_health_request_rejects(path: str) -> None:\n    assert _is_health_request(_fake_request(path)) is False\n\n\n# ---------------------------------------------------------------------------\n# access log middleware integration tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.fixture()\ndef log_app() -> FastAPI:\n    \"\"\"Minimal FastAPI app with access log middleware and a couple of routes.\"\"\"\n    test_app = FastAPI()\n\n    @test_app.get(\"/health\")\n    def health() -> dict[str, str]:\n        return {\"status\": \"ok\"}\n\n    @test_app.get(\"/other\")\n    def other() -> dict[str, str]:\n        return {\"data\": \"hello\"}\n\n    add_log_middleware(test_app)\n    return test_app\n\n\ndef test_access_log_suppressed_for_health(\n    isolated_logging: None,\n    log_app: FastAPI,\n    capfd: pytest.CaptureFixture[str],\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.delenv(\"LOG_FORMAT\", raising=False)\n    setup_logging(level=\"INFO\")\n\n    with TestClient(log_app) as client:\n        client.get(\"/health\")\n        client.get(\"/other\")\n\n    out, _ = capfd.readouterr()\n    access_lines = [line for line in out.splitlines() if \"[app.access]\" in line]\n    assert any(\"/other\" in line for line in access_lines)\n    assert not any(\"/health\" in line for line in access_lines)\n\n\ndef test_access_log_emitted_for_normal_routes(\n    isolated_logging: None,\n    log_app: FastAPI,\n    capfd: pytest.CaptureFixture[str],\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.delenv(\"LOG_FORMAT\", raising=False)\n    setup_logging(level=\"INFO\")\n\n    with TestClient(log_app) as client:\n        client.get(\"/other\")\n\n    out, _ = capfd.readouterr()\n    assert \"GET /other\" in out\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_deployment.py",
    "content": "import json\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\nfrom llama_agents.appserver.deployment import Deployment\nfrom workflows import Context, Workflow\nfrom workflows.handler import WorkflowHandler\n\n\n@pytest.fixture\ndef deployment(tmp_path: Path) -> Deployment:\n    # minimal Deployment with no workflows yet\n    return Deployment(workflows={})\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_without_session_without_kwargs(tmp_path: Path) -> None:\n    d = Deployment(workflows={})\n    wf = mock.MagicMock(spec=Workflow)\n    wf.run = mock.AsyncMock(return_value=\"ok\")\n    d._workflow_services = {\"svc\": wf}\n\n    result = await d.run_workflow(\"svc\")\n    assert result == \"ok\"\n    wf.run.assert_awaited_once_with()\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_session(tmp_path: Path) -> None:\n    d = Deployment(workflows={})\n    wf = mock.MagicMock(spec=Workflow)\n    wf.run = mock.AsyncMock(return_value=\"ok2\")\n    ctx = mock.MagicMock(spec=Context)\n    d._workflow_services = {\"svc\": wf}\n    d._contexts = {\"sid\": ctx}\n\n    result = await d.run_workflow(\"svc\", session_id=\"sid\", foo=1)\n    assert result == \"ok2\"\n    wf.run.assert_awaited_once_with(context=ctx, foo=1)\n\n\ndef test_run_workflow_no_wait_creates_session(tmp_path: Path) -> None:\n    d = Deployment(workflows={})\n    wf = mock.MagicMock(spec=Workflow)\n    handler = mock.MagicMock(spec=WorkflowHandler)\n    # Avoid constructing real Context; store a simple mock\n    handler.ctx = mock.MagicMock(spec=Context)\n    wf.run.return_value = handler\n    d._workflow_services = {\"svc\": wf}\n\n    with mock.patch(\"llama_agents.appserver.deployment.generate_id\") as gen:\n        gen.side_effect = [\"sess1\", \"handler1\"]\n        hid, sid = d.run_workflow_no_wait(\"svc\", None, foo=\"bar\")\n\n    assert hid == \"handler1\"\n    assert sid == \"sess1\"\n    assert d._handlers[hid] is handler\n    assert json.loads(d._handler_inputs[hid]) == {\"foo\": \"bar\"}\n\n\ndef test_run_workflow_no_wait_with_session(tmp_path: Path) -> None:\n    d = Deployment(workflows={})\n    wf = mock.MagicMock(spec=Workflow)\n    handler = mock.MagicMock(spec=WorkflowHandler)\n    ctx = mock.MagicMock(spec=Context)\n    wf.run.return_value = handler\n    d._workflow_services = {\"svc\": wf}\n    d._contexts = {\"sid\": ctx}\n\n    with mock.patch(\n        \"llama_agents.appserver.deployment.generate_id\", return_value=\"hid\"\n    ):\n        hid, sid = d.run_workflow_no_wait(\"svc\", \"sid\", a=1)\n\n    assert hid == \"hid\"\n    assert sid == \"sid\"\n    assert d._handlers[hid] is handler\n    wf.run.assert_called_once()\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_environment_loader.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Iterator\n\nimport pytest\nfrom llama_agents.appserver.workflow_loader import (\n    load_environment_variables,\n    validate_required_env_vars,\n)\nfrom llama_agents.core.deployment_config import DeploymentConfig\n\n\n@pytest.fixture(autouse=True)\ndef _cleanup_env() -> Iterator[None]:\n    # Ensure test-set env vars are cleaned between tests\n    before = set(os.environ.keys())\n    yield\n    for key in list(os.environ.keys()):\n        if key not in before:\n            os.environ.pop(key, None)\n\n\ndef test_env_loader_sets_from_env_dict_only(tmp_path: Path) -> None:\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"env\": {\"FOO\": \"bar\", \"HELLO\": \"world\"},\n        }\n    )\n    load_environment_variables(cfg, tmp_path)\n    assert os.environ.get(\"FOO\") == \"bar\"\n    assert os.environ.get(\"HELLO\") == \"world\"\n\n\ndef test_env_loader_env_files_override_env(tmp_path: Path) -> None:\n    env_file = tmp_path / \".env\"\n    env_file.write_text(\"FOO=baz\\nNEW_KEY=123\\n\")\n\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"name\": \"n\",\n            \"env\": {\"FOO\": \"bar\"},\n            \"env_files\": [\".env\"],\n        }\n    )\n    load_environment_variables(cfg, tmp_path)\n    # .env should override the inline env value\n    assert os.environ.get(\"FOO\") == \"baz\"\n    # .env should add new keys\n    assert os.environ.get(\"NEW_KEY\") == \"123\"\n\n\ndef test_env_loader_multiple_env_files_last_wins(tmp_path: Path) -> None:\n    (tmp_path / \"a.env\").write_text(\"X=1\\nY=from_a\\n\")\n    (tmp_path / \"b.env\").write_text(\"X=2\\n\")\n\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"name\": \"n\",\n            \"env\": {\"Y\": \"inline\"},\n            \"env_files\": [\"a.env\", \"b.env\"],\n        }\n    )\n    load_environment_variables(cfg, tmp_path)\n    # Later env file overrides earlier\n    assert os.environ.get(\"X\") == \"2\"\n    # First env file overrides inline\n    assert os.environ.get(\"Y\") == \"from_a\"\n\n\ndef test_env_loader_missing_env_file_is_ignored(tmp_path: Path) -> None:\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"env\": {\"FOO\": \"bar\"},\n            \"env_files\": [\"does_not_exist.env\"],\n        }\n    )\n    load_environment_variables(cfg, tmp_path)\n    # Should still set from inline env\n    assert os.environ.get(\"FOO\") == \"bar\"\n\n\ndef test_env_loader_skips_empty_values(tmp_path: Path) -> None:\n    (tmp_path / \".env\").write_text(\"EMPTY=\\n\")\n\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"name\": \"n\",\n            \"env\": {\"ALSO_EMPTY\": \"\"},\n            \"env_files\": [\".env\"],\n        }\n    )\n    load_environment_variables(cfg, tmp_path)\n    # Falsy values should not be set by the loader\n    assert \"EMPTY\" not in os.environ or os.environ[\"EMPTY\"]\n    assert \"ALSO_EMPTY\" not in os.environ or os.environ[\"ALSO_EMPTY\"]\n\n\ndef test_validate_required_env_vars_raises_for_missing(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    # Ensure a clean environment for this test\n    monkeypatch.delenv(\"REQ_ONE\", raising=False)\n    monkeypatch.delenv(\"REQ_TWO\", raising=False)\n\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"name\": \"n\",\n            \"required_env_vars\": [\"REQ_ONE\", \"REQ_TWO\"],\n        }\n    )\n    with pytest.raises(RuntimeError) as exc:\n        validate_required_env_vars(cfg)\n    assert \"REQ_ONE\" in str(exc.value) and \"REQ_TWO\" in str(exc.value)\n\n\ndef test_validate_required_env_vars_passes_when_set(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setenv(\"REQ_ONE\", \"1\")\n    monkeypatch.setenv(\"REQ_TWO\", \"2\")\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"name\": \"n\",\n            \"required_env_vars\": [\"REQ_ONE\", \"REQ_TWO\"],\n        }\n    )\n    # Should not raise\n    validate_required_env_vars(cfg)\n\n\ndef test_validate_required_env_vars_fill_missing(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    # Ensure env vars are not set\n    monkeypatch.delenv(\"FILL_ONE\", raising=False)\n    monkeypatch.delenv(\"FILL_TWO\", raising=False)\n\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"name\": \"n\",\n            \"required_env_vars\": [\"FILL_ONE\", \"FILL_TWO\"],\n        }\n    )\n    # Should not raise when fill_missing=True\n    validate_required_env_vars(cfg, fill_missing=True)\n\n    # Check that placeholder values were set\n    assert os.environ.get(\"FILL_ONE\") == \"__PLACEHOLDER_FILL_ONE__\"\n    assert os.environ.get(\"FILL_TWO\") == \"__PLACEHOLDER_FILL_TWO__\"\n\n\ndef test_validate_required_env_vars_fill_missing_only_fills_unset(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    # Set one env var, leave the other unset\n    monkeypatch.setenv(\"PARTIAL_ONE\", \"real_value\")\n    monkeypatch.delenv(\"PARTIAL_TWO\", raising=False)\n\n    cfg = DeploymentConfig.model_validate(\n        {\n            \"name\": \"n\",\n            \"required_env_vars\": [\"PARTIAL_ONE\", \"PARTIAL_TWO\"],\n        }\n    )\n    validate_required_env_vars(cfg, fill_missing=True)\n\n    # The set variable should keep its value\n    assert os.environ.get(\"PARTIAL_ONE\") == \"real_value\"\n    # The unset variable should get a placeholder\n    assert os.environ.get(\"PARTIAL_TWO\") == \"__PLACEHOLDER_PARTIAL_TWO__\"\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_preflight.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom pathlib import Path\nfrom types import SimpleNamespace\nfrom typing import Any\n\nimport pytest\nfrom workflows import (\n    Workflow,\n    step,\n)\nfrom workflows.events import StartEvent, StopEvent\n\n\ndef test_preflight_validate_success(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    # Import late to patch within module namespace\n    import llama_agents.appserver.app as app_mod\n\n    # Stub config/env setup to avoid real IO\n    monkeypatch.setattr(app_mod, \"get_deployment_config\", lambda: SimpleNamespace())\n    monkeypatch.setattr(app_mod, \"load_environment_variables\", lambda *a, **k: None)\n    monkeypatch.setattr(app_mod, \"validate_required_env_vars\", lambda *a, **k: None)\n\n    # Provide empty workflows and stub Deployment\n    monkeypatch.setattr(app_mod, \"load_workflows\", lambda cfg: {})\n    monkeypatch.setattr(app_mod, \"Deployment\", lambda workflows: SimpleNamespace())\n\n    # Should not raise\n    app_mod.preflight_validate(cwd=tmp_path, deployment_file=tmp_path / \"deploy.yaml\")\n\n\ndef test_preflight_validate_collects_errors(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    import llama_agents.appserver.app as app_mod\n\n    monkeypatch.setattr(app_mod, \"get_deployment_config\", lambda: SimpleNamespace())\n    monkeypatch.setattr(app_mod, \"load_environment_variables\", lambda *a, **k: None)\n    monkeypatch.setattr(app_mod, \"validate_required_env_vars\", lambda *a, **k: None)\n    monkeypatch.setattr(app_mod, \"Deployment\", lambda workflows: SimpleNamespace())\n\n    class BadWorkflow:\n        def _validate(self) -> None:\n            raise ValueError(\"boom\")\n\n    monkeypatch.setattr(app_mod, \"load_workflows\", lambda cfg: {\"svc\": BadWorkflow()})\n\n    with pytest.raises(app_mod.PreflightValidationError) as ei:\n        app_mod.preflight_validate(\n            cwd=tmp_path, deployment_file=tmp_path / \"deploy.yaml\"\n        )\n    # Ensure error content is reflected\n    assert isinstance(ei.value, app_mod.PreflightValidationError)\n    assert (\"svc\", \"boom\") in ei.value.errors\n\n\ndef test_start_preflight_in_target_venv_invokes_uv(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    import llama_agents.appserver.app as app_mod\n\n    # Simulate settings resolving to project subdir under provided cwd\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    # Replace settings object with minimal namespace\n    monkeypatch.setattr(\n        app_mod, \"settings\", SimpleNamespace(resolved_config_parent=proj)\n    )\n\n    # No-op configure_settings\n    monkeypatch.setattr(app_mod, \"configure_settings\", lambda *a, **k: None)\n\n    captured: dict[str, Any] = {}\n\n    def _fake_run_process(\n        args: list[str],\n        *,\n        cwd: Path,\n        env: dict | None = None,\n        line_transform: Any | None = None,\n    ) -> int:\n        captured[\"args\"] = args\n        captured[\"cwd\"] = cwd\n        return 0\n\n    monkeypatch.setattr(app_mod, \"run_process\", _fake_run_process)\n\n    df = tmp_path / \"deploy.yaml\"\n    app_mod.start_preflight_in_target_venv(cwd=tmp_path, deployment_file=df)\n\n    assert captured[\"cwd\"] == proj.relative_to(tmp_path)\n    assert captured[\"args\"][:6] == [\n        \"uv\",\n        \"run\",\n        \"--no-progress\",\n        \"python\",\n        \"-m\",\n        \"llama_agents.appserver.app\",\n    ]\n    assert \"--preflight\" in captured[\"args\"]\n    assert \"--deployment-file\" in captured[\"args\"]\n\n\ndef test_start_preflight_in_target_venv_skip_env_validation(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    import llama_agents.appserver.app as app_mod\n\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    monkeypatch.setattr(\n        app_mod, \"settings\", SimpleNamespace(resolved_config_parent=proj)\n    )\n    monkeypatch.setattr(app_mod, \"configure_settings\", lambda *a, **k: None)\n\n    captured: dict[str, Any] = {}\n\n    def _fake_run_process(\n        args: list[str],\n        *,\n        cwd: Path,\n        env: dict | None = None,\n        line_transform: Any | None = None,\n    ) -> int:\n        captured[\"args\"] = args\n        return 0\n\n    monkeypatch.setattr(app_mod, \"run_process\", _fake_run_process)\n\n    df = tmp_path / \"deploy.yaml\"\n    app_mod.start_preflight_in_target_venv(\n        cwd=tmp_path, deployment_file=df, skip_env_validation=True\n    )\n\n    assert \"--skip-env-validation\" in captured[\"args\"]\n\n\ndef test_export_json_graph_strips_event_type_and_writes_file(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    import llama_agents.appserver.app as app_mod\n\n    # Stub config/env setup to avoid real IO\n    monkeypatch.setattr(app_mod, \"get_deployment_config\", lambda: SimpleNamespace())\n    monkeypatch.setattr(app_mod, \"load_environment_variables\", lambda *a, **k: None)\n    monkeypatch.setattr(app_mod, \"validate_required_env_vars\", lambda *a, **k: None)\n\n    # Use a real, minimal LlamaIndex workflow instead of mocking the graph.\n    class _SimpleWorkflow(Workflow):\n        @step\n        async def start(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=ev.input)\n\n    wf = _SimpleWorkflow(timeout=5, verbose=False)\n    monkeypatch.setattr(app_mod, \"load_workflows\", lambda cfg: {\"my_workflow\": wf})\n\n    output = tmp_path / \"graph.json\"\n    app_mod.export_json_graph(\n        cwd=tmp_path,\n        deployment_file=tmp_path / \"deploy.yaml\",\n        output=output,\n    )\n\n    data = json.loads(output.read_text(encoding=\"utf-8\"))\n    assert isinstance(data, dict)\n    assert \"my_workflow\" in data\n    assert list(data.keys()) == [\"my_workflow\"]\n    assert isinstance(data[\"my_workflow\"], dict)\n    assert isinstance(data[\"my_workflow\"].get(\"nodes\"), list)\n    assert isinstance(data[\"my_workflow\"].get(\"edges\"), list)\n\n\ndef test_start_export_json_graph_in_target_venv_invokes_uv(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    import llama_agents.appserver.app as app_mod\n\n    proj = tmp_path / \"proj\"\n    proj.mkdir()\n    monkeypatch.setattr(\n        app_mod, \"settings\", SimpleNamespace(resolved_config_parent=proj)\n    )\n    monkeypatch.setattr(app_mod, \"configure_settings\", lambda *a, **k: None)\n\n    captured: dict[str, Any] = {}\n\n    def _fake_run_process(\n        args: list[str],\n        *,\n        cwd: Path,\n        env: dict | None = None,\n        line_transform: Any | None = None,\n    ) -> int:\n        captured[\"args\"] = args\n        captured[\"cwd\"] = cwd\n        return 0\n\n    monkeypatch.setattr(app_mod, \"run_process\", _fake_run_process)\n\n    df = tmp_path / \"deploy.yaml\"\n    output = tmp_path / \"graph.json\"\n    app_mod.start_export_json_graph_in_target_venv(\n        cwd=tmp_path,\n        deployment_file=df,\n        output=output,\n    )\n\n    assert captured[\"cwd\"] == proj.relative_to(tmp_path)\n    assert captured[\"args\"][:6] == [\n        \"uv\",\n        \"run\",\n        \"--no-progress\",\n        \"python\",\n        \"-m\",\n        \"llama_agents.appserver.app\",\n    ]\n    assert \"--export-json-graph\" in captured[\"args\"]\n    assert \"--deployment-file\" in captured[\"args\"]\n    assert \"--export-output\" in captured[\"args\"]\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_status.py",
    "content": "import pytest\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom llama_agents.appserver.routers.status import health_router\n\n\n@pytest.fixture()\ndef client() -> TestClient:\n    app = FastAPI()\n    app.include_router(health_router)\n    return TestClient(app)\n\n\n@pytest.mark.parametrize(\"path\", [\"/health\", \"/healthz\", \"/livez\", \"/readyz\"])\ndef test_health_endpoints(client: TestClient, path: str) -> None:\n    resp = client.get(path)\n    assert resp.status_code == 200\n    assert resp.json() == {\"status\": \"Healthy\"}\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_workflow_loader.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\nfrom llama_agents.appserver.settings import ApiserverSettings\nfrom llama_agents.appserver.workflow_loader import (\n    _ui_env,\n    build_ui,\n    load_environment_variables,\n    load_workflows,\n)\nfrom llama_agents.core.deployment_config import DeploymentConfig, UIConfig\n\n\ndef test_load_workflows_imports(tmp_path: Path) -> None:\n    cfg = DeploymentConfig(\n        name=\"n\",\n        workflows={\"svc\": \"m:mywf\"},\n    )\n\n    fake_wf = object()\n    with mock.patch(\"llama_agents.appserver.workflow_loader.importlib\") as imp:\n        imp.import_module.return_value = mock.MagicMock(mywf=fake_wf)\n        m = load_workflows(cfg)\n    assert m[\"svc\"] is fake_wf\n\n\ndef test_load_environment_variables_merges_env_and_files(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    env_file = tmp_path / \".env\"\n    env_file.write_text(\"API_KEY=123\\n\")\n    cfg = DeploymentConfig(\n        name=\"n\",\n        workflows={\"svc\": \"m:mywf\"},\n        env={\"FOO\": \"bar\"},\n        env_files=[\".env\"],\n    )\n    load_environment_variables(cfg, tmp_path)\n    assert os.environ.get(\"FOO\") == \"bar\"\n    assert os.environ.get(\"API_KEY\") == \"123\"\n\n\ndef test_build_ui_sets_env_and_calls_pnpm(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    ui_root = tmp_path / \"ui\"\n    ui_root.mkdir(parents=True)\n    package_json = ui_root / \"package.json\"\n    package_json.write_text('{\"scripts\": {\"build\": \"echo build\"} }')\n    cfg = DeploymentConfig(name=\"n\", ui=UIConfig(directory=\"ui\"))\n    # Configure UI proxy port via env (used by settings passed to build_ui)\n    monkeypatch.setenv(\"LLAMA_DEPLOY_APISERVER_PROXY_UI_PORT\", \"4503\")\n    api_settings = ApiserverSettings()\n    # Mock process runner used inside build_ui\n    with (\n        mock.patch(\"llama_agents.appserver.workflow_loader.run_process\") as run,\n    ):\n        run.side_effect = lambda *a, **k: None\n        build_ui(tmp_path, cfg, api_settings)\n\n        # Validate call\n        call = run.call_args\n        assert call is not None\n        args, kwargs = call\n        # first positional arg is the cmd list\n        assert args[0][:3] == [\"npm\", \"run\", \"build\"]\n        # cwd and env are kwargs\n        env = kwargs[\"env\"]\n        assert kwargs[\"cwd\"] == ui_root\n        assert env[\"LLAMA_DEPLOY_DEPLOYMENT_URL_ID\"] == \"n\"\n        assert env[\"LLAMA_DEPLOY_DEPLOYMENT_NAME\"] == \"n\"\n        assert env[\"LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH\"] == \"/deployments/n/ui\"\n        assert env[\"PORT\"] == \"4503\"\n\n\ndef test_ui_env_public_overrides_base(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setenv(\"LLAMA_CLOUD_BASE_URL\", \"https://original.example.com\")\n    monkeypatch.setenv(\"PUBLIC_LLAMA_CLOUD_BASE_URL\", \"https://public.example.com\")\n    cfg = DeploymentConfig(name=\"n\", ui=UIConfig(directory=\"ui\"))\n    settings = ApiserverSettings()\n    env = _ui_env(cfg, settings)\n    assert env[\"LLAMA_CLOUD_BASE_URL\"] == \"https://public.example.com\"\n    assert \"PUBLIC_LLAMA_CLOUD_BASE_URL\" not in env\n\n\ndef test_ui_env_public_without_base_creates_it(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.delenv(\"FOO\", raising=False)\n    monkeypatch.setenv(\"PUBLIC_FOO\", \"bar\")\n    cfg = DeploymentConfig(name=\"n\", ui=UIConfig(directory=\"ui\"))\n    settings = ApiserverSettings()\n    env = _ui_env(cfg, settings)\n    assert env[\"FOO\"] == \"bar\"\n    assert \"PUBLIC_FOO\" not in env\n\n\ndef test_ui_env_no_public_leaves_base_alone(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setenv(\"LLAMA_CLOUD_BASE_URL\", \"https://original.example.com\")\n    monkeypatch.delenv(\"PUBLIC_LLAMA_CLOUD_BASE_URL\", raising=False)\n    cfg = DeploymentConfig(name=\"n\", ui=UIConfig(directory=\"ui\"))\n    settings = ApiserverSettings()\n    env = _ui_env(cfg, settings)\n    assert env[\"LLAMA_CLOUD_BASE_URL\"] == \"https://original.example.com\"\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_workflow_loader_install.py",
    "content": "from __future__ import annotations\n\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nfrom llama_agents.appserver.settings import ApiserverSettings\nfrom llama_agents.appserver.workflow_loader import (\n    _ensure_compatible_workflows,\n    _ensure_uv_available,\n    _get_appserver_workflows_requirement,\n    _get_installed_version_within_target,\n    _install_and_add_appserver_if_missing,\n    _is_missing_or_outdated,\n    install_ui,\n    start_dev_ui_process,\n)\nfrom llama_agents.core.deployment_config import DeploymentConfig, UIConfig\nfrom llama_agents.core.path_util import validate_path_traversal\nfrom packaging.version import Version\n\n\n@pytest.fixture\ndef resolve_venv_to_pkg(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"\n    Stub ``_resolve_project_venv`` to return ``<source_root>/<path>/.venv``,\n    mirroring uv's choice for a non-workspace target. Tests that simulate a\n    workspace layout override this by patching ``_resolve_project_venv`` directly.\n    \"\"\"\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._resolve_project_venv\",\n        lambda source_root, path: source_root / path / \".venv\",\n    )\n\n\ndef test_ensure_uv_available_success_and_bootstrap(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    # Case 1: uv exists -> no pip install\n    calls = {\"check\": 0, \"run\": 0}\n\n    def ok_check_call(*a: Any, **k: dict[str, Any]) -> int:\n        calls[\"check\"] += 1\n        return 0\n\n    monkeypatch.setattr(\"subprocess.check_call\", ok_check_call)\n    _ensure_uv_available()\n    assert calls[\"check\"] == 1\n\n    def raise_missing(*a: Any, **k: dict[str, Any]) -> None:\n        raise FileNotFoundError(\"no uv\")\n\n    monkeypatch.setattr(\"subprocess.check_call\", raise_missing)\n    ran = {\"called\": False}\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\",\n        lambda *a, **k: ran.__setitem__(\"called\", True),\n    )\n    _ensure_uv_available()\n    assert ran[\"called\"] is True\n\n    # Case 3: pip install fails -> RuntimeError\n    monkeypatch.setattr(\"subprocess.check_call\", raise_missing)\n\n    class FakeCalledProcessError(subprocess.CalledProcessError):\n        def __init__(self) -> None:\n            super().__init__(returncode=1, cmd=[\"pip\"], stderr=\"bad\")\n\n    def raising_run(*a: Any, **k: dict[str, Any]) -> None:\n        raise FakeCalledProcessError()\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\",\n        raising_run,\n    )\n    with pytest.raises(RuntimeError) as e:\n        _ensure_uv_available()\n    assert \"Unable to install uv\" in str(e.value)\n\n\ndef test_add_appserver_pypi_install_calls_uv_with_prefix(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, resolve_venv_to_pkg: None\n) -> None:\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.are_we_editable_mode\", lambda: False\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._is_missing_or_outdated\",\n        lambda p: Version(\"1.2.3\"),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        lambda *a, **k: None,\n    )\n\n    _install_and_add_appserver_if_missing(Path(\"pkg\"), tmp_path)\n\n    assert len(cmds) >= 2\n    # Last call is the uv pip install\n    install_cmd = cmds[-1]\n    assert install_cmd[:3] == [\"uv\", \"pip\", \"install\"]\n    assert any(arg == \"llama-agents-appserver==1.2.3\" for arg in install_cmd)\n    assert \"--prefix\" in install_cmd\n    assert install_cmd[install_cmd.index(\"--prefix\") + 1] == str(pkg_dir / \".venv\")\n\n\ndef test_add_appserver_install_targets_resolved_venv_when_outside_pkg(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"\n    Install must target whichever venv ``_resolve_project_venv`` returns, even\n    when that path is outside ``<pkg>/.venv`` (e.g. a uv workspace member whose\n    venv lives at the workspace root). Regression guard for the install/runtime\n    venv-path disagreement that broke ``llamactl dev validate`` in workspace\n    layouts.\n    \"\"\"\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n    # Simulate uv picking a venv outside the target dir (what happens when the\n    # target is a workspace member).\n    resolved_venv = tmp_path / \"elsewhere\" / \".venv\"\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.are_we_editable_mode\", lambda: False\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._is_missing_or_outdated\",\n        lambda p: Version(\"1.2.3\"),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        lambda *a, **k: None,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._resolve_project_venv\",\n        lambda source_root, path: resolved_venv,\n    )\n\n    _install_and_add_appserver_if_missing(Path(\"pkg\"), tmp_path)\n\n    install_cmd = cmds[-1]\n    assert install_cmd[:3] == [\"uv\", \"pip\", \"install\"]\n    assert \"--prefix\" in install_cmd\n    assert install_cmd[install_cmd.index(\"--prefix\") + 1] == str(resolved_venv)\n    assert str(pkg_dir / \".venv\") not in install_cmd\n\n\ndef test_add_appserver_sdists_install(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, resolve_venv_to_pkg: None\n) -> None:\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        lambda *a, **k: None,\n    )\n\n    s1 = tmp_path / \"d1\" / \"a-0.1.0.tar.gz\"\n    s2 = tmp_path / \"d2\" / \"b-0.2.0.tar.gz\"\n    s1.parent.mkdir(parents=True, exist_ok=True)\n    s2.parent.mkdir(parents=True, exist_ok=True)\n    s1.write_text(\"x\")\n    s2.write_text(\"y\")\n\n    _install_and_add_appserver_if_missing(Path(\"pkg\"), tmp_path, sdists=[s1, s2])\n\n    assert len(cmds) >= 2\n    install_cmd = cmds[-1]\n    assert install_cmd[:3] == [\"uv\", \"pip\", \"install\"]\n    assert str(s1.resolve()) in install_cmd and str(s2.resolve()) in install_cmd\n    assert \"--prefix\" in install_cmd\n\n\ndef test_add_appserver_editable_install(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, resolve_venv_to_pkg: None\n) -> None:\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.are_we_editable_mode\", lambda: True\n    )\n    # set dev pyproject in sibling directory\n    dev_dir = tmp_path / \"appserver_src\"\n    dev_dir.mkdir()\n    (dev_dir / \"pyproject.toml\").write_text(\"[project]\\nname='app'\\n\")\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._find_development_pyproject\",\n        lambda: dev_dir / \"pyproject.toml\",\n    )\n    venv_path = pkg_dir / \".venv\"\n    venv_path.mkdir()\n    (venv_path / \"pyvenv.cfg\").write_text(\n        f\"version_info={sys.version_info.major}.{sys.version_info.minor}\\n\"\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        lambda *a, **k: None,\n    )\n\n    _install_and_add_appserver_if_missing(Path(\"pkg\"), tmp_path)\n\n    assert len(cmds) >= 2\n    install_cmd = cmds[-1]\n    assert install_cmd[:5] == [\n        \"uv\",\n        \"pip\",\n        \"install\",\n        \"--reinstall-package\",\n        \"llama-agents-appserver\",\n    ]\n    # file:// url should be present\n    assert any(str(arg).startswith(\"file://\") for arg in install_cmd)\n    assert \"--prefix\" in install_cmd\n\n\ndef test_install_ui_runs_pnpm_and_validates(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    ui_root = tmp_path / \"ui\"\n    ui_root.mkdir(parents=True, exist_ok=True)\n    cfg = DeploymentConfig(\n        name=\"n\",\n        ui=UIConfig(directory=\"ui\", proxy_port=3001),\n    )\n    ran: dict[str, Path | None] = {\"cwd\": None}\n\n    def run_capture(cmd: list[str], cwd: Path | None = None, **kwargs: Any) -> None:\n        ran[\"cwd\"] = cwd\n        assert cmd[:2] == [\"npm\", \"install\"]\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\",\n        run_capture,\n    )\n    install_ui(cfg, tmp_path)\n    assert ran[\"cwd\"] == ui_root\n\n\ndef test_start_dev_ui_process_port_open_and_spawn(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    cfg = DeploymentConfig(name=\"n\", ui=UIConfig(directory=\"ui\"))\n    # Configure API and UI proxy ports via env-backed settings\n    monkeypatch.setenv(\"LLAMA_DEPLOY_APISERVER_PORT\", \"4501\")\n    monkeypatch.setenv(\"LLAMA_DEPLOY_APISERVER_PROXY_UI_PORT\", \"3001\")\n    api_settings = ApiserverSettings()\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.socket.socket.connect_ex\",\n        lambda *a, **k: 0,\n    )\n    assert start_dev_ui_process(tmp_path, api_settings, cfg) is None\n\n    # Case: port not open -> spawn process and return immediately\n    class FakeProc:\n        def __init__(self) -> None:\n            self.terminated = False\n\n        def terminate(self) -> None:\n            self.terminated = True\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.socket.socket.connect_ex\",\n        lambda *a, **k: 1,\n    )\n    fake = FakeProc()\n\n    def make_proc(*a: Any, **k: dict[str, Any]) -> FakeProc:\n        # env should include base path and port\n        env = k.get(\"env\", {})\n        assert env.get(\"LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH\") == \"/deployments/n/ui\"\n        assert env.get(\"LLAMA_DEPLOY_DEPLOYMENT_NAME\") == \"n\"\n        assert env.get(\"PORT\") == \"3001\"\n        return fake\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.spawn_process\",\n        make_proc,\n    )\n    p = start_dev_ui_process(tmp_path, api_settings, cfg)\n    assert p is fake\n\n\ndef test_validate_path_is_safe_rejects_escape(tmp_path: Path) -> None:\n    with pytest.raises(RuntimeError):\n        validate_path_traversal(Path(\"../bad\"), tmp_path)\n\n\ndef test_get_installed_version_within_target_cases(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    # success\n    monkeypatch.setattr(\n        \"subprocess.check_output\",\n        lambda *a, **k: b\"1.2.3\\n\",\n    )\n    v = _get_installed_version_within_target(tmp_path)\n    assert str(v) == \"1.2.3\"\n\n    # invalid version\n    monkeypatch.setattr(\n        \"subprocess.check_output\",\n        lambda *a, **k: b\"not-a-version\\n\",\n    )\n    assert _get_installed_version_within_target(tmp_path) is None\n\n    # missing\n    def raise_cpe(*a: Any, **k: dict[str, Any]) -> None:\n        raise subprocess.CalledProcessError(1, cmd=[\"uv\"])  # noqa: F841\n\n    monkeypatch.setattr(\"subprocess.check_output\", raise_cpe)\n    assert _get_installed_version_within_target(tmp_path) is None\n\n\ndef test_current_and_outdated_logic(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_current_version\",\n        lambda: Version(\"2.0.0\"),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_installed_version_within_target\",\n        lambda p: None,\n    )\n    assert _is_missing_or_outdated(tmp_path) == Version(\"2.0.0\")\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_installed_version_within_target\",\n        lambda p: Version(\"1.0.0\"),\n    )\n    assert _is_missing_or_outdated(tmp_path) == Version(\"2.0.0\")\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_installed_version_within_target\",\n        lambda p: Version(\"2.0.0\"),\n    )\n    assert _is_missing_or_outdated(tmp_path) is None\n\n\ndef test_add_appserver_target_version_installs_from_pypi(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, resolve_venv_to_pkg: None\n) -> None:\n    \"\"\"When target_version is set, install that exact version from PyPI.\"\"\"\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.are_we_editable_mode\", lambda: False\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        lambda *a, **k: None,\n    )\n\n    _install_and_add_appserver_if_missing(\n        Path(\"pkg\"), tmp_path, target_version=\"0.4.15\"\n    )\n\n    assert len(cmds) >= 2\n    install_cmd = cmds[-1]\n    assert install_cmd[:3] == [\"uv\", \"pip\", \"install\"]\n    # 0.4.15 <= 0.5.3 so it uses the old dist name\n    assert any(arg == \"llama-deploy-appserver==0.4.15\" for arg in install_cmd)\n    assert \"--prefix\" in install_cmd\n\n\ndef test_add_appserver_target_version_ignored_in_editable_mode(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, resolve_venv_to_pkg: None\n) -> None:\n    \"\"\"In editable mode, target_version is ignored — editable installs use local source.\"\"\"\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.are_we_editable_mode\", lambda: True\n    )\n    dev_dir = tmp_path / \"appserver_src\"\n    dev_dir.mkdir()\n    (dev_dir / \"pyproject.toml\").write_text(\"[project]\\nname='app'\\n\")\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._find_development_pyproject\",\n        lambda: dev_dir / \"pyproject.toml\",\n    )\n    venv_path = pkg_dir / \".venv\"\n    venv_path.mkdir()\n    (venv_path / \"pyvenv.cfg\").write_text(\n        f\"version_info={sys.version_info.major}.{sys.version_info.minor}\\n\"\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        lambda *a, **k: None,\n    )\n\n    _install_and_add_appserver_if_missing(\n        Path(\"pkg\"), tmp_path, target_version=\"0.4.15\"\n    )\n\n    assert len(cmds) >= 2\n    install_cmd = cmds[-1]\n    assert install_cmd[:5] == [\n        \"uv\",\n        \"pip\",\n        \"install\",\n        \"--reinstall-package\",\n        \"llama-agents-appserver\",\n    ]\n    assert not any(\"llama-agents-appserver==0.4.15\" in str(arg) for arg in install_cmd)\n\n\ndef test_add_appserver_target_version_ignored_when_sdists_provided(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, resolve_venv_to_pkg: None\n) -> None:\n    \"\"\"When sdists are provided, they take priority over target_version.\"\"\"\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n        return None\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        lambda *a, **k: None,\n    )\n\n    s1 = tmp_path / \"d1\" / \"a-0.1.0.tar.gz\"\n    s2 = tmp_path / \"d2\" / \"b-0.2.0.tar.gz\"\n    s1.parent.mkdir(parents=True, exist_ok=True)\n    s2.parent.mkdir(parents=True, exist_ok=True)\n    s1.write_text(\"x\")\n    s2.write_text(\"y\")\n\n    _install_and_add_appserver_if_missing(\n        Path(\"pkg\"), tmp_path, sdists=[s1, s2], target_version=\"0.4.15\"\n    )\n\n    assert len(cmds) >= 2\n    install_cmd = cmds[-1]\n    assert install_cmd[:3] == [\"uv\", \"pip\", \"install\"]\n    assert str(s1.resolve()) in install_cmd and str(s2.resolve()) in install_cmd\n    assert \"llama-agents-appserver==0.4.15\" not in install_cmd\n\n\ndef test_get_workflows_version_in_target_success(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    monkeypatch.setattr(\n        \"subprocess.check_output\",\n        lambda *a, **k: b\"2.14.0\\n\",\n    )\n    v = _get_installed_version_within_target(tmp_path, package=\"llama-index-workflows\")\n    assert v == Version(\"2.14.0\")\n\n\ndef test_get_workflows_version_in_target_missing(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    monkeypatch.setattr(\n        \"subprocess.check_output\",\n        lambda *a, **k: b\"\\n\",\n    )\n    assert (\n        _get_installed_version_within_target(tmp_path, package=\"llama-index-workflows\")\n        is None\n    )\n\n\ndef test_get_workflows_version_in_target_error(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    def raise_cpe(*a: Any, **k: dict[str, Any]) -> None:\n        raise subprocess.CalledProcessError(1, cmd=[\"uv\"])\n\n    monkeypatch.setattr(\"subprocess.check_output\", raise_cpe)\n    assert (\n        _get_installed_version_within_target(tmp_path, package=\"llama-index-workflows\")\n        is None\n    )\n\n\ndef test_get_appserver_workflows_requirement() -> None:\n    _get_appserver_workflows_requirement.cache_clear()\n    req = _get_appserver_workflows_requirement()\n    assert req is not None\n    assert Version(\"2.16.0\") in req\n    assert Version(\"2.14.0\") not in req\n    assert Version(\"3.0.0\") not in req\n\n\ndef test_get_appserver_workflows_requirement_missing(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    _get_appserver_workflows_requirement.cache_clear()\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.pkg_requires\",\n        lambda _: [\"some-other-package>=1.0\"],\n    )\n    assert _get_appserver_workflows_requirement() is None\n\n\ndef test_ensure_compatible_workflows_compatible_noop(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"Compatible version -> no uv add call.\"\"\"\n    from packaging.specifiers import SpecifierSet\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_appserver_workflows_requirement\",\n        lambda: SpecifierSet(\">=2.16.0,<3.0.0\"),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_installed_version_within_target\",\n        lambda p, **kw: Version(\"2.16.1\"),\n    )\n\n    uv_calls: list[list[str]] = []\n\n    def track_run_uv(\n        source_root: Path, path: Path, cmd: str, args: list[str] = [], **kwargs: Any\n    ) -> None:\n        uv_calls.append([cmd] + args)\n\n    monkeypatch.setattr(\"llama_agents.appserver.workflow_loader.run_uv\", track_run_uv)\n\n    _ensure_compatible_workflows(tmp_path, Path(\".\"))\n    assert len(uv_calls) == 0\n\n\ndef test_ensure_compatible_workflows_incompatible_auto_updates(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"Incompatible version -> calls uv add to update.\"\"\"\n    from packaging.specifiers import SpecifierSet\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_appserver_workflows_requirement\",\n        lambda: SpecifierSet(\">=2.16.0,<3.0.0\"),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_installed_version_within_target\",\n        lambda p, **kw: Version(\"2.14.0\"),\n    )\n\n    uv_calls: list[tuple[str, list[str]]] = []\n\n    def track_run_uv(\n        source_root: Path,\n        path: Path,\n        cmd: str,\n        args: list[str] = [],\n        extra_env: dict[str, str] | None = None,\n    ) -> None:\n        uv_calls.append((cmd, args))\n\n    monkeypatch.setattr(\"llama_agents.appserver.workflow_loader.run_uv\", track_run_uv)\n\n    _ensure_compatible_workflows(tmp_path, Path(\".\"))\n    assert len(uv_calls) == 1\n    cmd, args = uv_calls[0]\n    assert cmd == \"add\"\n    assert any(\"llama-index-workflows\" in a for a in args)\n\n\ndef test_ensure_compatible_workflows_not_installed_noop(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"Not installed -> no action (appserver install will bring it in).\"\"\"\n    from packaging.specifiers import SpecifierSet\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_appserver_workflows_requirement\",\n        lambda: SpecifierSet(\">=2.16.0,<3.0.0\"),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_installed_version_within_target\",\n        lambda p, **kw: None,\n    )\n\n    uv_calls: list[Any] = []\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_uv\",\n        lambda *a, **k: uv_calls.append(1),\n    )\n\n    _ensure_compatible_workflows(tmp_path, Path(\".\"))\n    assert len(uv_calls) == 0\n\n\ndef test_ensure_compatible_workflows_update_fails_raises(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    \"\"\"If uv add fails, raises RuntimeError with helpful message.\"\"\"\n    from packaging.specifiers import SpecifierSet\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_appserver_workflows_requirement\",\n        lambda: SpecifierSet(\">=2.16.0,<3.0.0\"),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._get_installed_version_within_target\",\n        lambda p, **kw: Version(\"2.14.0\"),\n    )\n\n    def fail_run_uv(*a: Any, **k: Any) -> None:\n        raise subprocess.CalledProcessError(1, cmd=[\"uv\", \"add\"])\n\n    monkeypatch.setattr(\"llama_agents.appserver.workflow_loader.run_uv\", fail_run_uv)\n\n    with pytest.raises(RuntimeError, match=\"conflicting constraints\"):\n        _ensure_compatible_workflows(tmp_path, Path(\".\"))\n\n\ndef test_install_calls_ensure_compatible_workflows(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, resolve_venv_to_pkg: None\n) -> None:\n    \"\"\"Integration: _install_and_add_appserver_if_missing calls _ensure_compatible_workflows.\"\"\"\n    pkg_dir = tmp_path / \"pkg\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"pyproject.toml\").write_text(\"[project]\\nname='x'\\n\")\n\n    cmds: list[list[str]] = []\n\n    def run_capture(cmd: list[str], **kwargs: Any) -> None:\n        cmds.append(cmd)\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.run_process\", run_capture\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader.are_we_editable_mode\", lambda: False\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._is_missing_or_outdated\",\n        lambda p: Version(\"1.2.3\"),\n    )\n\n    compat_called = {\"called\": False}\n\n    def mock_ensure_compat(source_root: Path, path: Path) -> None:\n        compat_called[\"called\"] = True\n\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_compatible_workflows\",\n        mock_ensure_compat,\n    )\n\n    _install_and_add_appserver_if_missing(Path(\"pkg\"), tmp_path)\n    assert compat_called[\"called\"]\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_workflow_loader_load_workflows.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\nfrom llama_agents.appserver.workflow_loader import load_workflows\nfrom llama_agents.core.deployment_config import DeploymentConfig\nfrom llama_agents.server import WorkflowServer\n\n\ndef test_load_workflows_module_path(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    # Create a simple module file under rc_path\n    pkg_dir = tmp_path / \"src\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"m.py\").write_text(\"x = 1\\n\")\n\n    cfg = DeploymentConfig(\n        name=\"n\",\n        workflows={\"svc\": \"m:workflow\"},\n    )\n\n    # Mock importlib to confirm the module name used is derived from file name\n    with mock.patch(\"llama_agents.appserver.workflow_loader.importlib\") as imp:\n        imp.import_module.return_value = mock.MagicMock(workflow=object())\n        load_workflows(cfg)\n        # import_module called with the module filename\n        assert imp.import_module.call_args[0][0] in (\"m\",)\n\n\ndef test_load_workflows_from_workflow_server_app() -> None:\n    \"\"\"When config.app points to a WorkflowServer, load_workflows uses get_workflows().\"\"\"\n    sentinel_wf = object()\n    fake_server = mock.MagicMock(spec=WorkflowServer)\n    fake_server.get_workflows.return_value = {\"my_wf\": sentinel_wf}\n\n    cfg = DeploymentConfig(name=\"n\", app=\"fake_mod:app\")\n\n    with mock.patch(\"llama_agents.appserver.workflow_loader.importlib\") as imp:\n        imp.import_module.return_value = mock.MagicMock(app=fake_server)\n        result = load_workflows(cfg)\n\n    assert result == {\"my_wf\": sentinel_wf}\n    fake_server.get_workflows.assert_called_once()\n"
  },
  {
    "path": "packages/llama-agents-appserver/tests/test_workflow_loader_streaming.py",
    "content": "from __future__ import annotations\n\nimport io\nimport subprocess\nimport time\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.appserver.process_utils import (\n    _use_color,\n    run_process,\n    spawn_process,\n)\nfrom llama_agents.appserver.workflow_loader import (\n    inject_appserver_into_target,\n)\nfrom llama_agents.core.deployment_config import DeploymentConfig\n\n\n@pytest.fixture(autouse=True)\ndef _no_color(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.delenv(\"FORCE_COLOR\", raising=False)\n    _use_color.cache_clear()\n\n\nclass _FakePopen:\n    def __init__(self, text: str, ret: int = 0) -> None:\n        self.stdout = io.StringIO(text)\n        self.stderr = io.StringIO(\"\")\n        self._ret = ret\n\n    def wait(self) -> int:\n        return self._ret\n\n\ndef test_run_process_success_and_failure(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    # success path\n    monkeypatch.setattr(\n        \"subprocess.Popen\",\n        lambda *a, **k: _FakePopen(\"line1\\nline2\\n\", ret=0),\n    )\n    run_process([\"echo\"], prefix=\"[x]\")\n    out = capsys.readouterr().out\n    assert \"[x] line1\" in out and \"[x] line2\" in out\n\n    # failure path raises\n    monkeypatch.setattr(\n        \"subprocess.Popen\",\n        lambda *a, **k: _FakePopen(\"oops\\n\", ret=5),\n    )\n    with pytest.raises(subprocess.CalledProcessError):\n        run_process([\"false\"], prefix=\"[x]\")\n\n\ndef test_spawn_process_streams_in_background(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    # Provide a short output that will be consumed by the background thread\n    monkeypatch.setattr(\n        \"subprocess.Popen\",\n        lambda *a, **k: _FakePopen(\"a\\nb\\n\", ret=0),\n    )\n    spawn_process([\"cmd\"], prefix=\"[p]\", color_code=\"35\")\n    # Give the background thread a moment to flush\n    time.sleep(0.05)\n    out = capsys.readouterr().out\n    assert \"[p] a\" in out and \"[p] b\" in out\n\n\ndef test_install_python_dependencies_calls_when_target_found(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    calls: dict[str, int] = {\"ensure\": 0, \"install\": 0}\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._ensure_uv_available\",\n        lambda: calls.__setitem__(\"ensure\", calls[\"ensure\"] + 1),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.appserver.workflow_loader._install_and_add_appserver_if_missing\",\n        lambda *a, **k: calls.__setitem__(\"install\", calls[\"install\"] + 1),\n    )\n\n    cfg = DeploymentConfig(name=\"n\", workflows={})\n    inject_appserver_into_target(cfg, tmp_path)\n    assert calls == {\"ensure\": 1, \"install\": 1}\n"
  },
  {
    "path": "packages/llama-agents-client/CHANGELOG.md",
    "content": "# llama-agents-client\n\n## 0.3.7\n\n### Patch Changes\n\n- Updated dependencies [9bf247a]\n- Updated dependencies [2cc9fae]\n  - llama-index-workflows@2.20.0\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies [f7e037e]\n  - llama-index-workflows@2.19.1\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [2592c80]\n  - llama-index-workflows@2.19.0\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies [43ff242]\n  - llama-index-workflows@2.18.0\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies [b8c7c7e]\n  - llama-index-workflows@2.17.3\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [7e06f87]\n  - llama-index-workflows@2.17.2\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [979d68b]\n- Updated dependencies [983f6f6]\n  - llama-index-workflows@2.17.1\n\n## 0.3.0\n\n### Minor Changes\n\n- b32ec53: Drop python 3.9 support\n\n### Patch Changes\n\n- Updated dependencies [7fc1aae]\n- Updated dependencies [b32ec53]\n  - llama-index-workflows@2.17.0\n\n## 0.2.3\n\n### Patch Changes\n\n- Updated dependencies [c7bbedb]\n- Updated dependencies [703ec92]\n  - llama-index-workflows@2.16.1\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [5e7f9e5]\n- Updated dependencies [9f26314]\n  - llama-index-workflows@2.16.0\n\n## 0.2.1\n\n### Patch Changes\n\n- 6605457: Bump dependency requirements\n- Updated dependencies [6ec262c]\n  - llama-index-workflows@2.15.1\n\n## 0.2.0\n\n### Minor Changes\n\n- 4ba29dc: Add SSE event streaming with sequence-based cursors and automatic reconnection on connection drop\n\n### Patch Changes\n\n- 62ffc15: Add last sequence id accessor to workflow client event stream\n- 23385c7: Add better 500 error logging and structured responses\n- Updated dependencies [77a3f9c]\n- Updated dependencies [707a254]\n- Updated dependencies [05f5f4e]\n- Updated dependencies [3c22216]\n- Updated dependencies [96e437e]\n  - llama-index-workflows@2.15.0\n\n## 0.2.0-rc.1\n\n### Patch Changes\n\n- 8762129: Add last sequence id accessor to workflow client event stream\n- Updated dependencies [3720c61]\n- Updated dependencies [a2aad32]\n  - llama-index-workflows@2.15.0-rc.1\n\n## 0.2.0-rc.0\n\n### Minor Changes\n\n- 528d562: Add SSE event streaming with sequence-based cursors and automatic reconnection on connection drop\n\n### Patch Changes\n\n- Updated dependencies [e981f73]\n- Updated dependencies [b515a46]\n- Updated dependencies [7433d4c]\n  - llama-index-workflows@2.15.0-rc.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [3590913]\n- Updated dependencies [7433d4c]\n  - llama-index-workflows@2.14.2\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [6ece797]\n  - llama-index-workflows@2.14.1\n\n## 0.1.1\n\n### Patch Changes\n\n- db90f89: Separate server/client to their own packages under a llama_agents namespace\n- Updated dependencies [73c1254]\n- Updated dependencies [45e7614]\n- Updated dependencies [45e7614]\n- Updated dependencies [2900f58]\n- Updated dependencies [6fdc45c]\n  - llama-index-workflows@2.14.0\n"
  },
  {
    "path": "packages/llama-agents-client/README.md",
    "content": "# LlamaAgents Client\n\nAsync HTTP client for interacting with deployed [`llama-agents-server`](https://pypi.org/project/llama-agents-server/) instances.\n\n## Installation\n\n```bash\npip install llama-agents-client\n```\n\n## Quick Start\n\n```python\nimport asyncio\nfrom llama_agents.client import WorkflowClient\n\nasync def main():\n    client = WorkflowClient(base_url=\"http://localhost:8080\")\n\n    # Run a workflow asynchronously\n    handler = await client.run_workflow_nowait(\"my_workflow\")\n\n    # Stream events as they are produced\n    async for event in client.get_workflow_events(handler.handler_id):\n        print(f\"Event: {event.type} -> {event.value}\")\n\n    # Get the final result\n    result = await client.get_handler(handler.handler_id)\n    print(f\"Result: {result.result} (status: {result.status})\")\n\nasyncio.run(main())\n```\n\n## Features\n\n- Run workflows synchronously or asynchronously\n- Stream events in real-time as a workflow executes\n- Human-in-the-loop support via `send_event` for injecting events into running workflows\n- Bring your own `httpx.AsyncClient` for custom auth, headers, or transport\n\n## Documentation\n\nSee the full [deployment guide](https://developers.llamaindex.ai/python/llamaagents/workflows/deployment/) for detailed usage and API reference.\n"
  },
  {
    "path": "packages/llama-agents-client/package.json",
    "content": "{\n  \"name\": \"llama-agents-client\",\n  \"version\": \"0.3.7\",\n  \"private\": false,\n  \"license\": \"MIT\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"llama-index-workflows\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/llama-agents-client/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.9.6,<0.10.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"pytest>=8.4.2\",\n  \"pytest-asyncio>=1.0.0\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"llama-agents-server\"\n]\n\n[project]\nname = \"llama-agents-client\"\nversion = \"0.3.7\"\ndescription = \"HTTP client for connecting to and interacting with LlamaIndex workflow servers\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n  \"httpx>=0.28.1,<1\",\n  \"llama-index-workflows>=2.15.0,<3.0.0\"\n]\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"module\"\nasyncio_default_test_loop_scope = \"module\"\ntestpaths = [\"tests\"]\naddopts = \"-nauto --timeout=10\"\n\n[tool.uv.build-backend]\nmodule-name = \"llama_agents.client\"\n\n[tool.uv.sources]\nllama-index-workflows = {workspace = true}\n"
  },
  {
    "path": "packages/llama-agents-client/src/llama_agents/client/__init__.py",
    "content": "from .client import EventStream, WorkflowClient\nfrom .protocol import (\n    CancelHandlerResponse,\n    HandlerData,\n    HandlersListResponse,\n    SendEventResponse,\n)\nfrom .protocol.serializable_events import EventEnvelopeWithMetadata\n\n__all__ = [\n    \"CancelHandlerResponse\",\n    \"EventEnvelopeWithMetadata\",\n    \"EventStream\",\n    \"HandlerData\",\n    \"HandlersListResponse\",\n    \"SendEventResponse\",\n    \"WorkflowClient\",\n]\n"
  },
  {
    "path": "packages/llama-agents-client/src/llama_agents/client/client.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\nfrom typing import (\n    Any,\n    AsyncGenerator,\n    AsyncIterator,\n    Literal,\n    overload,\n)\n\nimport httpx\nfrom workflows import Context\nfrom workflows.events import Event, StartEvent\n\nfrom .protocol import (\n    CancelHandlerResponse,\n    HandlerData,\n    HandlersListResponse,\n    HealthResponse,\n    SendEventResponse,\n    Status,\n    WorkflowsListResponse,\n)\nfrom .protocol.serializable_events import (\n    EventEnvelope,\n    EventEnvelopeWithMetadata,\n)\n\n\ndef _raise_for_status_with_body(response: httpx.Response) -> None:\n    \"\"\"\n    Raise an HTTPStatusError with the first 200 characters of the response body\n    for 400 and 500 level errors.\n    \"\"\"\n    try:\n        response.raise_for_status()\n    except httpx.HTTPStatusError as e:\n        if 400 <= e.response.status_code < 600:\n            body_preview = e.response.text[:200]\n            method = e.request.method\n            url = e.request.url\n            status_code = e.response.status_code\n            raise httpx.HTTPStatusError(\n                f\"{status_code} {e.response.reason_phrase} for {method} {url}. Response: {body_preview}\",\n                request=e.request,\n                response=e.response,\n            ) from e\n        raise\n\n\n@dataclass(frozen=True)\nclass _QueuedEvent:\n    sequence: int | Literal[\"now\"]\n    event: EventEnvelopeWithMetadata\n\n\n@dataclass(frozen=True)\nclass _QueuedError:\n    error: BaseException\n\n\n@dataclass(frozen=True)\nclass _QueuedDone:\n    pass\n\n\n_QueueItem = _QueuedEvent | _QueuedError | _QueuedDone\n\n\nclass EventStream:\n    \"\"\"Async iterator over workflow events that exposes the current stream position.\n\n    Returned by ``WorkflowClient.get_workflow_events()``. Use\n    ``last_sequence`` to capture the cursor for resuming later::\n\n        stream = client.get_workflow_events(handler_id)\n        async for event in stream:\n            print(event.type, stream.last_sequence)\n\n        # Resume from where we left off:\n        stream = client.get_workflow_events(\n            handler_id, after_sequence=stream.last_sequence\n        )\n    \"\"\"\n\n    def __init__(\n        self,\n        queue: asyncio.Queue[_QueueItem],\n        task: asyncio.Task[None] | None,\n        initial_sequence: int | Literal[\"now\"],\n    ) -> None:\n        self._queue = queue\n        self._task = task\n        self._last_sequence: int | Literal[\"now\"] = initial_sequence\n        self._iter_started = False\n\n    @property\n    def last_sequence(self) -> int | Literal[\"now\"]:\n        \"\"\"The sequence number of the most recently yielded event, or the\n        initial ``after_sequence`` value if no events have been yielded yet.\"\"\"\n        return self._last_sequence\n\n    def __aiter__(self) -> AsyncIterator[EventEnvelopeWithMetadata]:\n        if self._iter_started:\n            raise RuntimeError(\"EventStream can only be iterated once\")\n        self._iter_started = True\n        return self._iterate()\n\n    async def _iterate(self) -> AsyncGenerator[EventEnvelopeWithMetadata, None]:\n        try:\n            while True:\n                item = await self._queue.get()\n                if isinstance(item, _QueuedDone):\n                    return\n                if isinstance(item, _QueuedError):\n                    raise item.error\n                self._last_sequence = item.sequence\n                yield item.event\n        finally:\n            await self.aclose()\n\n    async def aclose(self) -> None:\n        \"\"\"Cancel the background reader and release resources.\"\"\"\n        if self._task is None:\n            return\n        task, self._task = self._task, None\n        task.cancel()\n        try:\n            await task\n        except (asyncio.CancelledError, Exception):\n            pass\n\n\nclass WorkflowClient:\n    \"\"\"Python client for interacting with a ``WorkflowServer``.\n\n    Provides methods for listing workflows, running them synchronously or\n    asynchronously, streaming events, and sending events for\n    human-in-the-loop workflows.\n\n    Example:\n\n        from llama_agents.client import WorkflowClient\n        from workflows.events import StartEvent\n\n        client = WorkflowClient(base_url=\"http://localhost:8080\")\n\n        # Run synchronously\n        result = await client.run_workflow(\"greet\", start_event=StartEvent(name=\"Ada\"))\n        print(result.result)\n\n        # Run async and stream events\n        handler = await client.run_workflow_nowait(\"greet\")\n        stream = client.get_workflow_events(handler.handler_id)\n        async for event in stream:\n            print(event.type, event.value)\n\n    Args:\n        base_url: Base URL of the workflow server (e.g. ``\"http://localhost:8080\"``).\n        httpx_client: Pre-configured ``httpx.AsyncClient``. Use this for\n            custom auth headers, timeouts, or transport configuration.\n\n    Provide exactly one of ``base_url`` or ``httpx_client``.\n    \"\"\"\n\n    @overload\n    def __init__(self, *, httpx_client: httpx.AsyncClient): ...\n    @overload\n    def __init__(\n        self,\n        *,\n        base_url: str,\n    ): ...\n\n    def __init__(\n        self,\n        *,\n        httpx_client: httpx.AsyncClient | None = None,\n        base_url: str | None = None,\n    ):\n        if httpx_client is None and base_url is None:\n            raise ValueError(\"Either httpx_client or base_url must be provided\")\n        if httpx_client is not None and base_url is not None:\n            raise ValueError(\"Only one of httpx_client or base_url must be provided\")\n        self.httpx_client = httpx_client\n        self.base_url = base_url\n\n    @asynccontextmanager\n    async def _get_client(self) -> AsyncIterator[httpx.AsyncClient]:\n        if self.httpx_client:\n            yield self.httpx_client\n        else:\n            async with httpx.AsyncClient(base_url=self.base_url or \"\") as client:\n                yield client\n\n    async def is_healthy(self) -> HealthResponse:\n        \"\"\"Check whether the workflow server is healthy.\n\n        Raises:\n            httpx.HTTPStatusError: If the server returns an error status.\n        \"\"\"\n        async with self._get_client() as client:\n            response = await client.get(\"/health\")\n            _raise_for_status_with_body(response)\n            return HealthResponse.model_validate(response.json())\n\n    async def list_workflows(self) -> WorkflowsListResponse:\n        \"\"\"List the names of all workflows registered on the server.\"\"\"\n        async with self._get_client() as client:\n            response = await client.get(\"/workflows\")\n\n            _raise_for_status_with_body(response)\n\n            return WorkflowsListResponse.model_validate(response.json())\n\n    async def run_workflow(\n        self,\n        workflow_name: str,\n        handler_id: str | None = None,\n        start_event: StartEvent | dict[str, Any] | None = None,\n        context: Context | dict[str, Any] | None = None,\n    ) -> HandlerData:\n        \"\"\"Run the workflow and block until completion.\n\n        Args:\n            workflow_name: Name of the registered workflow to run.\n            start_event: Input event for the workflow. Can be a ``StartEvent``\n                instance or a plain dict.\n            context: Workflow context to restore, for continuing a previous run.\n            handler_id: Handler identifier to continue from a previous\n                completed run.\n\n        Returns:\n            HandlerData: Handler metadata including the final result.\n        \"\"\"\n        if start_event is not None:\n            try:\n                start_event = _serialize_event(start_event, bare=True)\n            except Exception as e:\n                raise ValueError(\n                    f\"Impossible to serialize the start event because of: {e}\"\n                )\n        if isinstance(context, Context):\n            try:\n                context = context.to_dict()\n            except Exception as e:\n                raise ValueError(f\"Impossible to serialize the context because of: {e}\")\n        request_body: dict[str, Any] = {\n            \"start_event\": start_event\n            if start_event is not None\n            else _serialize_event(StartEvent(), bare=True),\n            \"context\": context if context is not None else {},\n        }\n        if handler_id:\n            request_body[\"handler_id\"] = handler_id\n        async with self._get_client() as client:\n            response = await client.post(\n                f\"/workflows/{workflow_name}/run\", json=request_body\n            )\n\n            _raise_for_status_with_body(response)\n\n            return HandlerData.model_validate(response.json())\n\n    async def run_workflow_nowait(\n        self,\n        workflow_name: str,\n        handler_id: str | None = None,\n        start_event: StartEvent | dict[str, Any] | None = None,\n        context: Context | dict[str, Any] | None = None,\n    ) -> HandlerData:\n        \"\"\"Start the workflow without waiting for completion.\n\n        Use the returned ``handler_id`` to stream events, poll for results,\n        or send events.\n\n        Args:\n            workflow_name: Name of the registered workflow to run.\n            start_event: Input event for the workflow. Can be a ``StartEvent``\n                instance or a plain dict.\n            context: Workflow context to restore, for continuing a previous run.\n            handler_id: Handler identifier to continue from a previous\n                completed run.\n\n        Returns:\n            HandlerData: Handler metadata including the ``handler_id``.\n        \"\"\"\n        if start_event is not None:\n            try:\n                start_event = _serialize_event(start_event)\n            except Exception as e:\n                raise ValueError(\n                    f\"Impossible to serialize the start event because of: {e}\"\n                )\n        if isinstance(context, Context):\n            try:\n                context = context.to_dict()\n            except Exception as e:\n                raise ValueError(f\"Impossible to serialize the context because of: {e}\")\n        request_body: dict[str, Any] = {\n            \"start_event\": start_event\n            if start_event is not None\n            else _serialize_event(StartEvent()),\n            \"context\": context if context is not None else {},\n        }\n        if handler_id:\n            request_body[\"handler_id\"] = handler_id\n        async with self._get_client() as client:\n            response = await client.post(\n                f\"/workflows/{workflow_name}/run-nowait\", json=request_body\n            )\n\n            _raise_for_status_with_body(response)\n\n            return HandlerData.model_validate(response.json())\n\n    def get_workflow_events(\n        self,\n        handler_id: str,\n        include_internal_events: bool = False,\n        after_sequence: int | Literal[\"now\"] = -1,\n        max_reconnect_attempts: int = 3,\n    ) -> EventStream:\n        \"\"\"Stream events as they are produced by the workflow.\n\n        Returns an ``EventStream`` whose ``last_sequence`` property tracks\n        the sequence number of the most recently yielded event. Uses SSE\n        and automatically reconnects from the last received event on\n        connection drops.\n\n        Example:\n\n            stream = client.get_workflow_events(handler_id)\n            async for event in stream:\n                print(event.type, stream.last_sequence)\n\n        Args:\n            handler_id: ID of the handler running the workflow.\n            include_internal_events: Include internal dispatch events.\n                Defaults to ``False``.\n            after_sequence: Where to start streaming. ``-1`` (default) streams\n                all events from the beginning. ``\"now\"`` skips existing events\n                and only delivers new ones. An integer ``N`` streams events\n                after sequence ``N``.\n            max_reconnect_attempts: Maximum reconnect attempts on connection\n                drop. Defaults to ``3``.\n        \"\"\"\n        queue: asyncio.Queue[_QueueItem] = asyncio.Queue()\n        stream = EventStream(queue, None, after_sequence)\n\n        async def reader() -> None:\n            incl_inter = \"true\" if include_internal_events else \"false\"\n            url = f\"/events/{handler_id}\"\n            last_sequence: int | Literal[\"now\"] = after_sequence\n            attempts = 0\n            try:\n                while True:\n                    async with self._get_client() as client:\n                        try:\n                            async with client.stream(\n                                \"GET\",\n                                url,\n                                params={\n                                    \"sse\": \"true\",\n                                    \"include_internal\": incl_inter,\n                                    \"after_sequence\": str(last_sequence),\n                                },\n                                headers={\"Connection\": \"keep-alive\"},\n                                timeout=None,\n                            ) as response:\n                                if response.status_code == 404:\n                                    raise ValueError(\"Handler not found\")\n                                elif response.status_code == 204:\n                                    await queue.put(_QueuedDone())\n                                    return\n\n                                _raise_for_status_with_body(response)\n\n                                # Reset attempts on successful connection\n                                attempts = 0\n\n                                # Parse SSE stream: \"id: N\\ndata: {...}\\n\\n\"\n                                current_id: str | None = None\n                                async for line in response.aiter_lines():\n                                    stripped = line.strip()\n                                    if not stripped:\n                                        # Empty line = end of SSE event\n                                        continue\n                                    if stripped.startswith(\"id:\"):\n                                        current_id = stripped[3:].strip()\n                                    elif stripped.startswith(\"data:\"):\n                                        data = stripped[5:].strip()\n                                        event = EventEnvelopeWithMetadata.model_validate_json(\n                                            data\n                                        )\n                                        if current_id is not None:\n                                            try:\n                                                last_sequence = int(current_id)\n                                            except ValueError:\n                                                pass\n                                        await queue.put(\n                                            _QueuedEvent(\n                                                sequence=last_sequence,\n                                                event=event,\n                                            )\n                                        )\n                                        current_id = None\n\n                            # Stream ended normally (server closed connection)\n                            await queue.put(_QueuedDone())\n                            return\n\n                        except httpx.TimeoutException:\n                            raise TimeoutError(\n                                f\"Timeout waiting for events from handler {handler_id}\"\n                            )\n                        except (httpx.RequestError, ConnectionError):\n                            attempts += 1\n                            if attempts > max_reconnect_attempts:\n                                raise ConnectionError(\n                                    f\"Failed to connect to event stream after {max_reconnect_attempts} attempts\"\n                                )\n                            # Retry from last received sequence\n            except asyncio.CancelledError:\n                await queue.put(_QueuedDone())\n            except BaseException as exc:\n                await queue.put(_QueuedError(exc))\n\n        stream._task = asyncio.create_task(reader())\n        return stream\n\n    async def send_event(\n        self,\n        handler_id: str,\n        event: Event | dict[str, Any],\n        step: str | None = None,\n    ) -> SendEventResponse:\n        \"\"\"Send an event to a running workflow.\n\n        Useful for human-in-the-loop workflows that wait for external input.\n\n        Args:\n            handler_id: ID of the handler running the workflow.\n            event: Event to send, as an ``Event`` instance or a dict.\n            step: Target a specific workflow step. When ``None``, the event\n                is broadcast to all waiting steps.\n        \"\"\"\n        try:\n            serialized_event: dict[str, Any] = _serialize_event(event)\n        except Exception as e:\n            raise ValueError(f\"Error while serializing the provided event: {e}\")\n        request_body: dict[str, Any] = {\"event\": serialized_event}\n        if step:\n            request_body[\"step\"] = step\n        async with self._get_client() as client:\n            response = await client.post(f\"/events/{handler_id}\", json=request_body)\n            _raise_for_status_with_body(response)\n\n            return SendEventResponse.model_validate(response.json())\n\n    async def get_result(self, handler_id: str) -> HandlerData:\n        \"\"\"\n        Deprecated. Use get_handler instead.\n        \"\"\"\n        return await self.get_handler(handler_id)\n\n    async def get_handlers(\n        self,\n        status: list[Status] | None = None,\n        workflow_name: list[str] | None = None,\n    ) -> HandlersListResponse:\n        \"\"\"List all workflow handlers.\n\n        Args:\n            status: Filter by handler status (e.g. ``\"running\"``,\n                ``\"completed\"``).\n            workflow_name: Filter by workflow name.\n        \"\"\"\n        async with self._get_client() as client:\n            response = await client.get(\n                \"/handlers\",\n                params={\n                    \"status\": status,\n                    \"workflow_name\": workflow_name,\n                },\n            )\n            _raise_for_status_with_body(response)\n\n            return HandlersListResponse.model_validate(response.json())\n\n    async def get_handler(self, handler_id: str) -> HandlerData:\n        \"\"\"Get a workflow handler by ID.\n\n        Returns handler metadata including status, result (if completed),\n        and timestamps.\n\n        Args:\n            handler_id: ID of the handler.\n        \"\"\"\n        async with self._get_client() as client:\n            response = await client.get(f\"/handlers/{handler_id}\")\n            _raise_for_status_with_body(response)\n\n            return HandlerData.model_validate(response.json())\n\n    async def cancel_handler(\n        self, handler_id: str, purge: bool = False\n    ) -> CancelHandlerResponse:\n        \"\"\"Cancel a running workflow.\n\n        Args:\n            handler_id: ID of the handler to cancel.\n            purge: Also remove the handler from the persistence store.\n                Defaults to ``False``.\n        \"\"\"\n        async with self._get_client() as client:\n            response = await client.post(\n                f\"/handlers/{handler_id}/cancel\",\n                params={\"purge\": \"true\" if purge else \"false\"},\n            )\n            _raise_for_status_with_body(response)\n\n            return CancelHandlerResponse.model_validate(response.json())\n\n\ndef _serialize_event(\n    event: Event | dict[str, Any], bare: bool = False\n) -> dict[str, Any]:\n    if isinstance(event, dict):\n        return event  # assumes you know what you are doing. In many cases this needs to be a dict that contains type metadata and the value\n    return (\n        event.model_dump()\n        if bare\n        else EventEnvelope.from_event(event=event).model_dump()\n    )\n"
  },
  {
    "path": "packages/llama-agents-client/src/llama_agents/client/protocol/__init__.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Literal\n\nfrom pydantic import BaseModel\nfrom workflows.representation import WorkflowGraph\n\nfrom .serializable_events import EventEnvelopeWithMetadata\n\n# Shared protocol types between client and server\n\n# Mirrors server.store Status\nStatus = Literal[\"running\", \"completed\", \"failed\", \"cancelled\"]\n\n\ndef is_status_completed(status: Status) -> bool:\n    return status in {\"completed\", \"failed\", \"cancelled\"}\n\n\nclass HandlerData(BaseModel):\n    handler_id: str\n    workflow_name: str\n    run_id: str | None\n    error: str | None\n    result: EventEnvelopeWithMetadata | None\n    status: Status\n    started_at: str\n    updated_at: str | None\n    completed_at: str | None\n\n\nclass HandlersListResponse(BaseModel):\n    handlers: list[HandlerData]\n\n\nclass HealthResponse(BaseModel):\n    status: Literal[\"healthy\", \"unhealthy\"]\n\n\nclass WorkflowsListResponse(BaseModel):\n    workflows: list[str]\n\n\nclass SendEventResponse(BaseModel):\n    status: Literal[\"sent\"]\n\n\nclass CancelHandlerResponse(BaseModel):\n    status: Literal[\"deleted\", \"cancelled\"]\n\n\nclass WorkflowSchemaResponse(BaseModel):\n    start: dict[str, Any]\n    stop: dict[str, Any]\n\n\nclass WorkflowEventsListResponse(BaseModel):\n    events: list[dict[str, Any]]\n\n\nclass WorkflowGraphResponse(BaseModel):\n    graph: WorkflowGraph\n\n\n__all__ = [\n    \"Status\",\n    \"is_status_completed\",\n    \"HandlerData\",\n    \"HandlersListResponse\",\n    \"HealthResponse\",\n    \"WorkflowsListResponse\",\n    \"SendEventResponse\",\n    \"CancelHandlerResponse\",\n    \"WorkflowSchemaResponse\",\n    \"WorkflowEventsListResponse\",\n    \"WorkflowGraphResponse\",\n]\n"
  },
  {
    "path": "packages/llama-agents-client/src/llama_agents/client/protocol/serializable_events.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport builtins\nimport json\nfrom typing import Any\n\nfrom pydantic import BaseModel, ValidationError, model_validator\nfrom workflows.context.utils import import_module_from_qualified_name\nfrom workflows.events import Event\n\n\nclass EventEnvelopeWithMetadata(BaseModel):\n    \"\"\"\n    Client readable representation of an Event. Includes class metadata in order to support\n    matching event types semantically in an extendable manner (e.g. \"StartEvent\", \"StopEvent\", etc.).\n    \"\"\"\n\n    value: dict[str, Any]\n\n    # deprecated, use type instead\n    qualified_name: str | None\n\n    # New metadata\n    type: str\n    types: list[str] | None\n\n    def load_event(self, registry: list[type[Event]] = []) -> Event:\n        \"\"\"\n        Attempts to load the event data as a python class based on the envelope metadata.\n        Looks up the event from the registry, if provided. Falls back to the qualified_name, attempting to load from the module path.\n        \"\"\"\n        registry_lookup = {e.__name__: e for e in registry}\n        as_event_envelope = EventEnvelope(\n            value=self.value, type=self.type, qualified_name=self.qualified_name\n        ).model_dump()\n        return EventEnvelope.parse(\n            client_data=as_event_envelope, registry=registry_lookup\n        )\n\n    @classmethod\n    def from_event(\n        cls, event: Event, include_qualified_name: bool = True\n    ) -> EventEnvelopeWithMetadata:\n        \"\"\"\n        Build a backward-compatible envelope for an Event, preserving existing\n        fields (e.g., qualified_name, value) while adding metadata useful for\n        type-safe clients.\n\n        \"\"\"\n        # Start with the existing JSON-serializable structure\n        value = event.model_dump(mode=\"json\")\n\n        envelope = EventEnvelopeWithMetadata(\n            value=value,\n            qualified_name=_get_qualified_name(type(event))\n            if include_qualified_name\n            else None,\n            types=_get_event_subtypes(type(event)),\n            type=type(event).__name__,\n        )\n        return envelope\n\n\nclass EventEnvelope(BaseModel):\n    \"\"\"\n    Client write representation of an Event. Simpler than the server provided EventEnvelopeWithMetadata, as the metadata can be inferred based on looking up the runtime type\n    \"\"\"\n\n    value: Any | None\n    type: str | None = None\n    qualified_name: str | None = None\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _format_compatibility(cls, data: Any) -> Any:\n        if isinstance(data, str):\n            try:\n                data = json.loads(data)\n            except json.JSONDecodeError:\n                pass\n        if isinstance(data, dict):\n            if \"value\" not in data and \"data\" in data:\n                # Preserve other keys while defaulting \"value\" from legacy \"data\"\n                data = {**data, \"value\": data[\"data\"]}\n        return data\n\n    @classmethod\n    def from_event(cls, event: Event) -> EventEnvelope:\n        return cls(\n            value=event.model_dump(mode=\"json\"),\n            type=type(event).__name__,\n        )\n\n    @classmethod\n    def parse(\n        cls,\n        client_data: dict[str, Any] | str,\n        registry: dict[str, builtins.type[Event]] | None = None,\n        explicit_event: builtins.type[Event] | None = None,\n    ) -> Event:\n        \"\"\"\n        Parse client data into an Event. Raises an EventValidationError if the client data is invalid.\n\n        Args:\n            client_data: The client data to parse. Can be a dictionary, a string, or an explicit Event class.\n            registry: The registry of event type names to Event classes.\n            explicit_event: An explicit Event class to treat the dict as\n\n        Returns:\n            The parsed Event.\n        \"\"\"\n        registry = registry or {}\n        errors: list[str] = []\n        try:\n            as_dict = (\n                json.loads(client_data) if isinstance(client_data, str) else client_data\n            )\n        except json.JSONDecodeError:\n            as_dict = client_data\n        if not isinstance(as_dict, dict):\n            raise EventValidationError(\n                \"Failed to deserialize event. Must be a json object, or stringified json object\"\n            )\n        missing_qualifiers = (\n            \"qualified_name\" not in as_dict or \"type\" not in as_dict\n        ) and \"value\" not in as_dict\n        if missing_qualifiers and explicit_event:\n            if explicit_event.__name__ not in registry:\n                registry = {**registry, explicit_event.__name__: explicit_event}\n            as_dict = {\n                \"type\": explicit_event.__name__,\n                \"value\": as_dict,\n            }\n        try:\n            event = EventEnvelope.model_validate(as_dict)\n\n            if event.type:\n                if event.type not in registry:\n                    errors.append(\n                        f\"Invalid event type: {event.type}. Expected one of {', '.join(registry.keys())}\"\n                    )\n                else:\n                    return registry[event.type].model_validate(event.value)\n            if event.qualified_name:\n                module_class = import_module_from_qualified_name(event.qualified_name)\n                if not issubclass(module_class, Event):\n                    errors.append(\n                        f\"Invalid client data. Qualified name {event.qualified_name} does not correspond to an Event subclass\"\n                    )\n                else:\n                    return module_class.model_validate(event.value)\n        except ValidationError as e:\n            errors.append(f\"Failed to deserialize event: {str(e)}\")\n        errors = (\n            errors\n            if errors\n            else [\n                \"Invalid client data. Must have a type or a qualified name, got {event}\"\n            ]\n        )\n        raise EventValidationError(\" \".join(errors))\n\n\ndef _get_event_subtypes(cls: type[Event]) -> list[str] | None:\n    \"\"\"\n    Traverses the MRO (Module Resolution Order) of a class and returns the list of only Event subclasses.\n    \"\"\"\n    names: list[str] = []\n    # Skip the class itself by starting from the second MRO entry\n    for c in cls.mro()[1:]:\n        if c is Event:\n            break\n        if issubclass(c, Event):\n            names.append(c.__name__)\n    if not names:\n        return None\n    return names\n\n\ndef _get_qualified_name(event: type[Event]) -> str:\n    return f\"{event.__module__}.{event.__name__}\"\n\n\nclass EventValidationError(Exception):\n    \"\"\"Raised when the client data is invalid.\"\"\"\n"
  },
  {
    "path": "packages/llama-agents-client/tests/client/client_test_workflows.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nimport random\n\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\nclass InputEvent(StartEvent):\n    greeting: str\n    name: str\n\n\nclass GreetEvent(Event):\n    greeting: str\n    exclamation_marks: int\n\n\nclass OutputEvent(StopEvent):\n    greeting: str\n\n\nclass GreetingWorkflow(Workflow):\n    @step\n    async def first_step(self, ev: InputEvent, ctx: Context) -> GreetEvent:\n        ctx.write_event_to_stream(ev)\n        return GreetEvent(\n            greeting=f\"{ev.greeting} {ev.name}\", exclamation_marks=random.randint(1, 10)\n        )\n\n    @step\n    async def second_step(self, ev: GreetEvent, ctx: Context) -> OutputEvent:\n        ctx.write_event_to_stream(ev)\n        return OutputEvent(greeting=f\"{ev.greeting}{'!' * ev.exclamation_marks}\")\n\n\nclass CrashingWorkflow(Workflow):\n    @step\n    async def crashing_step(self, ev: StartEvent) -> StopEvent:\n        raise ValueError(\"Workflow crashed intentionally\")\n"
  },
  {
    "path": "packages/llama-agents-client/tests/client/test_client.py",
    "content": "# ty: ignore[invalid-argument-type, not-iterable]\nfrom __future__ import annotations\n\nfrom contextlib import asynccontextmanager\nfrom typing import AsyncIterator\nfrom unittest.mock import AsyncMock\n\nimport httpx\nimport pytest\nfrom client_test_workflows import (\n    CrashingWorkflow,\n    GreetEvent,\n    GreetingWorkflow,\n    InputEvent,\n    OutputEvent,\n)\nfrom httpx import ASGITransport, AsyncClient\nfrom llama_agents.client import WorkflowClient\nfrom llama_agents.client.protocol.serializable_events import (\n    EventEnvelopeWithMetadata,\n)\nfrom llama_agents.server import MemoryWorkflowStore\nfrom llama_agents.server.server import WorkflowServer\n\n\n@pytest.fixture()\ndef server() -> WorkflowServer:\n    # Use MemoryWorkflowStore so get_handlers() can retrieve from persistence\n    ws = WorkflowServer(workflow_store=MemoryWorkflowStore())\n    ws.add_workflow(name=\"greeting\", workflow=GreetingWorkflow())\n    ws.add_workflow(name=\"crashing\", workflow=CrashingWorkflow())\n    return ws\n\n\n@pytest.fixture()\ndef client(server: WorkflowServer) -> WorkflowClient:\n    transport = ASGITransport(server.app)\n    httpx_client = AsyncClient(transport=transport, base_url=\"http://test\")\n    return WorkflowClient(httpx_client=httpx_client)\n\n\n@pytest.mark.asyncio\nasync def test_is_healthy(client: WorkflowClient) -> None:\n    is_healthy = await client.is_healthy()\n    assert is_healthy.status == \"healthy\"\n\n\n@pytest.mark.asyncio\nasync def test_list_workflows(client: WorkflowClient) -> None:\n    wfs = await client.list_workflows()\n    assert len(wfs.workflows) == 2\n    assert \"greeting\" in wfs.workflows\n    assert \"crashing\" in wfs.workflows\n\n\n@pytest.mark.asyncio\nasync def test_run_nowait_and_stream_events(client: WorkflowClient) -> None:\n    handler = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    assert handler.handler_id\n    handler_id = handler.handler_id\n\n    events = []\n    async for event in client.get_workflow_events(handler_id=handler_id):\n        assert isinstance(event, EventEnvelopeWithMetadata)\n        events.append(event.load_event())\n    assert len(events) == 3\n    assert events[0] == InputEvent(greeting=\"hello\", name=\"John\")\n\n\n@pytest.mark.asyncio\nasync def test_get_result_for_handler(client: WorkflowClient) -> None:\n    handler = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    handler_id = handler.handler_id\n    # wait for completion\n    async for event in client.get_workflow_events(handler_id=handler_id):\n        pass\n\n    result = await client.get_result(handler_id)\n    assert result.result is not None\n    res = OutputEvent.model_validate(result.result.value)\n    assert \"John\" in res.greeting and \"!\" in res.greeting and \"hello\" in res.greeting\n\n    # Result should be retrievable again and reference the same handler\n    result_again = await client.get_result(handler_id)\n    assert result_again == result\n\n\n@pytest.mark.asyncio\nasync def test_get_handler(client: WorkflowClient) -> None:\n    handler = await client.run_workflow(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    assert handler.status == \"completed\"\n    handler_id = handler.handler_id\n\n    handler_data = await client.get_handler(handler_id)\n    assert handler_data.handler_id == handler_id\n    assert handler_data.workflow_name == \"greeting\"\n    assert handler_data.run_id == handler.run_id\n    assert handler_data.status == \"completed\"\n    assert handler_data.started_at is not None\n    assert handler_data.updated_at is not None\n    assert handler_data.completed_at is not None\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers(client: WorkflowClient) -> None:\n    handler = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    handler_id = handler.handler_id\n\n    handlers = await client.get_handlers()\n    assert len(handlers.handlers) == 1\n    assert handlers.handlers[0].handler_id == handler_id\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_filter_by_workflow_name(client: WorkflowClient) -> None:\n    await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    await client.run_workflow_nowait(\"crashing\", start_event={})\n\n    handlers = await client.get_handlers(workflow_name=[\"greeting\"])\n    assert len(handlers.handlers) >= 1\n    assert all(h.workflow_name == \"greeting\" for h in handlers.handlers)\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_filter_by_status(client: WorkflowClient) -> None:\n    await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    completed_handler = await client.run_workflow(\n        \"greeting\", start_event=InputEvent(greeting=\"hi\", name=\"Jane\")\n    )\n    # Wait for the crashing workflow to fail\n    try:\n        await client.run_workflow(\n            \"crashing\", start_event=InputEvent(greeting=\"test\", name=\"test\")\n        )\n    except Exception:\n        pass\n\n    handlers = await client.get_handlers(status=[\"completed\"])\n    handler_ids = {h.handler_id for h in handlers.handlers}\n    assert completed_handler.handler_id in handler_ids\n\n    failed_handlers = await client.get_handlers(status=[\"failed\"])\n    failed_ids = {h.handler_id for h in failed_handlers.handlers}\n    assert len(failed_ids) == 1\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_for_complex_workflows(\n    client: WorkflowClient, server: WorkflowServer\n) -> None:\n    handler1 = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    handler1_id = handler1.handler_id\n\n    handlers = await client.get_handlers()\n    assert len(handlers.handlers) == 1\n    assert handlers.handlers[0].handler_id == handler1_id\n\n    handler2 = await client.run_workflow(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"Jane\")\n    )\n    handler2_id = handler2.handler_id\n\n    # Restart the server\n    await server.stop()\n    await server.start()\n\n    handlers = await client.get_handlers()\n    assert len(handlers.handlers) == 2\n    assert handlers.handlers[0].handler_id == handler1_id\n    assert handlers.handlers[1].handler_id == handler2_id\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_sync_result(client: WorkflowClient) -> None:\n    result = await client.run_workflow(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    assert result.result is not None\n    res = OutputEvent.model_validate(result.result.value)\n    assert \"John\" in res.greeting and \"!\" in res.greeting and \"hello\" in res.greeting\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_including_internal(client: WorkflowClient) -> None:\n    handler = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    handler_id = handler.handler_id\n\n    events = []\n    async for event in client.get_workflow_events(\n        handler_id=handler_id, include_internal_events=True\n    ):\n        assert isinstance(event, EventEnvelopeWithMetadata)\n        events.append(event.load_event())\n    assert len(events) > 3\n\n\n@pytest.mark.asyncio\nasync def test_cancel_handler(client: WorkflowClient) -> None:\n    handler = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    handler_id = handler.handler_id\n\n    cancel_resp = await client.cancel_handler(handler_id=handler_id)\n    assert cancel_resp.status == \"cancelled\"\n    handler = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    handler_id = handler.handler_id\n\n    cancel_resp = await client.cancel_handler(handler_id=handler_id, purge=True)\n    assert cancel_resp.status == \"deleted\"\n\n\n@pytest.mark.asyncio\nasync def test_send_event(client: WorkflowClient) -> None:\n    handler = await client.run_workflow_nowait(\n        \"greeting\", start_event=InputEvent(greeting=\"hello\", name=\"John\")\n    )\n    handler_id = handler.handler_id\n\n    # Send an event to the running workflow\n    response = await client.send_event(\n        handler_id=handler_id,\n        event=GreetEvent(greeting=\"Bonjour John\", exclamation_marks=5),\n    )\n    assert response.status == \"sent\"\n\n    # Wait for completion\n    async for event in client.get_workflow_events(handler_id=handler_id):\n        pass\n\n    # Verify workflow completed successfully\n    result = await client.get_result(handler_id)\n    assert result.result is not None\n\n\n@pytest.mark.asyncio\nasync def test_error_message_format(client: WorkflowClient) -> None:\n    \"\"\"Test that error messages include method, URL, status code, and response body preview.\"\"\"\n    with pytest.raises(httpx.HTTPStatusError) as exc_info:\n        await client.run_workflow(\n            \"nonexistent_workflow\",\n            start_event=InputEvent(greeting=\"hello\", name=\"John\"),\n        )\n\n    error_message = str(exc_info.value)\n\n    # Verify error message contains the expected components\n    assert (\n        '404 Not Found for POST http://test/workflows/nonexistent_workflow/run. Response: {\"detail\":\"Workflow not found\"}'\n        == error_message\n    )\n\n\ndef _envelope(msg: str) -> EventEnvelopeWithMetadata:\n    return EventEnvelopeWithMetadata(\n        value={\"msg\": msg}, qualified_name=None, type=\"TestEvent\", types=None\n    )\n\n\n# Each \"connection\" in a script is a list of SSE events to yield, optionally\n# ending with an exception to simulate a disconnect. A bare exception means\n# the connection fails before yielding any data.\nConnectionScript = list[tuple[int, EventEnvelopeWithMetadata] | Exception] | Exception\n\n\nclass FakeStreamClient:\n    \"\"\"Mock httpx client that replays a scripted sequence of SSE connections.\"\"\"\n\n    def __init__(self, script: list[ConnectionScript]) -> None:\n        self._script = list(script)\n        self.captured_params: list[dict[str, str]] = []\n        self._call = 0\n\n    @asynccontextmanager\n    async def stream(\n        self,\n        method: str,\n        url: str,\n        params: dict[str, str] | None = None,\n        **kwargs: object,\n    ) -> AsyncIterator[AsyncMock]:\n        self.captured_params.append(params or {})\n        assert self._call < len(self._script), \"More connections than scripted\"\n        entry = self._script[self._call]\n        self._call += 1\n\n        if isinstance(entry, Exception):\n            raise entry\n\n        events = entry\n        tail_error: Exception | None = None\n        # If the last element is an exception, pop it as a mid-stream error\n        if events and isinstance(events[-1], Exception):\n            tail_error = events[-1]  # type: ignore[assignment]\n            events = events[:-1]  # type: ignore[assignment]\n\n        resp = AsyncMock()\n        resp.status_code = 200\n\n        async def aiter_lines() -> AsyncIterator[str]:\n            for seq, env in events:  # type: ignore[union-attr]\n                yield f\"id: {seq}\"\n                yield f\"data: {env.model_dump_json()}\"\n                yield \"\"\n            if tail_error is not None:\n                raise tail_error\n\n        resp.aiter_lines = aiter_lines\n        yield resp\n\n\nasync def _collect(\n    script: list[ConnectionScript], **kwargs: object\n) -> list[EventEnvelopeWithMetadata]:\n    fake = FakeStreamClient(script)\n    wf_client = WorkflowClient(httpx_client=fake)  # type: ignore[arg-type]\n    events = [\n        e\n        async for e in wf_client.get_workflow_events(handler_id=\"h\", **kwargs)  # type: ignore[arg-type]\n    ]\n    return events\n\n\n@pytest.mark.asyncio\nasync def test_reconnect_resumes_from_last_sequence() -> None:\n    e1, e2, e3 = _envelope(\"first\"), _envelope(\"second\"), _envelope(\"third\")\n    fake = FakeStreamClient(\n        [\n            [(0, e1), httpx.RemoteProtocolError(\"reset\")],\n            [(1, e2), (2, e3)],\n        ]\n    )\n    wf_client = WorkflowClient(httpx_client=fake)  # type: ignore[arg-type]\n    events = [e async for e in wf_client.get_workflow_events(handler_id=\"h\")]\n\n    assert [e.value[\"msg\"] for e in events] == [\"first\", \"second\", \"third\"]\n    assert fake.captured_params[0][\"after_sequence\"] == \"-1\"\n    assert fake.captured_params[1][\"after_sequence\"] == \"0\"\n\n\n@pytest.mark.asyncio\nasync def test_reconnect_exceeds_max_attempts_raises() -> None:\n    with pytest.raises(ConnectionError, match=\"after 2 attempts\"):\n        await _collect(\n            [httpx.ConnectError(\"refused\")] * 3,\n            max_reconnect_attempts=2,\n        )\n\n\n@pytest.mark.asyncio\nasync def test_reconnect_resets_attempts_on_success() -> None:\n    e1, e2 = _envelope(\"a\"), _envelope(\"b\")\n    events = await _collect(\n        [\n            [(0, e1), httpx.ReadError(\"broken\")],\n            httpx.ReadError(\"broken again\"),\n            [(1, e2)],\n        ],\n        max_reconnect_attempts=2,\n    )\n    assert [e.value[\"msg\"] for e in events] == [\"a\", \"b\"]\n\n\n@pytest.mark.asyncio\nasync def test_timeout_exception_not_retried() -> None:\n    with pytest.raises(TimeoutError, match=\"Timeout\"):\n        await _collect([httpx.ReadTimeout(\"timed out\")])\n\n\n@pytest.mark.asyncio\nasync def test_get_workflow_events_tracks_last_sequence() -> None:\n    e1, e2, e3 = _envelope(\"a\"), _envelope(\"b\"), _envelope(\"c\")\n    fake = FakeStreamClient([[(0, e1), (1, e2), (2, e3)]])\n    wf_client = WorkflowClient(httpx_client=fake)  # type: ignore[arg-type]\n\n    stream = wf_client.get_workflow_events(handler_id=\"h\")\n    assert stream.last_sequence == -1\n\n    sequences: list[int | str] = []\n    async for event in stream:\n        sequences.append(stream.last_sequence)\n\n    assert sequences == [0, 1, 2]\n    assert stream.last_sequence == 2\n\n\n@pytest.mark.asyncio\nasync def test_get_workflow_events_with_now() -> None:\n    e1 = _envelope(\"a\")\n    fake = FakeStreamClient([[(5, e1)]])\n    wf_client = WorkflowClient(httpx_client=fake)  # type: ignore[arg-type]\n\n    stream = wf_client.get_workflow_events(handler_id=\"h\", after_sequence=\"now\")\n    assert stream.last_sequence == \"now\"\n\n    async for _ in stream:\n        pass\n\n    assert stream.last_sequence == 5\n    assert fake.captured_params[0][\"after_sequence\"] == \"now\"\n"
  },
  {
    "path": "packages/llama-agents-client/tests/protocol/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-client/tests/protocol/test_serializable_events.py",
    "content": "# ty: ignore[invalid-argument-type]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\nimport json\n\nimport pytest\nfrom llama_agents.client.protocol.serializable_events import (\n    EventEnvelope,\n    EventEnvelopeWithMetadata,\n    EventValidationError,\n)\nfrom workflows.events import (\n    Event,\n    StepState,\n    StepStateChanged,\n    StopEvent,\n)\n\n\ndef test_envelope_user_defined_event() -> None:\n    class MyEvent(Event):\n        x: int\n\n    ev = MyEvent(x=1)\n    env = EventEnvelopeWithMetadata.from_event(ev).model_dump()\n\n    assert isinstance(env.get(\"value\", {}), dict)\n    types = env.get(\"types\")\n    assert types is None\n    # User-defined event\n    assert env.get(\"type\", \"\") == \"MyEvent\"\n\n\ndef test_envelope_builtin_stop_event() -> None:\n    ev = StopEvent()\n    env = EventEnvelopeWithMetadata.from_event(ev).model_dump()\n\n    assert isinstance(env.get(\"value\", {}), dict)\n    types = env.get(\"types\")\n    assert types is None\n    assert env.get(\"type\", \"\") == \"StopEvent\"\n\n\ndef test_envelope_stop_event_subclass() -> None:\n    class MyStop(StopEvent):\n        pass\n\n    ev = MyStop()\n    env = EventEnvelopeWithMetadata.from_event(ev).model_dump()\n\n    assert isinstance(env.get(\"value\", {}), dict)\n    # Subclass is user-defined\n    assert env.get(\"type\", \"\") == \"MyStop\"\n    # Must include base StopEvent in MRO\n    types = env.get(\"types\")\n    assert types is not None\n    assert \"StopEvent\" in types\n\n\ndef test_envelope_internal_event() -> None:\n    ev = StepStateChanged(\n        name=\"s\",\n        step_state=StepState.PREPARING,\n        worker_id=\"w1\",\n        input_event_name=\"X\",\n    )\n    env = EventEnvelopeWithMetadata.from_event(ev).model_dump()\n\n    assert isinstance(env.get(\"value\", {}), dict)\n    assert env.get(\"type\", \"\") == \"StepStateChanged\"\n    # Internal event types contains specific class and base Event\n    types = env.get(\"types\")\n    assert types is not None\n    assert \"InternalDispatchEvent\" in types\n\n\n# Module-scope events for qualified_name import tests\nclass ModuleScopeEvent(Event):\n    x: int\n\n\nclass ModuleScopeOtherEvent(Event):\n    y: int\n\n\ndef test_parse_with_registry_type_success() -> None:\n    class MyEvent(Event):\n        x: int\n\n    payload = {\"type\": \"MyEvent\", \"value\": {\"x\": 1}}\n    ev = EventEnvelope.parse(client_data=payload, registry={\"MyEvent\": MyEvent})\n    assert isinstance(ev, MyEvent)\n    assert ev.x == 1\n\n\ndef test_parse_with_qualified_name_fallback_success() -> None:\n    qn = f\"{ModuleScopeEvent.__module__}.{ModuleScopeEvent.__name__}\"\n    payload = {\"qualified_name\": qn, \"value\": {\"x\": 7}}\n    ev = EventEnvelope.parse(client_data=payload)\n    assert isinstance(ev, ModuleScopeEvent)\n    assert ev.x == 7\n\n\ndef test_parse_with_type_unknown_but_qualified_name_valid() -> None:\n    qn = f\"{ModuleScopeOtherEvent.__module__}.{ModuleScopeOtherEvent.__name__}\"\n    payload = {\"type\": \"NotInRegistry\", \"qualified_name\": qn, \"value\": {\"y\": 3}}\n    ev = EventEnvelope.parse(client_data=payload, registry={})\n    assert isinstance(ev, ModuleScopeOtherEvent)\n    assert ev.y == 3\n\n\ndef test_parse_alias_data_to_value() -> None:\n    class MyEvent(Event):\n        x: int\n\n    payload = {\"type\": \"MyEvent\", \"data\": {\"x\": 9}}\n    ev = EventEnvelope.parse(client_data=payload, registry={\"MyEvent\": MyEvent})\n    assert isinstance(ev, MyEvent)\n    assert ev.x == 9\n\n\ndef test_parse_from_json_string() -> None:\n    class MyEvent(Event):\n        x: int\n\n    obj = {\"type\": \"MyEvent\", \"value\": {\"x\": 11}}\n    ev = EventEnvelope.parse(client_data=json.dumps(obj), registry={\"MyEvent\": MyEvent})\n    assert isinstance(ev, MyEvent)\n    assert ev.x == 11\n\n\ndef test_parse_value_only_with_explicit_event() -> None:\n    class MyStart(Event):\n        foo: str\n\n    payload = {\"foo\": \"bar\"}\n    ev = EventEnvelope.parse(client_data=payload, explicit_event=MyStart)\n    assert isinstance(ev, MyStart)\n    assert ev.foo == \"bar\"\n\n\ndef test_parse_invalid_inputs_raise() -> None:\n    with pytest.raises(EventValidationError) as e:\n        EventEnvelope.parse(client_data=123)  # type: ignore[arg-type]\n    assert \"Failed to deserialize event\" in str(e)\n\n\ndef test_from_event_roundtrip_with_registry() -> None:\n    class MyEv(Event):\n        a: int\n\n    original = MyEv(a=5)\n    env = EventEnvelope.from_event(original).model_dump()\n    parsed = EventEnvelope.parse(client_data=env, registry={\"MyEv\": MyEv})\n    assert isinstance(parsed, MyEv)\n    assert parsed.a == 5\n\n\ndef test_metadata_envelope_load_event_with_registry() -> None:\n    class MyMeta(Event):\n        z: int\n\n    ev = MyMeta(z=42)\n    env = EventEnvelopeWithMetadata.from_event(ev)\n    loaded = env.load_event([MyMeta])\n    assert isinstance(loaded, MyMeta)\n    assert loaded.z == 42\n\n\ndef test_metadata_envelope_qualified_name_toggle() -> None:\n    class MyMetaQ(Event):\n        q: int\n\n    ev = MyMetaQ(q=1)\n    with_qn = EventEnvelopeWithMetadata.from_event(ev, include_qualified_name=True)\n    assert with_qn.qualified_name is not None\n\n    without_qn = EventEnvelopeWithMetadata.from_event(ev, include_qualified_name=False)\n    assert without_qn.qualified_name is None\n\n\ndef test_json_serializer_back_compat_with_pydantic_flag() -> None:\n    qn = f\"{ModuleScopeEvent.__module__}.{ModuleScopeEvent.__name__}\"\n    payload = {\n        \"__is_pydantic\": True,  # ignored if present\n        \"qualified_name\": qn,\n        \"value\": {\"x\": 123},\n    }\n    ev = EventEnvelope.parse(client_data=payload)\n    assert isinstance(ev, ModuleScopeEvent)\n    assert ev.x == 123\n\n\ndef test_missing_type_and_qualified_name_raises() -> None:\n    with pytest.raises(EventValidationError) as e:\n        EventEnvelope.parse(client_data={\"x\": 1})\n    assert \"Failed to deserialize event\" in str(e)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/CHANGELOG.md",
    "content": "# llama-agents-control-plane\n\n## 0.12.2\n\n### Patch Changes\n\n- c3fac21: Validate `appserver_version` as a public PEP 440 version\n- Updated dependencies [c3fac21]\n  - llama-agents-core@0.10.2\n\n## 0.12.1\n\n### Patch Changes\n\n- 463c79d: Add `follow=false` query param on `GET /deployments/{id}/logs` so clients can fetch currently-available logs and exit. The default stays `follow=true`; existing streaming consumers are unchanged.\n- Updated dependencies [463c79d]\n  - llama-agents-core@0.10.1\n\n## 0.12.0\n\n### Minor Changes\n\n- 2280e04: Rename deployment field `llama_deploy_version` to `appserver_version`. The old name remains as a deprecated input/output alias so existing clients and servers keep working.\n\n### Patch Changes\n\n- Updated dependencies [2280e04]\n  - llama-agents-core@0.10.0\n\n## 0.11.1\n\n### Patch Changes\n\n- 64579a9: Add `controlPlane.objectStorage.s3.unsigned` (Helm) / `S3_UNSIGNED` (env) toggle to send S3 requests unsigned, enabling authless S3-compatible backends (s3proxy, LocalStack, public-read buckets) without placeholder credentials. When enabled, applies to all S3 uses — builds, backups, and code-repo storage.\n\n## 0.11.0\n\n### Minor Changes\n\n- e8b8f47: feat: add support for organizations\n\n### Patch Changes\n\n- Updated dependencies [e8b8f47]\n  - llama-agents-core@0.9.0\n\n## 0.10.5\n\n### Patch Changes\n\n- 7ad3049: Reduce full clones from github for config, repo validation, and sha discovery. Reduce dependencies on system git, preferring dulwich\n- Updated dependencies [7ad3049]\n  - llama-agents-core@0.8.5\n\n## 0.10.4\n\n### Patch Changes\n\n- 740ee9e: Add a grace window to build artifact GC (configurable via `BUILD_ARTIFACT_GC_GRACE_SECONDS`, default 75m) and parallelize its delete loop with bounded concurrency. `llamactl auth`'s non-idempotent key-creation POST now only retries on connect-phase errors (`ConnectError`, `ConnectTimeout`, `PoolTimeout`) so initial-connectivity blips are absorbed without risking duplicate keys from a read-timeout retry.\n\n## 0.10.3\n\n### Patch Changes\n\n- Updated dependencies [f27d98f]\n  - llama-agents-core@0.8.4\n\n## 0.10.2\n\n### Patch Changes\n\n- 3f12660: Add SSRF protection to git URL validation, blocking private/internal IP addresses\n- Updated dependencies [3f12660]\n  - llama-agents-core@0.8.3\n\n## 0.10.1\n\n### Patch Changes\n\n- 46f2675: security patches\n- Updated dependencies [46f2675]\n  - llama-agents-core@0.8.2\n\n## 0.10.0\n\n### Minor Changes\n\n- 58e7942: Rename Docker image repos to per-component names (llama-agents-<component>) with plain version tags\n\n### Patch Changes\n\n- Updated dependencies [58e7942]\n  - llama-agents-core@0.8.1\n\n## 0.9.0\n\n### Minor Changes\n\n- e2f3abd: Rename deployment name to display_name, add optional explicit id on create\n\n### Patch Changes\n\n- Updated dependencies [e2f3abd]\n  - llama-agents-core@0.8.0\n\n## 0.8.0\n\n## 0.7.2\n\n### Patch Changes\n\n- e345a9b: Remove chunked encoding header to prevent double decoding\n\n## 0.7.1\n\n### Patch Changes\n\n- Updated dependencies [7bb9a90]\n  - llama-agents-core@0.7.0\n\n## 0.7.0\n\n### Minor Changes\n\n- 9641415: Add dulwich-based git serving for internal repos. Users can push code via `llamactl push` and build pods clone via the build API. Bare repos are stored as tarballs in S3.\n\n## 0.6.5\n\n## 0.6.4\n\n## 0.6.3\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [508b5da]\n  - llama-agents-core@0.6.2\n\n## 0.6.1\n\n### Patch Changes\n\n- a064cc6: Fix duplicate uvicorn logs by preventing propagation to root logger\n- Updated dependencies [1b86f90]\n  - llama-agents-core@0.6.1\n\n## 0.6.0\n\n### Minor Changes\n\n- 4ab011f: Rename packages from llama-deploy to llama-agents.\n\n### Patch Changes\n\n- Updated dependencies [4ab011f]\n  - llama-agents-core@0.6.0\n\n## 0.5.3\n\n## 0.5.2\n\n### Patch Changes\n\n- e11ad55: Fix version ranges\n\n## 0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- ac74af4: Run build separately as a 1x time process per deployment update. Build stored in s3. Allows for fast unsuspend, and better future support for replication\n\n### Patch Changes\n\n- Updated dependencies [ac74af4]\n  - llama-deploy-core@0.5.0\n"
  },
  {
    "path": "packages/llama-agents-control-plane/README.md",
    "content": "# llama-agents-control-plane\n\nControl plane API for managing LlamaAgents deployments and projects.\n\nFor an end-to-end introduction, see [Getting started with LlamaAgents](https://developers.llamaindex.ai/python/cloud/llamaagents/getting-started).\n"
  },
  {
    "path": "packages/llama-agents-control-plane/package.json",
    "content": "{\n  \"name\": \"llama-agents-control-plane\",\n  \"version\": \"0.12.2\",\n  \"publish\": {\n    \"pypi\": false\n  },\n  \"dependencies\": {\n    \"llama-agents-core\": \"workspace:*\"\n  },\n  \"docker\": {\n    \"dockerfile\": \"docker/Dockerfile\",\n    \"target\": \"controlplane\",\n    \"imageName\": \"llamaindex/llama-agents-control-plane\",\n    \"platforms\": [\n      \"linux/amd64\",\n      \"linux/arm64\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/llama-agents-control-plane/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.7.20,<0.8.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"kubernetes-stubs>=22.6.0.post1\",\n  \"pytest>=8.4.1\",\n  \"pytest-asyncio>=0.25.3\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"respx>=0.22.0\",\n  \"ty>=0.0.15\",\n  \"ruff>=0.12.9\",\n  \"moto[s3]>=5.0.0\",\n  \"aiomoto>=0.3.0\",\n  \"types-aioboto3>=15.5.0\",\n  \"types-aiobotocore-s3>=3.2.0\",\n  \"boto3-stubs[s3]>=1.42.61\"\n]\n\n[project]\nname = \"llama-agents-control-plane\"\nversion = \"0.12.2\"\ndescription = \"API to manage LlamaDeployment resources\"\nreadme = \"README.md\"\nauthors = [\n  {name = \"Adrian Lyjak\", email = \"adrianlyjak@gmail.com\"}\n]\nrequires-python = \">=3.10, <4\"\ndependencies = [\n  \"fastapi[standard]>=0.115.6,<0.116\",\n  \"llama-agents-core[server]>=0.5.0\",\n  \"prometheus-client>=0.21.1,<0.22\",\n  \"prometheus-fastapi-instrumentator>=7.1.0\",\n  \"kubernetes>=33.1.0\",\n  \"gitpython>=3.1.40,<4\",\n  \"pyjwt[crypto]>=2.10.1\",\n  \"python-dotenv>=1.1.1\",\n  \"aiocache>=0.12.3\",\n  \"pydantic-settings>=2.10.1\",\n  \"python-json-logger>=3.3.0\",\n  \"typing-extensions>=4.15.0 ; python_full_version < '3.11'\",\n  \"cryptography>=44.0.0\",\n  \"aioboto3>=13.0.0\",\n  \"pyyaml>=6.0\",\n  \"dulwich[https]>=0.22.0\",\n  \"a2wsgi>=1.10.0\"\n]\n\n[project.scripts]\nllama-agents-control-plane = \"llama_agents.control_plane:main\"\nllama-deploy-control-plane = \"llama_agents.control_plane:main\"\n\n[tool.uv.build-backend]\nmodule-name = [\"llama_agents.control_plane\", \"llama_deploy.control_plane\"]\nnamespace = true\n\n[tool.uv.sources]\nllama-agents-core = {workspace = true}\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/__init__.py",
    "content": "from .main import main\n\n__all__ = [\"main\"]\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/backup/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/backup/archive.py",
    "content": "\"\"\"Backup archive format: create and read .tar.gz archives of LlamaDeployment CRs and secrets.\"\"\"\n\nfrom __future__ import annotations\n\nimport io\nimport json\nimport tarfile\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nimport yaml\n\nfrom .encryption import decrypt, encrypt\n\n# Metadata keys to preserve when cleaning resources for backup.\n# Everything else (resourceVersion, uid, creationTimestamp, generation,\n# managedFields, selfLink, deletionTimestamp, etc.) is dropped automatically.\n_CRD_METADATA_KEEP = {\"name\", \"namespace\", \"labels\", \"annotations\"}\n_SECRET_METADATA_KEEP = {\"name\", \"namespace\", \"labels\", \"annotations\", \"finalizers\"}\n\n# Annotation prefixes added by the cluster or operator — stripped even though\n# we keep the annotations dict overall.\n_SYSTEM_ANNOTATION_PREFIXES = (\n    \"kubectl.kubernetes.io/\",\n    \"deploy.llamaindex.ai/\",\n)\n\n\n@dataclass\nclass BackupManifest:\n    version: int\n    timestamp: str\n    namespace: str\n    deployment_count: int\n    encrypted: bool\n\n\n@dataclass\nclass BackupEntry:\n    name: str\n    cr: dict[str, Any]\n    secret: dict[str, str] | None = None\n    generation: int | None = None\n\n\n@dataclass\nclass BackupContents:\n    manifest: BackupManifest\n    entries: list[BackupEntry] = field(default_factory=list)\n\n\ndef clean_crd_metadata(doc: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Remove cluster-specific metadata from a CRD dict.\n\n    Uses an allowlist of metadata keys to keep — any new system-added fields\n    are automatically excluded without updating a blocklist.\n    \"\"\"\n    return _clean_metadata(doc, keep_keys=_CRD_METADATA_KEEP)\n\n\ndef clean_secret_metadata(doc: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Remove cluster-specific metadata from a Secret dict.\"\"\"\n    return _clean_metadata(doc, keep_keys=_SECRET_METADATA_KEEP)\n\n\ndef _clean_metadata(\n    doc: dict[str, Any],\n    *,\n    keep_keys: set[str],\n) -> dict[str, Any]:\n    \"\"\"Strip cluster-specific metadata, keeping only allowlisted keys.\n\n    This is safer than a blocklist because new system-added metadata fields\n    (e.g. a future ownerReferences or managedFields variant) are automatically\n    excluded without code changes.\n    \"\"\"\n    doc.pop(\"status\", None)\n\n    meta = doc.get(\"metadata\", {})\n    for key in list(meta):\n        if key not in keep_keys:\n            del meta[key]\n\n    # Strip system annotations even though we keep the annotations dict\n    annotations = meta.get(\"annotations\", {})\n    for key in list(annotations):\n        if any(key.startswith(p) for p in _SYSTEM_ANNOTATION_PREFIXES):\n            del annotations[key]\n    if not annotations:\n        meta.pop(\"annotations\", None)\n\n    return doc\n\n\ndef create_backup_archive(\n    deployments: list[dict[str, Any]],\n    secrets: dict[str, dict[str, str]],\n    namespace: str,\n    timestamp: str,\n    encryption_password: str | None = None,\n    generations: dict[str, int] | None = None,\n) -> bytes:\n    \"\"\"Create a .tar.gz backup archive in memory.\n\n    Args:\n        deployments: List of cleaned CRD dicts.\n        secrets: Map of deployment name to decoded secret key-value data.\n        namespace: K8s namespace the backup was taken from.\n        timestamp: ISO-8601 timestamp string.\n        encryption_password: If set, encrypt secret data with this password.\n\n    Returns:\n        Bytes of the .tar.gz archive.\n    \"\"\"\n    buf = io.BytesIO()\n\n    with tarfile.open(fileobj=buf, mode=\"w:gz\") as tar:\n        # Write manifest\n        manifest = {\n            \"version\": 1,\n            \"timestamp\": timestamp,\n            \"namespace\": namespace,\n            \"deployment_count\": len(deployments),\n            \"encrypted\": encryption_password is not None,\n        }\n        _add_bytes_to_tar(tar, \"manifest.json\", json.dumps(manifest, indent=2).encode())\n\n        for cr in deployments:\n            name = cr.get(\"metadata\", {}).get(\"name\", \"unknown\")\n\n            # Write cleaned CR as YAML\n            cr_yaml = yaml.dump(cr, default_flow_style=False).encode()\n            _add_bytes_to_tar(tar, f\"{name}.yaml\", cr_yaml)\n\n            # Write secret data if available\n            secret_data = secrets.get(name)\n            if secret_data is not None:\n                secret_yaml = yaml.dump(secret_data, default_flow_style=False).encode()\n                if encryption_password:\n                    encrypted = encrypt(secret_yaml, encryption_password)\n                    _add_bytes_to_tar(tar, f\"{name}.secret.enc\", encrypted)\n                else:\n                    _add_bytes_to_tar(tar, f\"{name}.secret.yaml\", secret_yaml)\n\n            # Write generation metadata if available\n            if generations and name in generations:\n                meta_json = json.dumps({\"generation\": generations[name]}).encode()\n                _add_bytes_to_tar(tar, f\"{name}.meta.json\", meta_json)\n\n    return buf.getvalue()\n\n\ndef read_backup_archive(\n    data: bytes,\n    encryption_password: str | None = None,\n) -> BackupContents:\n    \"\"\"Read and parse a .tar.gz backup archive.\n\n    Args:\n        data: Raw bytes of the archive.\n        encryption_password: Password for decrypting secret files.\n\n    Returns:\n        BackupContents with manifest and entries.\n\n    Raises:\n        ValueError: If manifest is missing or version is unsupported.\n        cryptography.exceptions.InvalidTag: If decryption fails.\n    \"\"\"\n    buf = io.BytesIO(data)\n    cr_files: dict[str, dict[str, Any]] = {}\n    secret_files: dict[str, dict[str, str]] = {}\n    meta_files: dict[str, dict[str, Any]] = {}\n    manifest_data: dict[str, Any] | None = None\n\n    with tarfile.open(fileobj=buf, mode=\"r:gz\") as tar:\n        for member in tar.getmembers():\n            if not member.isfile():\n                continue\n            f = tar.extractfile(member)\n            if f is None:\n                continue\n            content = f.read()\n            name = member.name\n\n            if name == \"manifest.json\":\n                manifest_data = json.loads(content)\n            elif name.endswith(\".secret.enc\"):\n                deploy_name = name.removesuffix(\".secret.enc\")\n                if encryption_password is None:\n                    raise ValueError(\n                        f\"Archive contains encrypted secrets but no password provided \"\n                        f\"(file: {name})\"\n                    )\n                decrypted = decrypt(content, encryption_password)\n                secret_files[deploy_name] = yaml.safe_load(decrypted)\n            elif name.endswith(\".meta.json\"):\n                deploy_name = name.removesuffix(\".meta.json\")\n                meta_files[deploy_name] = json.loads(content)\n            elif name.endswith(\".secret.yaml\"):\n                deploy_name = name.removesuffix(\".secret.yaml\")\n                secret_files[deploy_name] = yaml.safe_load(content)\n            elif name.endswith(\".yaml\"):\n                deploy_name = name.removesuffix(\".yaml\")\n                cr_files[deploy_name] = yaml.safe_load(content)\n\n    if manifest_data is None:\n        raise ValueError(\"Archive missing manifest.json\")\n\n    if manifest_data.get(\"version\", 0) != 1:\n        raise ValueError(f\"Unsupported archive version: {manifest_data.get('version')}\")\n\n    manifest = BackupManifest(\n        version=manifest_data[\"version\"],\n        timestamp=manifest_data[\"timestamp\"],\n        namespace=manifest_data[\"namespace\"],\n        deployment_count=manifest_data[\"deployment_count\"],\n        encrypted=manifest_data[\"encrypted\"],\n    )\n\n    entries = []\n    for name, cr in cr_files.items():\n        meta = meta_files.get(name, {})\n        entries.append(\n            BackupEntry(\n                name=name,\n                cr=cr,\n                secret=secret_files.get(name),\n                generation=meta.get(\"generation\"),\n            )\n        )\n\n    return BackupContents(manifest=manifest, entries=entries)\n\n\ndef _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:\n    \"\"\"Add a bytes buffer as a file to a tar archive.\"\"\"\n    info = tarfile.TarInfo(name=name)\n    info.size = len(data)\n    tar.addfile(info, io.BytesIO(data))\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/backup/encryption.py",
    "content": "\"\"\"AES-256-GCM encryption/decryption with PBKDF2 key derivation.\n\nWire format: [16-byte salt][12-byte nonce][ciphertext + 16-byte GCM auth tag]\n\"\"\"\n\nimport os\n\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.ciphers.aead import AESGCM\nfrom cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC\n\nSALT_LENGTH = 16\nNONCE_LENGTH = 12\nKEY_LENGTH = 32  # AES-256\nPBKDF2_ITERATIONS = 600_000\n\n\ndef _derive_key(password: str, salt: bytes) -> bytes:\n    \"\"\"Derive a 256-bit key from a password and salt using PBKDF2-SHA256.\"\"\"\n    kdf = PBKDF2HMAC(\n        algorithm=hashes.SHA256(),\n        length=KEY_LENGTH,\n        salt=salt,\n        iterations=PBKDF2_ITERATIONS,\n    )\n    return kdf.derive(password.encode(\"utf-8\"))\n\n\ndef encrypt(plaintext: bytes, password: str) -> bytes:\n    \"\"\"Encrypt plaintext with AES-256-GCM using a password.\n\n    Returns wire format: [16-byte salt][12-byte nonce][ciphertext + 16-byte GCM tag]\n    \"\"\"\n    salt = os.urandom(SALT_LENGTH)\n    nonce = os.urandom(NONCE_LENGTH)\n    key = _derive_key(password, salt)\n    aesgcm = AESGCM(key)\n    ciphertext = aesgcm.encrypt(nonce, plaintext, None)\n    return salt + nonce + ciphertext\n\n\ndef decrypt(data: bytes, password: str) -> bytes:\n    \"\"\"Decrypt AES-256-GCM encrypted data using a password.\n\n    Expects wire format: [16-byte salt][12-byte nonce][ciphertext + 16-byte GCM tag]\n\n    Raises:\n        cryptography.exceptions.InvalidTag: if password is wrong or data is tampered.\n        ValueError: if data is too short to contain the header.\n    \"\"\"\n    min_length = SALT_LENGTH + NONCE_LENGTH + 16  # at least the auth tag\n    if len(data) < min_length:\n        raise ValueError(\n            f\"Encrypted data too short: {len(data)} bytes, minimum {min_length}\"\n        )\n    salt = data[:SALT_LENGTH]\n    nonce = data[SALT_LENGTH : SALT_LENGTH + NONCE_LENGTH]\n    ciphertext = data[SALT_LENGTH + NONCE_LENGTH :]\n    key = _derive_key(password, salt)\n    aesgcm = AESGCM(key)\n    return aesgcm.decrypt(nonce, ciphertext, None)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/backup/storage.py",
    "content": "\"\"\"S3-compatible backup storage backend.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\n\nfrom botocore.exceptions import ClientError\nfrom llama_agents.control_plane.storage import S3ObjectStorage\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass BackupInfo:\n    \"\"\"Metadata about a backup in S3.\"\"\"\n\n    backup_id: str\n    timestamp: datetime\n    size_bytes: int\n\n\nclass S3BackupStorage(S3ObjectStorage):\n    \"\"\"Upload, download, list, and delete backup archives in S3-compatible storage.\"\"\"\n\n    def __init__(\n        self,\n        bucket: str,\n        endpoint_url: str | None = None,\n        region: str | None = None,\n        access_key: str | None = None,\n        secret_key: str | None = None,\n        key_prefix: str = \"backups\",\n        unsigned: bool = False,\n    ) -> None:\n        super().__init__(\n            bucket=bucket,\n            endpoint_url=endpoint_url,\n            region=region,\n            access_key=access_key,\n            secret_key=secret_key,\n            key_prefix=key_prefix,\n            unsigned=unsigned,\n        )\n\n    def _key(self, backup_id: str) -> str:\n        return f\"{self._key_prefix}/{backup_id}.tar.gz\"\n\n    async def upload(self, backup_id: str, data: bytes) -> None:\n        \"\"\"Upload a backup archive to S3.\"\"\"\n        async with self._client() as client:\n            await client.put_object(\n                Bucket=self._bucket,\n                Key=self._key(backup_id),\n                Body=data,\n            )\n\n    async def download(self, backup_id: str) -> bytes:\n        \"\"\"Download a backup archive from S3.\"\"\"\n        async with self._client() as client:\n            response = await client.get_object(\n                Bucket=self._bucket,\n                Key=self._key(backup_id),\n            )\n            return await response[\"Body\"].read()\n\n    async def list_backups(self) -> list[BackupInfo]:\n        \"\"\"List all backups in S3, sorted by timestamp descending.\"\"\"\n        async with self._client() as client:\n            paginator = client.get_paginator(\"list_objects_v2\")\n            backups: list[BackupInfo] = []\n            async for page in paginator.paginate(\n                Bucket=self._bucket, Prefix=f\"{self._key_prefix}/\"\n            ):\n                for obj in page.get(\"Contents\", []):\n                    key = obj.get(\"Key\")\n                    if key is None or not key.endswith(\".tar.gz\"):\n                        continue\n                    last_modified = obj.get(\"LastModified\")\n                    size = obj.get(\"Size\")\n                    if last_modified is None or size is None:\n                        continue\n                    backup_id = key.removeprefix(f\"{self._key_prefix}/\").removesuffix(\n                        \".tar.gz\"\n                    )\n                    backups.append(\n                        BackupInfo(\n                            backup_id=backup_id,\n                            timestamp=last_modified,\n                            size_bytes=size,\n                        )\n                    )\n            backups.sort(key=lambda b: b.timestamp, reverse=True)\n            return backups\n\n    async def delete(self, backup_id: str) -> None:\n        \"\"\"Delete a backup from S3.\"\"\"\n        async with self._client() as client:\n            await client.delete_object(\n                Bucket=self._bucket,\n                Key=self._key(backup_id),\n            )\n\n    async def get_info(self, backup_id: str) -> BackupInfo | None:\n        \"\"\"Get metadata for a single backup, or None if not found.\"\"\"\n        async with self._client() as client:\n            try:\n                resp = await client.head_object(\n                    Bucket=self._bucket,\n                    Key=self._key(backup_id),\n                )\n                return BackupInfo(\n                    backup_id=backup_id,\n                    timestamp=resp[\"LastModified\"],\n                    size_bytes=resp[\"ContentLength\"],\n                )\n            except ClientError as e:\n                if e.response.get(\"Error\", {}).get(\"Code\") == \"404\":\n                    return None\n                raise\n\n\ndef generate_backup_id() -> str:\n    \"\"\"Generate a human-readable, sortable backup ID.\"\"\"\n    return f\"backup-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}\"\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/build_api/build_app.py",
    "content": "import logging\nimport re\nimport tempfile\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Annotated\nfrom urllib.parse import urlparse\n\nimport httpx\nfrom fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Request, Response\nfrom fastapi.responses import StreamingResponse\nfrom llama_agents.control_plane import k8s_client\nfrom llama_agents.control_plane.build_api.build_auth import (\n    authenticate_deployment,\n    authenticate_deployment_basic,\n)\nfrom llama_agents.control_plane.build_api.build_gc import gc_build_artifacts\nfrom llama_agents.control_plane.build_api.build_service import build_artifact_storage\nfrom llama_agents.control_plane.code_repo.git_server import handle_git_request_readonly\nfrom llama_agents.control_plane.code_repo.service import code_repo_storage\nfrom llama_agents.control_plane.git import git_service\nfrom llama_agents.control_plane.git._git_service import (\n    GitHubAppAccess,\n    GitRepository,\n    InaccessibleRepository,\n)\nfrom llama_agents.core.git.git_util import GitAccessError, _check_hostname_not_private\nfrom llama_agents.core.schema.deployments import (\n    INTERNAL_CODE_REPO_SCHEME,\n    LlamaDeploymentCRD,\n)\nfrom prometheus_fastapi_instrumentator import Instrumentator\nfrom pydantic import BaseModel\n\nlogger = logging.getLogger(__name__)\n\n\n@asynccontextmanager\nasync def _lifespan(app: FastAPI) -> AsyncIterator[None]:\n    if build_artifact_storage is None:\n        logger.warning(\n            \"S3_BUCKET is not set — build artifact storage is disabled, /health will report unhealthy\"\n        )\n    else:\n        logger.info(\"Build artifact storage configured\")\n    yield\n\n\n# Build API app\nbuild_app = FastAPI(title=\"LlamaDeploy Build API\", lifespan=_lifespan)\nInstrumentator().instrument(build_app).expose(build_app, include_in_schema=False)\n\n\nclass HelloResponse(BaseModel):\n    message: str\n    deployment_id: str\n    timestamp: str\n\n\n# temp for validating connectivity\n@build_app.get(\"/deployments/{deployment_id}/hello\", response_model=HelloResponse)\nasync def hello(\n    deployment: Annotated[LlamaDeploymentCRD, Depends(authenticate_deployment)],\n) -> HelloResponse:\n    \"\"\"Hello endpoint with token authentication - proof of concept\"\"\"\n    return HelloResponse(\n        message=f\"Hello from deployment {deployment.metadata.name}!\",\n        deployment_id=deployment.metadata.name,\n        timestamp=datetime.now().isoformat(),\n    )\n\n\n@build_app.get(\"/health\")\nasync def health() -> Response:\n    \"\"\"Health check endpoint — reports 503 when S3 is not configured.\"\"\"\n    if build_artifact_storage is None:\n        return Response(\n            content='{\"status\": \"unhealthy\", \"reason\": \"Build artifact storage not configured (S3_BUCKET not set)\"}',\n            status_code=503,\n            media_type=\"application/json\",\n        )\n    return Response(\n        content='{\"status\": \"ok\", \"service\": \"build-api\"}',\n        status_code=200,\n        media_type=\"application/json\",\n    )\n\n\n# Build Artifact Endpoints\n# ========================\n\n\n@build_app.head(\"/deployments/{deployment_id}/builds/{build_id}\")\nasync def artifact_exists(\n    deployment: Annotated[LlamaDeploymentCRD, Depends(authenticate_deployment)],\n    build_id: str,\n) -> Response:\n    \"\"\"Check if a build artifact exists.\"\"\"\n    if build_artifact_storage is None:\n        raise HTTPException(\n            status_code=503, detail=\"Build artifact storage not configured\"\n        )\n    exists = await build_artifact_storage.artifact_exists(\n        deployment.metadata.name, build_id\n    )\n    if not exists:\n        raise HTTPException(status_code=404, detail=\"Artifact not found\")\n    return Response(status_code=200)\n\n\n@build_app.get(\"/deployments/{deployment_id}/builds/{build_id}\")\nasync def download_artifact(\n    deployment: Annotated[LlamaDeploymentCRD, Depends(authenticate_deployment)],\n    build_id: str,\n) -> StreamingResponse:\n    \"\"\"Download a build artifact (streamed from S3).\"\"\"\n    if build_artifact_storage is None:\n        raise HTTPException(\n            status_code=503, detail=\"Build artifact storage not configured\"\n        )\n    try:\n        (\n            content_length,\n            stream,\n        ) = await build_artifact_storage.download_artifact_streaming(\n            deployment.metadata.name, build_id\n        )\n    except build_artifact_storage.NotFoundError:\n        raise HTTPException(status_code=404, detail=\"Artifact not found\")\n    return StreamingResponse(\n        stream,\n        media_type=\"application/gzip\",\n        headers={\n            \"Content-Disposition\": f\"attachment; filename={build_id}.tar.gz\",\n            \"Content-Length\": str(content_length),\n        },\n    )\n\n\n@build_app.put(\"/deployments/{deployment_id}/builds/{build_id}\")\nasync def upload_artifact(\n    request: Request,\n    deployment: Annotated[LlamaDeploymentCRD, Depends(authenticate_deployment)],\n    build_id: str,\n    background_tasks: BackgroundTasks,\n) -> dict[str, str]:\n    \"\"\"Upload a build artifact (streamed via temp file to S3).\"\"\"\n    if build_artifact_storage is None:\n        raise HTTPException(\n            status_code=503, detail=\"Build artifact storage not configured\"\n        )\n    # Stream request body to a temp file to avoid buffering the full artifact\n    # in memory, then upload the temp file to S3.\n    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=\".tar.gz\")\n    try:\n        size = 0\n        async for chunk in request.stream():\n            size += len(chunk)\n            tmp.write(chunk)\n        tmp.close()\n\n        with open(tmp.name, \"rb\") as f:\n            await build_artifact_storage.upload_artifact_fileobj(\n                deployment.metadata.name, build_id, f\n            )\n    finally:\n        Path(tmp.name).unlink(missing_ok=True)\n\n    logger.info(\n        \"Artifact uploaded deployment=%s build_id=%s size=%d\",\n        deployment.metadata.name,\n        build_id,\n        size,\n    )\n    # GC old artifacts in the background. Retain the just-uploaded build_id to\n    # avoid a race where it's GC'd before the operator creates the new ReplicaSet.\n    background_tasks.add_task(\n        gc_build_artifacts, deployment.metadata.name, keep_build_ids={build_id}\n    )\n    return {\"status\": \"uploaded\", \"build_id\": build_id}\n\n\n# Git HTTP Protocol Endpoints\n# ==========================\n\n\n@build_app.get(\"/deployments/{deployment_id}/{git_path:path}\")\nasync def git_proxy_get(\n    request: Request,\n    deployment: Annotated[LlamaDeploymentCRD, Depends(authenticate_deployment_basic)],\n    git_path: str,\n) -> Response:\n    \"\"\"\n    Proxy all Git HTTP GET requests.\n\n    Handles all Git HTTP protocol GET operations including:\n    - info/refs (reference discovery)\n    - HEAD (default branch)\n    - objects/* (loose objects, pack files, pack indexes)\n    - refs/* (individual reference files)\n    \"\"\"\n    return await proxy_git_request(request, deployment, git_path)\n\n\n@build_app.post(\"/deployments/{deployment_id}/{git_path:path}\")\nasync def git_proxy_post(\n    request: Request,\n    deployment: Annotated[LlamaDeploymentCRD, Depends(authenticate_deployment_basic)],\n    git_path: str,\n) -> Response:\n    \"\"\"\n    Proxy all Git HTTP POST requests.\n\n    Handles all Git HTTP protocol POST operations including:\n    - git-upload-pack (fetch/clone operations)\n    - git-receive-pack (push operations)\n    \"\"\"\n    return await proxy_git_request(request, deployment, git_path)\n\n\n# Allowed git HTTP protocol paths\n_GIT_PATH_PATTERN = re.compile(\n    r\"^(\"\n    r\"info/refs\"\n    r\"|HEAD\"\n    r\"|objects/.+\"\n    r\"|git-upload-pack\"\n    r\"|git-receive-pack\"\n    r\")$\"\n)\n\n\ndef _validate_git_path(git_path: str) -> None:\n    \"\"\"Validate that git_path matches expected git protocol paths.\"\"\"\n    normalized = git_path.strip(\"/\")\n    if not _GIT_PATH_PATTERN.match(normalized):\n        raise HTTPException(\n            status_code=400,\n            detail=f\"Invalid git path: {git_path}\",\n        )\n\n\ndef _validate_url_not_private(url: str) -> None:\n    \"\"\"Resolve URL hostname and block private/internal IP addresses (SSRF protection).\n\n    Must be called at proxy time (not creation time) to prevent DNS rebinding.\n    \"\"\"\n    parsed = urlparse(url)\n    hostname = parsed.hostname\n    if not hostname:\n        raise HTTPException(status_code=400, detail=\"Invalid URL: no hostname\")\n\n    try:\n        _check_hostname_not_private(hostname)\n    except GitAccessError as e:\n        status = 403 if \"private network\" in e.message else 400\n        raise HTTPException(status_code=status, detail=e.message) from e\n\n\nasync def proxy_git_request(\n    request: Request, deployment: LlamaDeploymentCRD, git_path: str\n) -> Response:\n    \"\"\"Proxy Git requests.\n\n    Internal repos (repoUrl == INTERNAL_CODE_REPO_SCHEME) are served directly\n    via dulwich from S3. External repos are proxied to the upstream URL.\n    \"\"\"\n    # Route internal repos to dulwich\n    if deployment.spec.repoUrl == INTERNAL_CODE_REPO_SCHEME:\n        if code_repo_storage is None:\n            raise HTTPException(\n                status_code=503,\n                detail=\"Code repo storage not configured (S3_BUCKET not set).\",\n            )\n        return await handle_git_request_readonly(\n            request=request,\n            deployment_id=deployment.metadata.name,\n            git_path=git_path,\n            storage=code_repo_storage,\n        )\n\n    _validate_git_path(git_path)\n    _validate_url_not_private(deployment.spec.repoUrl)\n\n    existing_pat = await k8s_client.get_deployment_pat(deployment.metadata.name)\n    auth, access = await git_service.obtain_basic_auth_token(\n        deployment.spec.repoUrl, deployment.metadata.name, pat=existing_pat\n    )\n    if isinstance(access, InaccessibleRepository):\n        logger.warning(\n            \"Git access denied for deployment=%s repo=%s: %s\",\n            deployment.metadata.name,\n            deployment.spec.repoUrl,\n            access.message,\n        )\n        raise HTTPException(status_code=403, detail=access.message)\n\n    # Log which auth method was resolved for this proxy request\n    access_label = _describe_access(access)\n    logger.info(\n        \"Git proxy deployment=%s repo=%s auth=%s path=%s\",\n        deployment.metadata.name,\n        deployment.spec.repoUrl,\n        access_label,\n        git_path,\n    )\n\n    structured_auth = None\n    if auth:\n        splits = auth.split(\":\")\n        structured_auth = httpx.BasicAuth(\n            splits[0], splits[1] if len(splits) > 1 else \"\"\n        )\n\n    # Build the target URL properly\n    target_url = f\"{deployment.spec.repoUrl.rstrip('/')}/{git_path}\"\n    if request.url.query:\n        target_url += f\"?{request.url.query}\"\n\n    async with httpx.AsyncClient(auth=structured_auth, timeout=120.0) as http_client:\n        try:\n            # Read request body once\n            body = await request.body()\n\n            # Forward headers, excluding host and other problematic ones\n            forward_headers = {\n                k: v\n                for k, v in request.headers.items()\n                if k.lower()\n                not in {\"host\", \"content-length\", \"transfer-encoding\", \"authorization\"}\n            }\n\n            response = await http_client.request(\n                request.method,\n                target_url,\n                content=body,\n                headers=forward_headers,\n            )\n\n            if response.status_code >= 400:\n                rate_limit_remaining = response.headers.get(\"x-ratelimit-remaining\")\n                rate_limit_reset = response.headers.get(\"x-ratelimit-reset\")\n                rate_info = \"\"\n                if rate_limit_remaining is not None:\n                    rate_info = (\n                        f\" ratelimit_remaining={rate_limit_remaining}\"\n                        f\" ratelimit_reset={rate_limit_reset}\"\n                    )\n                logger.warning(\n                    \"Upstream error proxying deployment=%s auth=%s: %s %s returned %d%s\",\n                    deployment.metadata.name,\n                    access_label,\n                    request.method,\n                    target_url,\n                    response.status_code,\n                    rate_info,\n                )\n\n            # Return response with proper status code and headers\n            response_headers = {\n                k: v\n                for k, v in response.headers.items()\n                if k.lower()\n                not in {\"content-length\", \"transfer-encoding\", \"connection\"}\n            }\n\n            return Response(\n                content=response.content,\n                status_code=response.status_code,\n                headers=response_headers,\n            )\n\n        except httpx.TimeoutException:\n            logger.error(f\"Timeout proxying to {target_url}\")\n            raise HTTPException(status_code=504, detail=\"Gateway Timeout\")\n        except httpx.RequestError as e:\n            logger.error(f\"Request error proxying to {target_url}: {e}\")\n            raise HTTPException(status_code=502, detail=\"Bad Gateway\")\n\n\ndef _describe_access(\n    access: GitHubAppAccess | GitRepository | InaccessibleRepository,\n) -> str:\n    match access:\n        case GitHubAppAccess() as a:\n            return f\"github_app(installation={a.installation_id})\"\n        case GitRepository() as r:\n            return \"public\" if r.access_token is None else \"pat\"\n        case _:\n            return \"none\"\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/build_api/build_auth.py",
    "content": "import logging\nfrom typing import Annotated\n\nfrom aiocache import cached\nfrom fastapi import HTTPException, Path, Security\nfrom fastapi.security import (\n    HTTPAuthorizationCredentials,\n    HTTPBasic,\n    HTTPBasicCredentials,\n    HTTPBearer,\n)\nfrom llama_agents.core.schema.deployments import (\n    LlamaDeploymentCRD,\n)\n\nfrom ..k8s_client import validate_deployment_token\n\nlogger = logging.getLogger(__name__)\n\n\n@cached(ttl=15)\nasync def _validate_token_raw(deployment_id: str, token: str) -> str:\n    result = await validate_deployment_token(deployment_id, token)\n    if result is None:\n        logger.warning(\n            \"Auth rejected for deployment=%s: invalid/expired token or deployment not found\",\n            deployment_id,\n        )\n        raise HTTPException(\n            status_code=403,\n            detail=\"Invalid or expired token, or deployment does not exist\",\n        )\n\n    return result.model_dump_json()\n\n\nasync def validate_token(deployment_id: str, token: str) -> LlamaDeploymentCRD:\n    result = await _validate_token_raw(deployment_id, token)\n    return LlamaDeploymentCRD.model_validate_json(result)\n\n\nbearer = HTTPBearer(auto_error=True)\n\n\nasync def authenticate_deployment(\n    deployment_id: Annotated[str, Path()],\n    creds: Annotated[HTTPAuthorizationCredentials, Security(bearer)],\n) -> LlamaDeploymentCRD:\n    \"\"\"\n    FastAPI dependency to validate the token for a deployment.\n    \"\"\"\n    token = creds.credentials\n    return await validate_token(deployment_id, token)\n\n\nbasic = HTTPBasic(auto_error=True)\n\n\nasync def authenticate_deployment_basic(\n    deployment_id: Annotated[str, Path()],\n    creds: Annotated[HTTPBasicCredentials, Security(basic)],\n) -> LlamaDeploymentCRD:\n    \"\"\"\n    FastAPI dependency to validate basic auth for a deployment.\n    Uses password as token, or username as token if password is empty.\n    \"\"\"\n    token = creds.password if creds.password else creds.username\n    return await validate_token(deployment_id, token)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/build_api/build_gc.py",
    "content": "\"\"\"Build artifact garbage collection.\n\nThe grace window preserves the invariant: a build Job surviving within its\nTTLSecondsAfterFinished window with Status.Succeeded > 0 must still have its\nartifact in S3. The operator short-circuits new builds on that assumption, so\nthe grace window must exceed the Job TTL.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom datetime import datetime, timedelta, timezone\n\nfrom llama_agents.control_plane import k8s_client\nfrom llama_agents.control_plane.build_api.build_service import build_artifact_storage\nfrom llama_agents.control_plane.settings import settings\n\nlogger = logging.getLogger(__name__)\n\n# Bounds concurrency when deleting aged-out build artifacts so a large\n# catch-up cohort can't saturate the storage client pool.\n_GC_DELETE_CONCURRENCY = 10\n\n\nasync def _get_referenced_build_ids_from_replicasets(\n    deployment_id: str,\n) -> set[str]:\n    \"\"\"Return build IDs referenced by ReplicaSets in the deployment's revision history.\"\"\"\n    build_ids: set[str] = set()\n\n    try:\n        rs_list = await k8s_client.list_replicasets_for_deployment(deployment_id)\n    except Exception:\n        logger.warning(\"Failed to list ReplicaSets for %s, skipping GC\", deployment_id)\n        return build_ids\n\n    for rs in rs_list:\n        if not rs.spec or not rs.spec.template or not rs.spec.template.spec:\n            continue\n        for container in rs.spec.template.spec.containers or []:\n            for env in container.env or []:\n                if env.name == \"LLAMA_DEPLOY_BUILD_ID\" and env.value:\n                    build_ids.add(env.value)\n        for container in rs.spec.template.spec.init_containers or []:\n            for env in container.env or []:\n                if env.name == \"LLAMA_DEPLOY_BUILD_ID\" and env.value:\n                    build_ids.add(env.value)\n\n    return build_ids\n\n\nasync def gc_build_artifacts(\n    deployment_id: str,\n    keep_build_ids: set[str] | None = None,\n    *,\n    now: datetime | None = None,\n) -> int:\n    \"\"\"Delete artifacts that are both unreferenced and older than the grace window.\n\n    Returns the number of artifacts deleted.\n    \"\"\"\n    storage = build_artifact_storage\n    if storage is None:\n        return 0\n\n    referenced_build_ids = await _get_referenced_build_ids_from_replicasets(\n        deployment_id\n    )\n    if keep_build_ids:\n        referenced_build_ids |= keep_build_ids\n    artifacts = await storage.list_artifacts(deployment_id)\n\n    current_time = now if now is not None else datetime.now(timezone.utc)\n    grace_cutoff = current_time - timedelta(\n        seconds=settings.build_artifact_gc_grace_seconds\n    )\n\n    to_delete: list[str] = []\n    retained_by_grace = 0\n    for artifact in artifacts:\n        if artifact.build_id in referenced_build_ids:\n            continue\n\n        # S3 LastModified may come back naive in some backends; normalize to UTC.\n        artifact_ts = artifact.timestamp\n        if artifact_ts.tzinfo is None:\n            artifact_ts = artifact_ts.replace(tzinfo=timezone.utc)\n\n        if artifact_ts > grace_cutoff:\n            retained_by_grace += 1\n            continue\n\n        to_delete.append(artifact.build_id)\n\n    deleted = 0\n    if to_delete:\n        sem = asyncio.Semaphore(_GC_DELETE_CONCURRENCY)\n\n        async def _delete(build_id: str) -> None:\n            async with sem:\n                logger.info(\n                    \"Deleting unreferenced build artifact: deployment=%s build_id=%s\",\n                    deployment_id,\n                    build_id,\n                )\n                await storage.delete_artifact(deployment_id, build_id)\n\n        results = await asyncio.gather(\n            *(_delete(bid) for bid in to_delete),\n            return_exceptions=True,\n        )\n        for build_id, result in zip(to_delete, results):\n            if isinstance(result, BaseException):\n                logger.warning(\n                    \"Failed to delete build artifact: deployment=%s build_id=%s error=%s\",\n                    deployment_id,\n                    build_id,\n                    result,\n                )\n            else:\n                deleted += 1\n\n    if deleted > 0 or retained_by_grace > 0:\n        logger.info(\n            \"GC complete: deployment=%s deleted=%d retained_by_grace=%d total=%d\",\n            deployment_id,\n            deleted,\n            retained_by_grace,\n            len(artifacts),\n        )\n    return deleted\n\n\nasync def gc_all_build_artifacts() -> int:\n    \"\"\"Run GC across all deployments. Returns total artifacts deleted.\"\"\"\n    if build_artifact_storage is None:\n        return 0\n\n    total_deleted = 0\n    try:\n        deployments = await k8s_client.list_all_deployments()\n        deployment_names = {d.metadata.name for d in deployments if d.metadata}\n\n        # For each known deployment, GC its artifacts\n        for name in deployment_names:\n            total_deleted += await gc_build_artifacts(name)\n\n    except Exception:\n        logger.exception(\"Failed to run GC across all deployments\")\n\n    return total_deleted\n\n\nasync def delete_all_artifacts_for_deployment(deployment_id: str) -> int:\n    \"\"\"Delete all build artifacts for a deployment (used on deployment deletion).\"\"\"\n    if build_artifact_storage is None:\n        return 0\n\n    return await build_artifact_storage.delete_all_artifacts(deployment_id)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/build_api/build_service.py",
    "content": "\"\"\"Build artifact service — creates and manages BuildArtifactStorage.\"\"\"\n\nfrom __future__ import annotations\n\nfrom ..build_api.build_storage import BuildArtifactStorage\nfrom ..settings import settings\n\n\ndef create_build_artifact_storage() -> BuildArtifactStorage | None:\n    \"\"\"Create a BuildArtifactStorage if S3 is configured, else return None.\"\"\"\n    if not settings.s3_bucket:\n        return None\n\n    return BuildArtifactStorage(\n        bucket=settings.s3_bucket,\n        endpoint_url=settings.s3_endpoint_url,\n        region=settings.s3_region,\n        access_key=settings.s3_access_key,\n        secret_key=settings.s3_secret_key,\n        key_prefix=settings.build_s3_key_prefix,\n        unsigned=settings.s3_unsigned,\n    )\n\n\nbuild_artifact_storage = create_build_artifact_storage()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/build_api/build_storage.py",
    "content": "\"\"\"S3-compatible build artifact storage backend.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import AsyncIterator\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import IO, Any\n\nfrom botocore.exceptions import ClientError\nfrom llama_agents.control_plane.storage import S3ObjectStorage\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass ArtifactInfo:\n    \"\"\"Metadata about a build artifact in S3.\"\"\"\n\n    deployment_name: str\n    build_id: str\n    timestamp: datetime\n    size_bytes: int\n\n\nclass BuildArtifactStorage(S3ObjectStorage):\n    \"\"\"Upload, download, list, and delete build artifacts in S3-compatible storage.\"\"\"\n\n    class NotFoundError(Exception):\n        \"\"\"Raised when an artifact is not found in S3.\"\"\"\n\n    def __init__(\n        self,\n        bucket: str,\n        endpoint_url: str | None = None,\n        region: str | None = None,\n        access_key: str | None = None,\n        secret_key: str | None = None,\n        key_prefix: str = \"builds\",\n        unsigned: bool = False,\n    ) -> None:\n        super().__init__(\n            bucket=bucket,\n            endpoint_url=endpoint_url,\n            region=region,\n            access_key=access_key,\n            secret_key=secret_key,\n            key_prefix=key_prefix,\n            unsigned=unsigned,\n        )\n\n    def _key(self, deployment_name: str, build_id: str) -> str:\n        return f\"{self._key_prefix}/{deployment_name}/{build_id}.tar.gz\"\n\n    async def upload_artifact(\n        self, deployment_name: str, build_id: str, data: bytes\n    ) -> None:\n        \"\"\"Upload a build artifact to S3.\"\"\"\n        async with self._client() as client:\n            await client.put_object(\n                Bucket=self._bucket,\n                Key=self._key(deployment_name, build_id),\n                Body=data,\n            )\n\n    async def upload_artifact_fileobj(\n        self, deployment_name: str, build_id: str, fileobj: IO[Any]\n    ) -> None:\n        \"\"\"Upload a build artifact to S3 from a file-like object.\"\"\"\n        async with self._client() as client:\n            await client.upload_fileobj(\n                fileobj,\n                self._bucket,\n                self._key(deployment_name, build_id),\n            )\n\n    async def download_artifact_streaming(\n        self, deployment_name: str, build_id: str, chunk_size: int = 65536\n    ) -> tuple[int, AsyncIterator[bytes]]:\n        \"\"\"Stream a build artifact from S3.\n\n        Returns (content_length, async_iterator_of_chunks).\n        Raises NotFoundError if the artifact does not exist.\n\n        The caller must consume the iterator within the same context — the S3\n        client session stays open until the iterator is exhausted.\n        \"\"\"\n        client_cm = self._client()\n        client = await client_cm.__aenter__()\n        try:\n            response = await client.get_object(\n                Bucket=self._bucket,\n                Key=self._key(deployment_name, build_id),\n            )\n        except ClientError as e:\n            await client_cm.__aexit__(type(e), e, e.__traceback__)\n            if e.response.get(\"Error\", {}).get(\"Code\") == \"NoSuchKey\":\n                raise self.NotFoundError(\n                    f\"Artifact not found: {deployment_name}/{build_id}\"\n                ) from e\n            raise\n\n        content_length: int = response[\"ContentLength\"]\n        body = response[\"Body\"]\n\n        async def _stream() -> AsyncIterator[bytes]:\n            try:\n                async for chunk in body.iter_chunks(chunk_size):\n                    yield chunk\n            finally:\n                await client_cm.__aexit__(None, None, None)\n\n        return content_length, _stream()\n\n    async def artifact_exists(self, deployment_name: str, build_id: str) -> bool:\n        \"\"\"Check if a build artifact exists in S3.\"\"\"\n        async with self._client() as client:\n            try:\n                await client.head_object(\n                    Bucket=self._bucket,\n                    Key=self._key(deployment_name, build_id),\n                )\n                return True\n            except ClientError as e:\n                if e.response.get(\"Error\", {}).get(\"Code\") == \"404\":\n                    return False\n                raise\n\n    async def delete_artifact(self, deployment_name: str, build_id: str) -> None:\n        \"\"\"Delete a build artifact from S3.\"\"\"\n        async with self._client() as client:\n            await client.delete_object(\n                Bucket=self._bucket,\n                Key=self._key(deployment_name, build_id),\n            )\n\n    async def list_artifacts(self, deployment_name: str) -> list[ArtifactInfo]:\n        \"\"\"List all build artifacts for a deployment, sorted by timestamp descending.\"\"\"\n        prefix = f\"{self._key_prefix}/{deployment_name}/\"\n        async with self._client() as client:\n            paginator = client.get_paginator(\"list_objects_v2\")\n            artifacts: list[ArtifactInfo] = []\n            async for page in paginator.paginate(Bucket=self._bucket, Prefix=prefix):\n                for obj in page.get(\"Contents\", []):\n                    key = obj.get(\"Key\")\n                    if key is None or not key.endswith(\".tar.gz\"):\n                        continue\n                    last_modified = obj.get(\"LastModified\")\n                    size = obj.get(\"Size\")\n                    if last_modified is None or size is None:\n                        continue\n                    build_id = key.removeprefix(prefix).removesuffix(\".tar.gz\")\n                    artifacts.append(\n                        ArtifactInfo(\n                            deployment_name=deployment_name,\n                            build_id=build_id,\n                            timestamp=last_modified,\n                            size_bytes=size,\n                        )\n                    )\n            artifacts.sort(key=lambda a: a.timestamp, reverse=True)\n            return artifacts\n\n    async def delete_all_artifacts(self, deployment_name: str) -> int:\n        \"\"\"Delete all build artifacts for a deployment. Returns count of deleted artifacts.\"\"\"\n        artifacts = await self.list_artifacts(deployment_name)\n        for artifact in artifacts:\n            await self.delete_artifact(deployment_name, artifact.build_id)\n        return len(artifacts)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/code_repo/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/code_repo/git_server.py",
    "content": "\"\"\"Git HTTP server backed by dulwich and S3.\n\nProvides WSGI-based git serving for both the manage API (read+write)\nand the build API (read-only). Bare repos are stored as tarballs in S3.\n\nThe readonly path uses ``a2wsgi.WSGIMiddleware`` for true bidirectional\nstreaming (no full-body buffering).  The read+write path spools the\nrequest body to a ``SpooledTemporaryFile`` to cap memory usage while\npreserving post-processing (ref diff, S3 upload, callback).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport shutil\nfrom collections.abc import Awaitable, Callable\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tempfile import SpooledTemporaryFile\nfrom typing import Any, cast\n\nfrom a2wsgi.asgi_typing import HTTPScope\nfrom a2wsgi.wsgi import Body, WSGIResponder, build_environ\nfrom a2wsgi.wsgi_typing import Environ\nfrom dulwich.refs import Ref\nfrom dulwich.repo import Repo\nfrom dulwich.server import BackendRepo, DictBackend\nfrom dulwich.web import make_wsgi_chain\nfrom fastapi import Request\nfrom fastapi.responses import Response\nfrom starlette.concurrency import run_in_threadpool\n\nfrom .storage import CodeRepoStorage\n\nlogger = logging.getLogger(__name__)\n\n# Shared thread pool for WSGIMiddleware instances (readonly path).\n_wsgi_executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix=\"WSGI-git\")\n\n# Request bodies smaller than this stay in memory; larger ones spool to disk.\n_SPOOL_MAX_SIZE = 10 * 1024 * 1024  # 10 MB\n\n\ndef _create_wsgi_app(\n    repo: Repo,\n) -> Any:\n    \"\"\"Create a dulwich WSGI app for the given bare repo.\"\"\"\n    backend = DictBackend({\"/\": cast(\"BackendRepo\", repo)})\n    return make_wsgi_chain(backend)\n\n\ndef _call_wsgi(\n    wsgi_app: Any,\n    environ: Environ,\n) -> tuple[int, list[tuple[str, str]], bytes]:\n    \"\"\"Call a WSGI app synchronously, returning (status_code, headers, body).\"\"\"\n    status_code = 500\n    response_headers: list[tuple[str, str]] = []\n    body_parts: list[bytes] = []\n\n    def start_response(\n        status: str,\n        headers: list[tuple[str, str]],\n        exc_info: Any = None,\n    ) -> Any:\n        nonlocal status_code, response_headers\n        status_code = int(status.split(\" \", 1)[0])\n        response_headers = list(headers)\n        return lambda s: body_parts.append(s)\n\n    result = wsgi_app(environ, start_response)\n    try:\n        for chunk in result:\n            body_parts.append(chunk)\n    finally:\n        # PEP 3333: WSGI iterators may optionally have a close() method\n        if hasattr(result, \"close\"):\n            result.close()\n\n    return status_code, response_headers, b\"\".join(body_parts)\n\n\ndef _git_scope(request: Request, git_path: str) -> HTTPScope:\n    \"\"\"Build an ASGI scope with the path overridden to the git sub-path.\"\"\"\n    scope = dict(request.scope)\n    scope[\"path\"] = f\"/{git_path}\"\n    scope[\"root_path\"] = \"\"\n    return cast(HTTPScope, scope)\n\n\n_HOP_BY_HOP_HEADERS = frozenset((\"transfer-encoding\", \"connection\"))\n\n\nasync def _serve_wsgi_git(\n    request: Request,\n    repo: Repo,\n    git_path: str,\n) -> tuple[int, dict[str, str], bytes]:\n    \"\"\"Run a dulwich WSGI git request and return (status, headers, body).\n\n    The request body is spooled to a ``SpooledTemporaryFile`` so that\n    large pack files (e.g. from ``git push``) don't exhaust memory.\n    \"\"\"\n    wsgi_app = _create_wsgi_app(repo)\n\n    spooled: SpooledTemporaryFile[bytes] = SpooledTemporaryFile(\n        max_size=_SPOOL_MAX_SIZE\n    )\n    try:\n        async for chunk in request.stream():\n            spooled.write(chunk)\n        content_length = spooled.tell()\n        spooled.seek(0)\n\n        environ = build_environ(_git_scope(request, git_path), cast(Body, spooled))\n\n        # The ASGI server (uvicorn) already de-chunks the request body, but\n        # build_environ preserves the original Transfer-Encoding header.\n        # Dulwich's WSGI handler tries to de-chunk the body when it sees\n        # this header, causing a ValueError on already-de-chunked data.\n        # Set the real content length and remove the stale header.\n        env = cast(dict[str, Any], environ)\n        env[\"CONTENT_LENGTH\"] = str(content_length)\n        env.pop(\"HTTP_TRANSFER_ENCODING\", None)\n\n        status_code, response_headers, response_body = await run_in_threadpool(\n            _call_wsgi, wsgi_app, environ\n        )\n    finally:\n        spooled.close()\n\n    headers = {\n        k: v for k, v in response_headers if k.lower() not in _HOP_BY_HOP_HEADERS\n    }\n\n    return status_code, headers, response_body\n\n\ndef _get_resolved_refs(repo: Repo) -> dict[Ref, bytes]:\n    \"\"\"Safely get resolved refs, handling unresolvable symbolic refs.\n\n    ``repo.get_refs()`` raises ``KeyError`` when HEAD is a symbolic ref\n    pointing to a branch that does not yet exist (e.g. freshly initialised\n    bare repo).  This helper skips refs that cannot be resolved.\n    \"\"\"\n    result: dict[Ref, bytes] = {}\n    for key in repo.refs.allkeys():\n        try:\n            result[key] = repo.refs[key]\n        except KeyError:\n            continue\n    return result\n\n\nasync def handle_git_request(\n    request: Request,\n    deployment_id: str,\n    git_path: str,\n    storage: CodeRepoStorage,\n    on_push_complete: Callable[[str, str | None, str | None], Awaitable[None]]\n    | None = None,\n) -> Response:\n    \"\"\"Handle a git HTTP request (read+write).\n\n    Downloads the bare repo from S3, serves the git request via dulwich,\n    and if refs changed (receive-pack), uploads the updated repo back to S3.\n\n    Args:\n        request: The incoming HTTP request.\n        deployment_id: The deployment to serve.\n        git_path: The git-specific path (e.g., \"info/refs\", \"git-receive-pack\").\n        storage: The CodeRepoStorage instance.\n        on_push_complete: Optional async callback called after a successful push.\n            Called with (deployment_id, new_sha, git_ref).\n    \"\"\"\n    repo_path = await storage.download_repo(deployment_id)\n    if repo_path is None:\n        repo_path = CodeRepoStorage.init_bare_repo(deployment_id)\n\n    try:\n        with Repo(str(repo_path)) as repo:\n            refs_before = _get_resolved_refs(repo)\n            status_code, headers, response_body = await _serve_wsgi_git(\n                request, repo, git_path\n            )\n            refs_after = _get_resolved_refs(repo)\n\n        new_sha: str | None = None\n        git_ref: str | None = None\n        if refs_before != refs_after:\n            # Find the first changed non-HEAD ref — this is the branch\n            # that was actually pushed (e.g. refs/heads/my-feature).\n            # Use its SHA directly rather than trying to resolve HEAD,\n            # which may point to a different branch (e.g. dulwich defaults\n            # HEAD to refs/heads/master on a fresh bare repo).\n            for ref_name in refs_after:\n                if ref_name == b\"HEAD\":\n                    continue\n                if (\n                    ref_name not in refs_before\n                    or refs_after[ref_name] != refs_before[ref_name]\n                ):\n                    new_sha = refs_after[ref_name].decode()\n                    git_ref = ref_name.decode().removeprefix(\"refs/heads/\")\n                    break\n\n            logger.info(\n                \"Refs changed for deployment %s, uploading to S3\",\n                deployment_id,\n            )\n            await storage.upload_repo(deployment_id, repo_path)\n\n            if on_push_complete:\n                await on_push_complete(deployment_id, new_sha, git_ref)\n\n        return Response(\n            content=response_body,\n            status_code=status_code,\n            headers=headers,\n        )\n    finally:\n        if repo_path:\n            shutil.rmtree(repo_path.parent, ignore_errors=True)\n\n\nclass _StreamingWSGIResponse(Response):\n    \"\"\"A Response that delegates to a2wsgi.WSGIMiddleware for streaming.\n\n    Starlette calls ``Response.__call__(scope, receive, send)`` to send\n    the response.  This subclass overrides ``__call__`` to forward\n    directly to the WSGIMiddleware ASGI app, giving us true streaming\n    without buffering the entire response body in memory.\n\n    Cleanup (temp directory removal) runs in the ``finally`` block after\n    ``WSGIMiddleware.__call__`` returns — which only happens after the\n    response is fully sent.\n    \"\"\"\n\n    def __init__(\n        self,\n        asgi_app: WSGIResponder,\n        git_scope: HTTPScope,\n        receive: Any,\n        cleanup: Callable[[], None] | None,\n    ) -> None:\n        self.background = None\n        self.asgi_app = asgi_app\n        self.git_scope = git_scope\n        self._receive = receive\n        self.cleanup = cleanup\n\n    async def __call__(self, scope: Any, receive: Any, send: Any) -> None:\n        try:\n            await self.asgi_app(self.git_scope, self._receive, send)\n        finally:\n            if self.cleanup:\n                self.cleanup()\n\n\nasync def handle_git_request_readonly(\n    request: Request,\n    deployment_id: str,\n    git_path: str,\n    storage: CodeRepoStorage,\n) -> Response:\n    \"\"\"Handle a read-only git HTTP request (upload-pack only).\n\n    Used by the build API for git clone operations.  The response is\n    streamed directly via ``a2wsgi.WSGIMiddleware`` — never fully\n    buffered in memory.\n    \"\"\"\n    repo_path = await storage.download_repo(deployment_id)\n    if repo_path is None:\n        return Response(\n            content=\"No code has been pushed to this deployment yet.\",\n            status_code=404,\n        )\n\n    # Reject receive-pack requests\n    if \"git-receive-pack\" in git_path:\n        shutil.rmtree(repo_path.parent, ignore_errors=True)\n        return Response(\n            content=\"Push not allowed on this endpoint.\",\n            status_code=403,\n        )\n\n    repo: Repo | None = None\n    try:\n        repo = Repo(str(repo_path))\n        wsgi_app = _create_wsgi_app(repo)\n        responder = WSGIResponder(wsgi_app, _wsgi_executor, send_queue_size=10)\n    except Exception:\n        if repo is not None:\n            repo.close()\n        shutil.rmtree(repo_path.parent, ignore_errors=True)\n        raise\n\n    def _cleanup() -> None:\n        repo.close()\n        shutil.rmtree(repo_path.parent, ignore_errors=True)\n\n    return _StreamingWSGIResponse(\n        asgi_app=responder,\n        git_scope=_git_scope(request, git_path),\n        receive=request.receive,\n        cleanup=_cleanup,\n    )\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/code_repo/service.py",
    "content": "\"\"\"Code repo service — creates and manages CodeRepoStorage.\"\"\"\n\nfrom __future__ import annotations\n\nfrom ..settings import settings\nfrom .storage import CodeRepoStorage\n\n\ndef create_code_repo_storage() -> CodeRepoStorage | None:\n    \"\"\"Create a CodeRepoStorage if S3 is configured, else return None.\"\"\"\n    if not settings.s3_bucket:\n        return None\n\n    return CodeRepoStorage(\n        bucket=settings.s3_bucket,\n        endpoint_url=settings.s3_endpoint_url,\n        region=settings.s3_region,\n        access_key=settings.s3_access_key,\n        secret_key=settings.s3_secret_key,\n        key_prefix=settings.code_repo_s3_key_prefix,\n        unsigned=settings.s3_unsigned,\n    )\n\n\ncode_repo_storage = create_code_repo_storage()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/code_repo/storage.py",
    "content": "\"\"\"S3 storage for bare git repositories as tarballs.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport shutil\nimport sys\nimport tarfile\nimport tempfile\nfrom pathlib import Path\n\nfrom botocore.exceptions import ClientError\nfrom dulwich.objects import ObjectID\nfrom dulwich.porcelain import gc as dulwich_gc\nfrom dulwich.repo import Repo\nfrom llama_agents.core.git.git_util import GitAccessError, resolve_ref_in_repo\nfrom starlette.concurrency import run_in_threadpool\n\nfrom ..storage import S3ObjectStorage\n\nlogger = logging.getLogger(__name__)\n\n\nclass CodeRepoStorage(S3ObjectStorage):\n    \"\"\"Stores bare git repositories as tarballs in S3.\n\n    Each deployment gets one bare repo stored as a gzipped tarball at:\n        {key_prefix}/{deployment_id}/repo.tar.gz\n    \"\"\"\n\n    def _s3_key(self, deployment_id: str) -> str:\n        if self._key_prefix:\n            return f\"{self._key_prefix}/{deployment_id}/repo.tar.gz\"\n        return f\"{deployment_id}/repo.tar.gz\"\n\n    async def download_repo(self, deployment_id: str) -> Path | None:\n        \"\"\"Download and extract repo tarball from S3 to a temp dir.\n\n        Returns the path to the bare repo directory, or None if no repo exists.\n        The caller is responsible for cleaning up the temp dir.\n        \"\"\"\n        key = self._s3_key(deployment_id)\n        tmp_dir = Path(tempfile.mkdtemp(prefix=f\"code-repo-{deployment_id}-\"))\n        tar_path = tmp_dir / \"repo.tar.gz\"\n        try:\n            async with self._client() as client:\n                response = await client.get_object(Bucket=self._bucket, Key=key)\n                with open(tar_path, \"wb\") as f:\n                    async for chunk in response[\"Body\"].iter_chunks():\n                        f.write(chunk)\n            with tarfile.open(tar_path, \"r:gz\") as tar:\n                if sys.version_info >= (3, 12):\n                    tar.extractall(path=tmp_dir, filter=\"data\")\n                else:\n                    # filter param added in 3.12; safe here since we\n                    # create the tarballs ourselves in upload_repo.\n                    tar.extractall(path=tmp_dir)\n            tar_path.unlink()\n            repo_path = tmp_dir / \"repo\"\n            if not repo_path.exists():\n                logger.error(\"Tarball for %s missing 'repo' directory\", deployment_id)\n                shutil.rmtree(tmp_dir, ignore_errors=True)\n                return None\n            return repo_path\n        except ClientError as e:\n            error_code = e.response.get(\"Error\", {}).get(\"Code\", \"\")\n            if error_code in (\"404\", \"NoSuchKey\"):\n                # No repo has been pushed yet\n                shutil.rmtree(tmp_dir, ignore_errors=True)\n                return None\n            shutil.rmtree(tmp_dir, ignore_errors=True)\n            raise\n        except Exception:\n            shutil.rmtree(tmp_dir, ignore_errors=True)\n            raise\n\n    async def upload_repo(self, deployment_id: str, repo_path: Path) -> None:\n        \"\"\"Run dulwich GC on the repo, tar+gzip it, and upload to S3.\"\"\"\n        await run_in_threadpool(dulwich_gc, str(repo_path))\n\n        key = self._s3_key(deployment_id)\n        tar_path = repo_path.parent / \"repo-upload.tar.gz\"\n        try:\n            await run_in_threadpool(self._create_tarball, repo_path, tar_path)\n            with open(tar_path, \"rb\") as f:\n                async with self._client() as client:\n                    await client.upload_fileobj(f, self._bucket, key)\n            logger.info(\n                \"Uploaded repo for deployment %s (%d bytes)\",\n                deployment_id,\n                tar_path.stat().st_size,\n            )\n        finally:\n            if tar_path.exists():\n                tar_path.unlink()\n\n    async def delete_repo(self, deployment_id: str) -> None:\n        \"\"\"Delete the repo tarball from S3.\"\"\"\n        key = self._s3_key(deployment_id)\n        async with self._client() as client:\n            await client.delete_object(Bucket=self._bucket, Key=key)\n        logger.info(\"Deleted repo for deployment %s\", deployment_id)\n\n    async def repo_exists(self, deployment_id: str) -> bool:\n        \"\"\"Check if a repo tarball exists in S3.\"\"\"\n        key = self._s3_key(deployment_id)\n        try:\n            async with self._client() as client:\n                await client.head_object(Bucket=self._bucket, Key=key)\n            return True\n        except ClientError as e:\n            if e.response.get(\"Error\", {}).get(\"Code\", \"\") in (\"404\", \"NoSuchKey\"):\n                return False\n            raise\n\n    @staticmethod\n    def _create_tarball(repo_path: Path, tar_path: Path) -> None:\n        \"\"\"Create a gzipped tarball of the repo directory.\"\"\"\n        with tarfile.open(tar_path, \"w:gz\") as tar:\n            tar.add(str(repo_path), arcname=\"repo\")\n\n    async def resolve_ref(self, deployment_id: str, git_ref: str) -> str | None:\n        \"\"\"Resolve a branch, tag, or commit SHA from the S3-stored bare repo.\n\n        Downloads the repo tarball, reads the ref, and cleans up.\n        Returns the SHA hex string, or None if the ref or repo doesn't exist.\n        \"\"\"\n        repo_path = await self.download_repo(deployment_id)\n        if repo_path is None:\n            return None\n        try:\n            with Repo(str(repo_path)) as repo:\n                try:\n                    target_sha = resolve_ref_in_repo(repo, git_ref)\n                except GitAccessError:\n                    return None\n                if target_sha is None:\n                    return None\n                # Only accept commit objects — reject trees, blobs, etc.\n                try:\n                    obj = repo.get_object(ObjectID(target_sha))\n                except KeyError:\n                    return None\n                if obj.type_name != b\"commit\":\n                    return None\n                return target_sha.decode()\n        finally:\n            shutil.rmtree(repo_path.parent, ignore_errors=True)\n\n    @staticmethod\n    def init_bare_repo(deployment_id: str) -> Path:\n        \"\"\"Create an empty bare repo in a temp directory.\n\n        Returns the path to the bare repo. The caller is responsible for\n        cleaning up the temp dir.\n        \"\"\"\n        tmp_dir = Path(tempfile.mkdtemp(prefix=f\"code-repo-{deployment_id}-\"))\n        repo_path = tmp_dir / \"repo\"\n        repo_path.mkdir()\n        Repo.init_bare(str(repo_path))\n        return repo_path\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/git/__init__.py",
    "content": "from . import github_api_client, github_api_schema\nfrom ._git_service import GitService, git_service\n\n__all__ = [\n    \"git_service\",\n    \"GitService\",\n    \"github_api_client\",\n    \"github_api_schema\",\n]\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/git/_git_service.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nimport asyncio\nimport logging\nimport shutil\nimport tempfile\nimport urllib.parse\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom aiocache import cached\nfrom httpx import HTTPStatusError\nfrom llama_agents.core.config import DEFAULT_DEPLOYMENT_FILE_PATH\nfrom llama_agents.core.deployment_config import (\n    read_deployment_config,\n    resolve_config_parent,\n)\nfrom llama_agents.core.git.git_util import (\n    GitAccessError,\n    clone_repo,\n    parse_github_repo_url,\n    validate_git_credential_access,\n    validate_git_public_access,\n)\nfrom llama_agents.core.schema.git_validation import (\n    GitApplicationValidationResponse,\n    RepositoryValidationResponse,\n)\nfrom llama_agents.core.ui_build import ui_build_output_path\n\nfrom .. import k8s_client\nfrom ._github_auth import GitHubAppAuth, get_github_app_auth\nfrom .github_api_client import (\n    GitHubApiClient,\n    github_app_api_client,\n    installation_api_client,\n    pat_api_client,\n)\n\n_CONFIG_FILES = (\n    \"llama_agents.toml\",\n    \"llama_deploy.toml\",\n    \"pyproject.toml\",\n    \"llama_agents.yaml\",\n    \"llama_deploy.yaml\",\n)\n\n_CONFIG_EXTENSIONS = (\".yaml\", \".yml\", \".toml\")\n\n\ndef _looks_like_config_file(path: str) -> bool:\n    lower = path.lower()\n    return any(lower.endswith(ext) for ext in _CONFIG_EXTENSIONS)\n\n\ndef _normalize_repo_relative_path(repo_path: str) -> Path:\n    \"\"\"Return a normalized repo-relative path or raise on traversal attempts.\"\"\"\n    candidate = Path(repo_path)\n    if candidate.is_absolute() or \"..\" in candidate.parts:\n        raise ValueError(f\"Path must stay within the repository: {repo_path}\")\n\n    normalized_parts = [part for part in candidate.parts if part not in (\"\", \".\")]\n    if not normalized_parts:\n        return Path(\".\")\n    return Path(*normalized_parts)\n\n\ndef _repo_target_path(repo_root: Path, repo_path: str) -> Path:\n    \"\"\"Build a safe local path under ``repo_root`` for a repo-relative path.\"\"\"\n    return repo_root / _normalize_repo_relative_path(repo_path)\n\n\ndef _deployment_base_dir(deployment_rel_path: str) -> Path:\n    \"\"\"Return the directory that contains the deployment config.\"\"\"\n    normalized = _normalize_repo_relative_path(deployment_rel_path)\n    if _looks_like_config_file(deployment_rel_path):\n        return normalized.parent\n    if normalized != Path(\".\"):\n        return normalized\n    return Path(\".\")\n\n\n@dataclass\nclass GitHubAppAccess:\n    owner: str\n    repo: str\n    installation_id: int\n\n\n@dataclass\nclass GitRepository:\n    url: str\n    access_token: str | None  # passed as basic auth colon delimited username:password\n    default_branch: str | None = None\n\n\n@dataclass\nclass InaccessibleRepository:\n    message: str\n    github_app_name: str | None = None\n    github_app_installation_url: str | None = None\n\n\nGitAccessType = GitHubAppAccess | GitRepository | InaccessibleRepository\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass RepositoryExistenceResult:\n    exists: bool | None\n    message: str | None = None\n    via_installation: bool = False\n\n\nclass GitService:\n    @cached(ttl=10)\n    async def get_access(\n        self,\n        repository_url: str,\n        deployment_id: str,\n        pat: str | None = None,\n        existing_pat: str | None = None,\n    ) -> GitAccessType:\n        if self._is_github_repository(repository_url):\n            return await self._check_github_access_type(\n                repository_url, pat, existing_pat\n            )\n        else:\n            return await self._check_generic_access_type(\n                repository_url, deployment_id, pat, existing_pat\n            )\n\n    async def validate_repository(\n        self,\n        repository_url: str,\n        project_id: str,\n        deployment_id: str | None = None,\n        pat: str | None = None,\n    ) -> RepositoryValidationResponse:\n        \"\"\"\n        Validates repository access and returns unified response.\n\n        Args:\n            repository_url: The repository URL to validate\n            deployment_id: Optional existing deployment ID to check for existing credentials\n            pat: Optional PAT to validate (for new deployments or PAT updates)\n        \"\"\"\n\n        existing_pat = (\n            None\n            if deployment_id is None\n            else await k8s_client.get_deployment_pat(deployment_id)\n        )\n\n        access = await self.get_access(\n            repository_url, deployment_id, pat, existing_pat=existing_pat\n        )\n\n        # Check if PAT is obsolete - this happens when:\n        # 1. Repo is public and deployment has a PAT\n        # 2. GitHub App is available for GitHub repos and deployment has a PAT\n        pat_is_obsolete = False\n        if deployment_id and existing_pat:\n            github_app_auth = get_github_app_auth()\n            if self._is_github_repository(repository_url):\n                # For GitHub repos, PAT is obsolete if GitHub App is available\n                pat_is_obsolete = github_app_auth is not None\n            else:\n                # For non-GitHub repos, PAT is obsolete if repo is public\n                pat_is_obsolete = (\n                    isinstance(access, GitRepository) and access.access_token is None\n                )\n\n        # For GitHub repos, always try to include the app name and connect URL\n        github_app_name: str | None = None\n        github_app_installation_url: str | None = None\n        github_app_settings_url: str | None = None\n        if self._is_github_repository(repository_url):\n            github_app_auth = get_github_app_auth()\n            if github_app_auth:\n                github_app_name = github_app_auth.app_name\n                try:\n                    (owner, _) = parse_github_repo_url(repository_url)\n                    github_app_installation_url = await self._construct_install_url(\n                        github_app_auth, owner\n                    )\n                except ValueError:\n                    # Invalid URL format, skip connect URL\n                    pass\n\n        # For GitHubAppAccess, construct the settings URL for managing the installation\n        if isinstance(access, GitHubAppAccess):\n            github_app_settings_url = await self._construct_settings_url(\n                access.owner, access.installation_id\n            )\n\n        response: RepositoryValidationResponse\n        match access:\n            case GitHubAppAccess():\n                response = RepositoryValidationResponse(\n                    accessible=True,\n                    message=\"Access confirmed via GitHub App installation.\",\n                    pat_is_obsolete=pat_is_obsolete,\n                    github_app_name=github_app_name,\n                    github_app_installation_url=github_app_installation_url,\n                    github_app_settings_url=github_app_settings_url,\n                )\n            case GitRepository() as git_repository:\n                if git_repository.access_token is None:\n                    message = \"Repository is a public repository.\"\n                else:\n                    if existing_pat and pat is None:\n                        message = \"Access confirmed via existing Personal Access Token.\"\n                    else:\n                        message = \"Access confirmed via Personal Access Token.\"\n\n                response = RepositoryValidationResponse(\n                    accessible=True,\n                    message=message,\n                    pat_is_obsolete=pat_is_obsolete,\n                    github_app_name=github_app_name,\n                    github_app_installation_url=github_app_installation_url,\n                )\n            case InaccessibleRepository() as inaccessible_repository:\n                response = RepositoryValidationResponse(\n                    accessible=False,\n                    message=inaccessible_repository.message,\n                    github_app_name=inaccessible_repository.github_app_name,\n                    github_app_installation_url=inaccessible_repository.github_app_installation_url,\n                )\n            case _:\n                raise ValueError(f\"Invalid access type: {access}\")\n\n        return response\n\n    def _is_github_repository(self, repository_url: str) -> bool:\n        \"\"\"Check if the repository URL is a GitHub repository.\"\"\"\n        if repository_url.startswith(\"git@\"):\n            host = repository_url.split(\"@\", 1)[1].split(\":\", 1)[0]\n            return host == \"github.com\"\n\n        url = repository_url\n        if \"://\" not in url:\n            url = \"https://\" + url\n        parsed = urllib.parse.urlparse(url)\n        hostname = parsed.hostname or \"\"\n        return hostname in {\"github.com\", \"www.github.com\"}\n\n    async def _check_github_access_type(\n        self,\n        repository_url: str,\n        pat: str | None = None,\n        existing_pat: str | None = None,\n    ) -> GitAccessType:\n        \"\"\"Validate GitHub repository access.\"\"\"\n        try:\n            (owner, repo) = parse_github_repo_url(repository_url)\n        except ValueError:\n            return InaccessibleRepository(\n                message=(\n                    \"Invalid GitHub repository URL. \"\n                    \"Expected format 'https://github.com/<owner>/<repository>'.\"\n                )\n            )\n\n        try:\n            if await validate_git_public_access(repository_url):\n                logger.info(\"Access resolved for %s/%s: public\", owner, repo)\n                return GitRepository(\n                    url=repository_url,\n                    access_token=None,\n                )\n        except GitAccessError as e:\n            logger.info(\n                \"Public probe skipped for %s/%s: %s\",\n                owner,\n                repo,\n                e,\n            )\n\n        logger.info(\n            \"Public probe failed for %s/%s, trying authenticated methods\", owner, repo\n        )\n\n        github_app_auth = get_github_app_auth()\n        org_installation = None\n\n        if github_app_auth:\n            repo_installation = await github_app_api_client.get_repository_installation(\n                owner, repo\n            )\n\n            if repo_installation and repo_installation.id:\n                logger.info(\n                    \"Access resolved for %s/%s: github_app repo_installation=%d\",\n                    owner,\n                    repo,\n                    repo_installation.id,\n                )\n                return GitHubAppAccess(\n                    owner=owner,\n                    repo=repo,\n                    installation_id=repo_installation.id,\n                )\n\n            org_installation = await github_app_api_client.get_org_installation(owner)\n            if org_installation:\n                logger.info(\n                    \"Found org installation for %s: id=%s selection=%s\",\n                    owner,\n                    org_installation.id,\n                    org_installation.repository_selection,\n                )\n            else:\n                logger.info(\"No GitHub App installation found for %s/%s\", owner, repo)\n\n        pat_to_test = pat or existing_pat\n\n        if pat_to_test:\n            if await self._validate_pat_access(owner, repo, pat_to_test):\n                logger.info(\"Access resolved for %s/%s: pat\", owner, repo)\n                return GitRepository(\n                    url=repository_url,\n                    access_token=pat_to_test,\n                )\n            logger.info(\"PAT validation failed for %s/%s\", owner, repo)\n\n        installation_token: str | None = None\n        if org_installation and org_installation.id:\n            try:\n                installation_token = (\n                    await github_app_api_client.get_installation_access_token(\n                        org_installation.id\n                    )\n                )\n            except HTTPStatusError:\n                installation_token = None\n\n        existence = await self._check_github_repository_exists(\n            owner=owner,\n            repo_name=repo,\n            pat=pat_to_test,\n            installation_token=installation_token,\n        )\n\n        if (\n            existence.exists\n            and existence.via_installation\n            and org_installation\n            and org_installation.id\n        ):\n            logger.info(\n                \"Access resolved for %s/%s: github_app org_installation=%d\",\n                owner,\n                repo,\n                org_installation.id,\n            )\n            return GitHubAppAccess(\n                owner=owner,\n                repo=repo,\n                installation_id=org_installation.id,\n            )\n\n        if (\n            existence.exists is None\n            and org_installation is not None\n            and org_installation.repository_selection == \"all\"\n        ):\n            existence = RepositoryExistenceResult(\n                exists=False,\n                message=f\"GitHub repository '{owner}/{repo}' does not exist.\",\n            )\n        elif (\n            existence.exists is None\n            and org_installation is not None\n            and org_installation.repository_selection == \"selected\"\n        ):\n            existence = RepositoryExistenceResult(\n                exists=None,\n                message=(\n                    \"GitHub App installation is limited to selected repositories and \"\n                    f\"does not currently include '{owner}/{repo}'.\"\n                ),\n            )\n\n        github_app_auth = get_github_app_auth()\n        app_name = github_app_auth.app_name if github_app_auth else None\n\n        app_install_url = (\n            await self._construct_install_url(github_app_auth, owner)\n            if github_app_auth\n            else None\n        )\n\n        if existence.exists is False:\n            message = (\n                existence.message\n                or f\"GitHub repository '{owner}/{repo}' does not exist.\"\n            )\n            app_name = None\n            app_install_url = None\n        else:\n            if existence.message:\n                message = existence.message\n            elif pat_to_test:\n                message = (\n                    \"Personal Access Token does not have access to this repository.\"\n                )\n            else:\n                message = (\n                    f\"Unable to access GitHub repository '{owner}/{repo}'. \"\n                    \"If the repository is private, install the GitHub App for this owner \"\n                    \"or provide a Personal Access Token.\"\n                )\n\n        logger.warning(\n            \"GitHub repo inaccessible: %s/%s — %s\",\n            owner,\n            repo,\n            message,\n        )\n        return InaccessibleRepository(\n            message=message,\n            github_app_name=app_name,\n            github_app_installation_url=app_install_url,\n        )\n\n    async def _check_github_repository_exists(\n        self,\n        owner: str,\n        repo_name: str,\n        pat: str | None,\n        installation_token: str | None,\n    ) -> RepositoryExistenceResult:\n        \"\"\"\n        Attempt to verify that a GitHub repository exists using anonymous and PAT-backed requests.\n\n        Returns:\n            RepositoryExistenceResult where `exists` is:\n                True  - repository confirmed to exist\n                False - repository confirmed not to exist (e.g. owner missing)\n                None  - repository existence could not be confirmed\n        \"\"\"\n\n        clients: list[tuple[GitHubApiClient, str]] = []\n\n        if pat:\n            clients.append((pat_api_client(pat), \"pat\"))\n\n        if installation_token:\n            clients.append(\n                (installation_api_client(installation_token), \"installation\")\n            )\n\n        clients.append((GitHubApiClient(), \"unauthenticated\"))\n\n        for client, source in clients:\n            try:\n                repo_info = await client.get_repository_info(owner, repo_name)\n            except HTTPStatusError:\n                repo_info = None\n\n            if repo_info is not None:\n                return RepositoryExistenceResult(\n                    exists=True, via_installation=source == \"installation\"\n                )\n\n        for client, _ in clients:\n            try:\n                owner_info = await client.get_owner_info(owner)\n            except HTTPStatusError:\n                owner_info = None\n\n            if owner_info is not None:\n                return RepositoryExistenceResult(exists=None)\n\n        return RepositoryExistenceResult(\n            exists=False,\n            message=f\"GitHub owner '{owner}' does not exist.\",\n        )\n\n    async def _check_generic_access_type(\n        self,\n        repository_url: str,\n        deployment_id: str | None = None,\n        pat: str | None = None,\n        existing_pat: str | None = None,\n    ) -> GitAccessType:\n        \"\"\"Validate non-GitHub repository access using git commands.\"\"\"\n\n        if await validate_git_public_access(repository_url):\n            return GitRepository(\n                url=repository_url,\n                access_token=None,\n            )\n\n        pat_to_test = pat or existing_pat\n\n        if pat_to_test:\n            if await validate_git_credential_access(repository_url, pat_to_test):\n                return GitRepository(\n                    url=repository_url,\n                    access_token=pat_to_test,\n                )\n            else:\n                logger.warning(\n                    \"Generic repo inaccessible: %s — PAT rejected\",\n                    repository_url,\n                )\n                return InaccessibleRepository(\n                    message=\"Personal Access Token does not have access to this repository.\",\n                )\n\n        logger.warning(\n            \"Generic repo inaccessible: %s — private or does not exist\",\n            repository_url,\n        )\n        return InaccessibleRepository(\n            message=\"Repository is private or does not exist.\"\n        )\n\n    async def _validate_pat_access(self, owner: str, repo: str, pat: str) -> bool:\n        \"\"\"Validate that a PAT has access to the repository by attempting to fetch it.\"\"\"\n        return bool(await pat_api_client(pat).get_repository_info(owner, repo))\n\n    async def _check_pat_obsolete(self, deployment_id: str) -> bool:\n        \"\"\"Check if a deployment has PAT but GitHub App access is now available.\"\"\"\n        if not deployment_id:\n            return False\n\n        github_app_auth = get_github_app_auth()\n        if not github_app_auth:\n            return False\n\n        return await k8s_client.has_deployment_pat(deployment_id)\n\n    async def _check_has_existing_pat(self, deployment_id: str) -> bool:\n        \"\"\"Check if a deployment has PAT for a now-public non-GitHub repository.\"\"\"\n        if not deployment_id:\n            return False\n\n        has_pat = await k8s_client.has_deployment_pat(deployment_id)\n        return has_pat\n\n    async def _construct_install_url(self, app: GitHubAppAuth, owner: str) -> str:\n        \"\"\"\n        Construct a targeted GitHub App installation URL.\n\n        Uses the owner's numeric ID to skip the account picker and jump straight\n        to the installation flow for that specific account.\n\n        https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-using-url-parameters\n        \"\"\"\n        owner_info = await GitHubApiClient().get_owner_info(owner)\n        if owner_info:\n            return f\"https://github.com/apps/{app.app_name}/installations/new/permissions?target_id={owner_info.id}\"\n\n        # Fallback to basic installation URL that shows account picker.\n        # This should probably also return an error or warning, as it's likely to mean that the owner is invalid.\n        # All owners should be accessible via this API.\n        return f\"https://github.com/apps/{app.app_name}/installations/new\"\n\n    async def _construct_settings_url(\n        self, owner: str, installation_id: int\n    ) -> str | None:\n        \"\"\"Construct a GitHub App installation settings URL.\n\n        Returns the URL to manage an existing installation's repository access.\n        The URL format differs for organizations vs personal accounts.\n        \"\"\"\n        owner_info = await GitHubApiClient().get_owner_info(owner)\n        if not owner_info:\n            return None\n        if owner_info.type == \"Organization\":\n            return f\"https://github.com/organizations/{owner}/settings/installations/{installation_id}\"\n        return f\"https://github.com/settings/installations/{installation_id}\"\n\n    async def obtain_basic_auth_token(\n        self,\n        repository_url: str,\n        deployment_id: str | None = None,\n        pat: str | None = None,\n    ) -> tuple[str | None, GitAccessType]:\n        \"\"\"\n        Obtain a basic auth token for a repository.\n        Returns a tuple of the basic auth token and the access type. (If token is none, you may want to check and validate uf the reason being InaccessibleRepository)\n        \"\"\"\n        access = await self.get_access(repository_url, deployment_id, pat)\n\n        auth = None\n        match access:\n            case GitHubAppAccess() as github_app_access:\n                installation_auth = (\n                    await github_app_api_client.get_installation_access_token(\n                        github_app_access.installation_id\n                    )\n                )\n                auth = f\"x-access-token:{installation_auth}\"\n            case GitRepository() as git_repository:\n                auth = git_repository.access_token\n\n        return auth, access\n\n    async def validate_git_application(\n        self,\n        repository_url: str,\n        git_ref: str | None = None,\n        deployment_file_path: str | None = None,\n        deployment_id: str | None = None,\n        pat: str | None = None,\n    ) -> GitApplicationValidationResponse:\n        \"\"\"Verify that the specified configuration is 1. reachable, and 2. has a valid deployment file.\"\"\"\n\n        pat = (\n            await k8s_client.get_deployment_pat(deployment_id)\n            if deployment_id is not None and pat is None\n            else pat\n        )\n\n        auth, access = await self.obtain_basic_auth_token(\n            repository_url, deployment_id, pat\n        )\n\n        if isinstance(access, InaccessibleRepository):\n            return GitApplicationValidationResponse(\n                is_valid=False,\n                error_message=access.message,\n            )\n\n        if self._is_github_repository(repository_url):\n            return await self._validate_github_application(\n                repository_url=repository_url,\n                git_ref=git_ref,\n                deployment_file_path=deployment_file_path,\n                access=access,\n            )\n\n        return await self._validate_via_clone(\n            repository_url=repository_url,\n            git_ref=git_ref,\n            deployment_file_path=deployment_file_path,\n            auth=auth,\n        )\n\n    async def _validate_via_clone(\n        self,\n        repository_url: str,\n        git_ref: str | None,\n        deployment_file_path: str | None,\n        auth: str | None,\n    ) -> GitApplicationValidationResponse:\n        \"\"\"Used for non-GitHub repositories where the GitHub Contents API is unavailable.\"\"\"\n        with tempfile.TemporaryDirectory() as temp_dir:\n            try:\n                result = await clone_repo(\n                    repository_url,\n                    git_ref=git_ref,\n                    basic_auth=auth,\n                    dest_dir=Path(temp_dir),\n                )\n            except GitAccessError as e:\n                return GitApplicationValidationResponse(\n                    is_valid=False,\n                    error_message=e.message,\n                )\n            repo_root = Path(temp_dir)\n            deployment_rel_path = deployment_file_path or DEFAULT_DEPLOYMENT_FILE_PATH\n            config_path = repo_root / deployment_rel_path\n            try:\n                config = read_deployment_config(repo_root, config_path)\n                config.validate_config()\n                is_valid = True\n            except Exception as e:\n                return GitApplicationValidationResponse(\n                    is_valid=False,\n                    error_message=f\"Invalid deployment config: {str(e)}\",\n                )\n\n            config_parent = resolve_config_parent(repo_root, config_path)\n            ui_dist_relative_to_config = ui_build_output_path(config_parent, config)\n            ui_dist_relative_to_repo = (\n                (config_parent / ui_dist_relative_to_config).relative_to(repo_root)\n                if ui_dist_relative_to_config\n                else None\n            )\n\n            return GitApplicationValidationResponse(\n                is_valid=is_valid,\n                git_sha=result.git_sha,\n                git_ref=result.git_ref,\n                valid_deployment_file_path=deployment_file_path,\n                ui_build_output_path=ui_dist_relative_to_repo,\n            )\n\n    async def _validate_github_application(\n        self,\n        repository_url: str,\n        git_ref: str | None,\n        deployment_file_path: str | None,\n        access: GitAccessType,\n    ) -> GitApplicationValidationResponse:\n        \"\"\"Validate a GitHub repository's deployment config without cloning.\n\n        Resolve the ref via the Commits API, fetch candidate config files via\n        the Contents API, then parse the result on disk.\n        \"\"\"\n        try:\n            owner, repo = parse_github_repo_url(repository_url)\n        except ValueError as e:\n            return GitApplicationValidationResponse(\n                is_valid=False,\n                error_message=str(e),\n            )\n\n        client = await self._github_client_for_access(access)\n\n        try:\n            target_ref = git_ref\n            if target_ref is None:\n                if (\n                    isinstance(access, GitRepository)\n                    and access.default_branch is not None\n                ):\n                    target_ref = access.default_branch\n                else:\n                    target_ref = await client.get_default_branch(owner, repo)\n            if target_ref is None:\n                return GitApplicationValidationResponse(\n                    is_valid=False,\n                    error_message=(\n                        f\"Could not determine default branch for {owner}/{repo}.\"\n                    ),\n                )\n            commit_sha = await client.get_commit_sha(owner, repo, target_ref)\n        except HTTPStatusError as e:\n            return GitApplicationValidationResponse(\n                is_valid=False,\n                error_message=f\"GitHub API error resolving ref: {e}\",\n            )\n        if commit_sha is None:\n            return GitApplicationValidationResponse(\n                is_valid=False,\n                error_message=f\"Git ref not found: {git_ref or target_ref}\",\n            )\n\n        deployment_rel_path = deployment_file_path or DEFAULT_DEPLOYMENT_FILE_PATH\n        try:\n            normalized_deployment_rel_path = _normalize_repo_relative_path(\n                deployment_rel_path\n            )\n        except ValueError as e:\n            return GitApplicationValidationResponse(\n                is_valid=False,\n                error_message=f\"Invalid deployment path: {e}\",\n            )\n\n        temp_dir = tempfile.mkdtemp(prefix=\"cp_repo_config_\")\n        try:\n            repo_root = Path(temp_dir)\n            try:\n                fetched_any = await self._fetch_config_files(\n                    client,\n                    owner,\n                    repo,\n                    commit_sha,\n                    normalized_deployment_rel_path.as_posix(),\n                    repo_root,\n                )\n            except HTTPStatusError as e:\n                return GitApplicationValidationResponse(\n                    is_valid=False,\n                    error_message=f\"GitHub API error fetching config: {e}\",\n                )\n\n            if not fetched_any:\n                return GitApplicationValidationResponse(\n                    is_valid=False,\n                    error_message=(\n                        f\"No deployment config found at {deployment_rel_path}\"\n                    ),\n                )\n\n            config_path = repo_root / normalized_deployment_rel_path\n            try:\n                config = read_deployment_config(repo_root, config_path)\n                config.validate_config()\n            except Exception as e:\n                return GitApplicationValidationResponse(\n                    is_valid=False,\n                    error_message=f\"Invalid deployment config: {str(e)}\",\n                )\n\n            if config.ui and config.ui.directory:\n                try:\n                    await self._fetch_ui_package_json(\n                        client,\n                        owner,\n                        repo,\n                        commit_sha,\n                        normalized_deployment_rel_path.as_posix(),\n                        config.ui.directory,\n                        repo_root,\n                    )\n                except HTTPStatusError as e:\n                    logger.warning(\n                        \"Failed to fetch UI package.json for %s/%s: %s\",\n                        owner,\n                        repo,\n                        e,\n                    )\n                try:\n                    config = read_deployment_config(repo_root, config_path)\n                    config.validate_config()\n                except Exception as e:\n                    return GitApplicationValidationResponse(\n                        is_valid=False,\n                        error_message=f\"Invalid deployment config: {str(e)}\",\n                    )\n\n            config_parent = resolve_config_parent(repo_root, config_path)\n            ui_dist_relative_to_config = ui_build_output_path(config_parent, config)\n            ui_dist_relative_to_repo = (\n                (config_parent / ui_dist_relative_to_config).relative_to(repo_root)\n                if ui_dist_relative_to_config\n                else None\n            )\n\n            return GitApplicationValidationResponse(\n                is_valid=True,\n                git_sha=commit_sha,\n                git_ref=git_ref or target_ref,\n                valid_deployment_file_path=deployment_file_path,\n                ui_build_output_path=ui_dist_relative_to_repo,\n            )\n        finally:\n            shutil.rmtree(temp_dir, ignore_errors=True)\n\n    async def _github_client_for_access(self, access: GitAccessType) -> GitHubApiClient:\n        \"\"\"Return a Contents-API client appropriate for the resolved access type.\"\"\"\n        if isinstance(access, GitHubAppAccess):\n            installation_token = (\n                await github_app_api_client.get_installation_access_token(\n                    access.installation_id\n                )\n            )\n            return installation_api_client(installation_token)\n        if isinstance(access, GitRepository) and access.access_token:\n            return pat_api_client(access.access_token)\n        return GitHubApiClient()\n\n    @staticmethod\n    async def _fetch_config_files(\n        client: GitHubApiClient,\n        owner: str,\n        repo: str,\n        ref: str,\n        deployment_rel_path: str,\n        repo_root: Path,\n    ) -> bool:\n        \"\"\"Fetch candidate config files from the GitHub Contents API.\n\n        Writes any successfully fetched files into ``repo_root`` so the\n        on-disk parser (`read_deployment_config`) can read them. Returns\n        True if at least one file was fetched.\n        \"\"\"\n        if _looks_like_config_file(deployment_rel_path):\n            content = await client.get_file_contents(\n                owner, repo, deployment_rel_path, ref\n            )\n            if content is None:\n                return False\n            target = _repo_target_path(repo_root, deployment_rel_path)\n            target.parent.mkdir(parents=True, exist_ok=True)\n            target.write_bytes(content)\n            return True\n\n        config_dir = _repo_target_path(repo_root, deployment_rel_path)\n        config_dir.mkdir(parents=True, exist_ok=True)\n\n        async def fetch(filename: str) -> tuple[str, bytes | None]:\n            file_path = (\n                f\"{deployment_rel_path}/{filename}\"\n                if deployment_rel_path not in (\"\", \".\")\n                else filename\n            )\n            return filename, await client.get_file_contents(owner, repo, file_path, ref)\n\n        results = await asyncio.gather(*[fetch(name) for name in _CONFIG_FILES])\n        fetched_any = False\n        for filename, content in results:\n            if content is None:\n                continue\n            target = config_dir / filename\n            target.write_bytes(content)\n            fetched_any = True\n        return fetched_any\n\n    @staticmethod\n    async def _fetch_ui_package_json(\n        client: GitHubApiClient,\n        owner: str,\n        repo: str,\n        ref: str,\n        deployment_rel_path: str,\n        ui_directory: str,\n        repo_root: Path,\n    ) -> bool:\n        \"\"\"Fetch ``package.json`` for the UI directory and write it to disk.\"\"\"\n        base_dir = _deployment_base_dir(deployment_rel_path)\n        relative_pkg_path = _normalize_repo_relative_path(\n            (base_dir / ui_directory / \"package.json\").as_posix()\n        )\n        api_path = relative_pkg_path.as_posix().removeprefix(\"./\")\n\n        content = await client.get_file_contents(owner, repo, api_path, ref)\n        if content is None:\n            return False\n        target = _repo_target_path(repo_root, relative_pkg_path.as_posix())\n        target.parent.mkdir(parents=True, exist_ok=True)\n        target.write_bytes(content)\n        return True\n\n\ngit_service = GitService()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/git/_github_auth.py",
    "content": "import datetime\nimport functools\nimport os\nfrom pathlib import Path\n\nimport jwt\n\n\nclass GitHubAppAuth:\n    \"\"\"Handles GitHub App authentication and JWT generation.\"\"\"\n\n    jwt: str | None = None\n    jwt_expires_at: datetime.datetime | None = None\n    # max 10 minutes for github apps\n    jwt_expiration_seconds: int = 600\n\n    def __init__(self, client_id: str, private_key: str | Path, app_name: str):\n        \"\"\"\n        Initialize GitHub App authentication.\n\n        Args:\n            app_id: GitHub App ID\n            private_key: Either the private key content as string or path to private key file\n        \"\"\"\n        self.client_id = client_id\n        self.app_name = app_name\n        self.private_key = self._load_private_key(private_key)\n\n    def _load_private_key(self, private_key: str | Path) -> str:\n        \"\"\"Load private key from string or file path.\"\"\"\n        if isinstance(private_key, Path) or (\n            isinstance(private_key, str) and private_key.startswith(\"/\")\n        ):\n            # Treat as file path\n            key_path = Path(private_key)\n            if not key_path.exists():\n                raise FileNotFoundError(f\"Private key file not found: {key_path}\")\n            return key_path.read_text().strip()\n        else:\n            # Treat as key content\n            return private_key.strip()\n\n    def get_jwt(self) -> str:\n        \"\"\"Get a JWT for GitHub App authentication.\"\"\"\n        if self.jwt is None:\n            expiration, jwt = self._generate_jwt()\n            self.jwt_expires_at = expiration\n            self.jwt = jwt\n        elif (\n            self.jwt_expires_at is not None\n            and self.jwt_expires_at\n            < datetime.datetime.now(tz=datetime.timezone.utc)\n            + datetime.timedelta(seconds=self.jwt_expiration_seconds / 20)\n        ):\n            expiration, jwt = self._generate_jwt()\n            self.jwt_expires_at = expiration\n            self.jwt = jwt\n        return self.jwt\n\n    def _generate_jwt(self) -> tuple[datetime.datetime, str]:\n        \"\"\"\n        Generate a JWT for GitHub App authentication.\n\n        Args:\n            expiration_seconds: JWT expiration time in seconds (max 600 for GitHub Apps)\n\n        Returns:\n            JWT token string\n\n        Raises:\n            ValueError: If expiration exceeds GitHub's 10-minute limit\n        \"\"\"\n\n        now = datetime.datetime.now(tz=datetime.timezone.utc)\n        expires_at = now + datetime.timedelta(seconds=self.jwt_expiration_seconds)\n        payload = {\n            \"iss\": self.client_id,  # Issuer: GitHub App ID\n            \"iat\": int(now.timestamp()),  # Issued at\n            \"exp\": int(  # Expiration\n                (\n                    now + datetime.timedelta(seconds=self.jwt_expiration_seconds)\n                ).timestamp()\n            ),\n        }\n\n        return expires_at, jwt.encode(payload, self.private_key, algorithm=\"RS256\")\n\n\n@functools.lru_cache(maxsize=None)\ndef get_github_app_auth() -> GitHubAppAuth | None:\n    \"\"\"Get the GitHubAppAuth instance.\"\"\"\n    client_id = os.getenv(\"GITHUB_APP_CLIENT_ID\")\n    private_key = os.getenv(\"GITHUB_APP_PRIVATE_KEY\")\n    app_name = os.getenv(\"GITHUB_APP_NAME\")\n\n    if client_id is None or private_key is None or app_name is None:\n        return None\n\n    return GitHubAppAuth(client_id, private_key, app_name)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/git/github_api_client.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nimport base64\nimport urllib.parse\nfrom typing import Callable\n\nimport httpx\nfrom aiocache import cached\nfrom pydantic import TypeAdapter\n\nfrom ._github_auth import get_github_app_auth\nfrom .github_api_schema import GithubAppInstallation, GitHubOwnerInfo, GitHubRepository\n\n\nclass GitHubApiClient:\n    def __init__(\n        self,\n        auth_middleware: Callable[[httpx.Request], httpx.Request] = lambda x: x,\n    ):\n        self.client = httpx.AsyncClient(\n            base_url=\"https://api.github.com\", auth=auth_middleware\n        )\n\n    async def get_owner_info(self, owner: str) -> GitHubOwnerInfo | None:\n        \"\"\"Get owner information including numeric ID for GitHub App installation URLs.\"\"\"\n        response = await self.client.get(f\"/users/{owner}\", timeout=10.0)\n        if response.status_code == 200:\n            data = response.json()\n            return GitHubOwnerInfo(\n                id=data[\"id\"], login=data[\"login\"], type=data[\"type\"]\n            )\n        elif response.status_code == 404:\n            return None\n        else:\n            response.raise_for_status()\n            return None  # unreachable, but satisfies type checker\n\n    async def get_repository_info(\n        self, owner: str, repo: str\n    ) -> GitHubRepository | None:\n        \"\"\"Get repository information if accessible.\"\"\"\n        # WARNING! This will throw 401 errors if you have an App JWT token\n        response = await self.client.get(f\"/repos/{owner}/{repo}\")\n        if response.status_code == 404:\n            return None\n        response.raise_for_status()\n        return GitHubRepository.model_validate(response.json())\n\n    async def get_commit_sha(self, owner: str, repo: str, ref: str) -> str | None:\n        \"\"\"Resolve any git ref (branch, tag, full SHA, short SHA) to a full commit SHA.\n\n        Returns the full 40-character SHA, or None if the ref does not resolve.\n        Treats GitHub's 422 (ambiguous short SHA) the same as a 404.\n        \"\"\"\n        encoded_ref = urllib.parse.quote(ref, safe=\"\")\n        response = await self.client.get(f\"/repos/{owner}/{repo}/commits/{encoded_ref}\")\n        if response.status_code in (404, 422):\n            return None\n        response.raise_for_status()\n        data = response.json()\n        sha = data.get(\"sha\")\n        if not isinstance(sha, str):\n            return None\n        return sha\n\n    async def get_default_branch(self, owner: str, repo: str) -> str | None:\n        \"\"\"Return the default branch name for the repository, or None if 404.\"\"\"\n        repo_info = await self.get_repository_info(owner, repo)\n        if repo_info is None:\n            return None\n        return repo_info.default_branch\n\n    async def get_file_contents(\n        self, owner: str, repo: str, path: str, ref: str\n    ) -> bytes | None:\n        \"\"\"Fetch a file from a repository via the Contents API.\n\n        Returns the decoded file bytes, or None if the path does not exist\n        or points at a directory.\n        \"\"\"\n        response = await self.client.get(\n            f\"/repos/{owner}/{repo}/contents/{path}\", params={\"ref\": ref}\n        )\n        if response.status_code == 404:\n            return None\n        response.raise_for_status()\n        data = response.json()\n        if isinstance(data, list):  # directory listing, not a file\n            return None\n        if data.get(\"type\") != \"file\":\n            return None\n        content = data.get(\"content\")\n        if not isinstance(content, str):\n            return None\n        return base64.b64decode(content.replace(\"\\n\", \"\"))\n\n\ndef get_app_jwt_client() -> httpx.AsyncClient:\n    github_app_auth = get_github_app_auth()\n    if github_app_auth is None:\n        raise ValueError(\"Github app auth is not configured\")\n    jwt_token = github_app_auth.get_jwt()\n    return httpx.AsyncClient(\n        base_url=\"https://api.github.com\",\n        headers={\"Authorization\": f\"Bearer {jwt_token}\"},\n    )\n\n\nclass GitHubAppApiClient(GitHubApiClient):\n    def __init__(self) -> None:\n        super().__init__(auth_middleware=self._auth_middleware)\n\n    def _auth_middleware(self, request: httpx.Request) -> httpx.Request:\n        github_app_auth = get_github_app_auth()\n        if github_app_auth is None:\n            raise ValueError(\"Github app auth is not configured\")\n        jwt_token = github_app_auth.get_jwt()\n        request.headers.update({\"Authorization\": f\"Bearer {jwt_token}\"})\n        return request\n\n    async def get_repository_installation(\n        self, owner: str, repo: str\n    ) -> GithubAppInstallation | None:\n        response = await self.client.get(f\"/repos/{owner}/{repo}/installation\")\n        if response.status_code == 404:\n            return None\n        response.raise_for_status()\n\n        return GithubAppInstallation.model_validate(response.json())\n\n    async def get_org_installation(self, org: str) -> GithubAppInstallation | None:\n        response = await self.client.get(f\"/orgs/{org}/installation\")\n        if response.status_code == 404:\n            return None\n        response.raise_for_status()\n\n        return GithubAppInstallation.model_validate(response.json())\n\n    async def list_installations(self) -> list[GithubAppInstallation]:\n        response = await self.client.get(\"/app/installations\")\n        response.raise_for_status()\n        return ListInstallations.validate_python(response.json())\n\n    @cached(ttl=60)\n    async def get_installation_access_token(self, installation_id: int) -> str:\n        response = await self.client.post(\n            f\"/app/installations/{installation_id}/access_tokens\"\n        )\n        response.raise_for_status()\n        return response.json()[\"token\"]\n\n\ndef pat_api_client(pat: str) -> GitHubApiClient:\n    def authenticate_with_pat(request: httpx.Request) -> httpx.Request:\n        request.headers.update({\"Authorization\": f\"token {pat}\"})\n        return request\n\n    return GitHubApiClient(auth_middleware=authenticate_with_pat)\n\n\ndef installation_api_client(access_token: str) -> GitHubApiClient:\n    def authenticate_with_installation(request: httpx.Request) -> httpx.Request:\n        request.headers.update({\"Authorization\": f\"token {access_token}\"})\n        return request\n\n    return GitHubApiClient(auth_middleware=authenticate_with_installation)\n\n\ngithub_app_api_client = GitHubAppApiClient()\n\nListInstallations = TypeAdapter(list[GithubAppInstallation])\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/git/github_api_schema.py",
    "content": "from datetime import datetime\nfrom typing import Any\n\nimport pydantic\n\n\nclass GitHubOwnerInfo(pydantic.BaseModel):\n    id: int\n    login: str\n    type: str  # \"User\" or \"Organization\"\n\n\nclass GitHubRepository(pydantic.BaseModel):\n    id: int | None = None\n    name: str | None = None\n    full_name: str | None = None\n    owner: GitHubOwnerInfo | None = None\n    private: bool = False\n    html_url: str | None = None\n    description: str | None = None\n    fork: bool = False\n    created_at: datetime | None = None\n    updated_at: datetime | None = None\n    pushed_at: datetime | None = None\n    clone_url: str | None = None\n    ssh_url: str | None = None\n    size: int = 0\n    stargazers_count: int = 0\n    watchers_count: int = 0\n    language: str | None = None\n    forks_count: int = 0\n    archived: bool = False\n    disabled: bool = False\n    open_issues_count: int = 0\n    topics: list[str] = []\n    visibility: str | None = None\n    default_branch: str | None = None\n\n\nclass GithubAppInstallation(pydantic.BaseModel):\n    id: int | None = None\n    account: dict[str, Any] | None = None\n    repository_selection: str | None = (\n        None  # must making these optional since idk what is\n    )\n    access_tokens_url: str | None = None\n    repositories_url: str | None = None\n    html_url: str | None = None\n    permissions: dict[str, str] | None = None\n    events: list[str] | None = None\n    created_at: datetime | None = None\n    updated_at: datetime | None = None\n    single_file_name: str | None = None\n    has_multiple_single_files: bool | None = None\n    single_file_paths: list[str] | None = None\n    app_slug: str | None = None\n    # Not sure of these types\n    # suspended_at: datetime | None = None\n    # suspended_by: dict[str, Any] | None = None\n\n    # Example response:\n    # {\n    #   \"id\": 1,\n    #   \"account\": {\n    #     \"login\": \"github\",\n    #     \"id\": 1,\n    #     \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n    #     \"avatar_url\": \"https://github.com/images/error/hubot_happy.gif\",\n    #     \"gravatar_id\": \"\",\n    #     \"url\": \"https://api.github.com/orgs/github\",\n    #     \"html_url\": \"https://github.com/github\",\n    #     \"followers_url\": \"https://api.github.com/users/github/followers\",\n    #     \"following_url\": \"https://api.github.com/users/github/following{/other_user}\",\n    #     \"gists_url\": \"https://api.github.com/users/github/gists{/gist_id}\",\n    #     \"starred_url\": \"https://api.github.com/users/github/starred{/owner}{/repo}\",\n    #     \"subscriptions_url\": \"https://api.github.com/users/github/subscriptions\",\n    #     \"organizations_url\": \"https://api.github.com/users/github/orgs\",\n    #     \"repos_url\": \"https://api.github.com/orgs/github/repos\",\n    #     \"events_url\": \"https://api.github.com/orgs/github/events\",\n    #     \"received_events_url\": \"https://api.github.com/users/github/received_events\",\n    #     \"type\": \"Organization\",\n    #     \"site_admin\": false\n    #   },\n    #   \"repository_selection\": \"all\",\n    #   \"access_tokens_url\": \"https://api.github.com/app/installations/1/access_tokens\",\n    #   \"repositories_url\": \"https://api.github.com/installation/repositories\",\n    #   \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n    #   \"app_id\": 1,\n    #   \"client_id\": \"Iv1.ab1112223334445c\",\n    #   \"target_id\": 1,\n    #   \"target_type\": \"Organization\",\n    #   \"permissions\": {\n    #     \"checks\": \"write\",\n    #     \"metadata\": \"read\",\n    #     \"contents\": \"read\"\n    #   },\n    #   \"events\": [\n    #     \"push\",\n    #     \"pull_request\"\n    #   ],\n    #   \"created_at\": \"2018-02-09T20:51:14Z\",\n    #   \"updated_at\": \"2018-02-09T20:51:14Z\",\n    #   \"single_file_name\": \"config.yml\",\n    #   \"has_multiple_single_files\": true,\n    #   \"single_file_paths\": [\n    #     \"config.yml\",\n    #     \".github/issue_TEMPLATE.md\"\n    #   ],\n    #   \"app_slug\": \"github-actions\",\n    #   \"suspended_at\": null,\n    #   \"suspended_by\": null\n    # }\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/k8s_client.py",
    "content": "import asyncio\nimport base64\nimport functools\nimport hashlib\nimport logging\nimport queue as thread_queue\nimport random\nimport re\nimport threading\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import (\n    Any,\n    AsyncGenerator,\n    Callable,\n    Coroutine,\n    List,\n    Literal,\n    ParamSpec,\n    TypeVar,\n    cast,\n)\n\nfrom kubernetes import client\nfrom kubernetes import config as k8s_config\nfrom kubernetes.client import (\n    AppsV1Api,\n    CoreV1Api,\n    CoreV1Event,\n    CustomObjectsApi,\n    NetworkingV1Api,\n    V1Pod,\n    V1ReplicaSet,\n)\nfrom kubernetes.client.exceptions import ApiException\nfrom llama_agents.core.config import DEFAULT_DEPLOYMENT_FILE_PATH\nfrom llama_agents.core.iter_utils import merge_generators\nfrom llama_agents.core.schema.deployments import (\n    DeploymentEvent,\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    DeploymentUpdate,\n    LlamaDeploymentCRD,\n    LlamaDeploymentMetadata,\n    LlamaDeploymentPhase,\n    LlamaDeploymentSpec,\n    LlamaDeploymentStatus,\n    ReleaseHistoryItem,\n    apply_deployment_update,\n    image_tag_to_version,\n)\nfrom llama_agents.core.schema.projects import ProjectSummary\nfrom pydantic import HttpUrl\nfrom urllib3 import HTTPResponse\nfrom urllib3.exceptions import ProtocolError\n\nfrom .settings import settings\n\nlogger = logging.getLogger(__name__)\n\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\n\n\ndef to_async(func: Callable[P, R]) -> Callable[P, Coroutine[Any, Any, R]]:\n    \"\"\"Decorator that exposes a synchronous function as an async callable.\n\n    Runs the original function in a separate thread using ``asyncio.to_thread``.\n    Type hints are preserved using ``ParamSpec``/``TypeVar`` (Python 3.12+).\n    \"\"\"\n\n    @functools.wraps(func)\n    async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n        return await asyncio.to_thread(func, *args, **kwargs)\n\n    return wrapper\n\n\nclass K8sClient:\n    def __init__(self) -> None:\n        # Configure namespace\n        self.namespace = self._get_namespace()\n\n        # Configure ingress settings from settings\n        self.enable_ingress = settings.local_dev_ingress\n        self.domain = settings.local_dev_domain\n\n        # Initialize Kubernetes client attributes (will be set lazily)\n        self._k8s_core_v1: CoreV1Api | None = None\n        self._k8s_custom_objects: CustomObjectsApi | None = None\n        self._k8s_networking_v1: NetworkingV1Api | None = None\n        self._k8s_apps_v1: AppsV1Api | None = None\n        self._k8s_initialized = False\n\n    def _get_namespace(self) -> str:\n        \"\"\"Get the namespace from settings or current pod namespace\"\"\"\n        # Check settings first\n        namespace = settings.kubernetes_namespace\n        if namespace:\n            return namespace\n\n        # Try to read from service account token (when running in pod)\n        try:\n            with open(\n                \"/var/run/secrets/kubernetes.io/serviceaccount/namespace\", \"r\"\n            ) as f:\n                return f.read().strip()\n        except (FileNotFoundError, PermissionError):\n            # Fall back to default namespace\n            return \"llama-agents\"\n\n    def _ensure_k8s_client(self) -> None:\n        \"\"\"Initialize Kubernetes client if not already initialized\"\"\"\n        if not self._k8s_initialized:\n            try:\n                # Try to load in-cluster config first\n                _cfg = cast(Any, k8s_config)\n                _cfg.load_incluster_config()\n            except Exception:\n                # Fall back to local kubeconfig\n                _cfg = cast(Any, k8s_config)\n                _cfg.load_kube_config()\n\n            self._k8s_core_v1 = client.CoreV1Api()\n            self._k8s_custom_objects = client.CustomObjectsApi()\n            self._k8s_networking_v1 = client.NetworkingV1Api()\n            self._k8s_apps_v1 = client.AppsV1Api()\n            self._k8s_initialized = True\n\n    @property\n    def k8s_core_v1(self) -> CoreV1Api:\n        \"\"\"Lazily initialized CoreV1Api client\"\"\"\n        self._ensure_k8s_client()\n        assert self._k8s_core_v1 is not None\n        return self._k8s_core_v1\n\n    @property\n    def k8s_custom_objects(self) -> CustomObjectsApi:\n        \"\"\"Lazily initialized CustomObjectsApi client\"\"\"\n        self._ensure_k8s_client()\n        assert self._k8s_custom_objects is not None\n        return self._k8s_custom_objects\n\n    @property\n    def k8s_networking_v1(self) -> NetworkingV1Api:\n        \"\"\"Lazily initialized NetworkingV1Api client\"\"\"\n        self._ensure_k8s_client()\n        assert self._k8s_networking_v1 is not None\n        return self._k8s_networking_v1\n\n    @property\n    def k8s_apps_v1(self) -> AppsV1Api:\n        \"\"\"Lazily initialized AppsV1Api client\"\"\"\n        self._ensure_k8s_client()\n        assert self._k8s_apps_v1 is not None\n        return self._k8s_apps_v1\n\n\n# Global k8s client instance\n_k8s_client = K8sClient()\n\n\nasync def validate_deployment_id(deployment_id: str) -> bool:\n    \"\"\"Check if a deployment ID is available (returns True if available)\"\"\"\n    try:\n        await get_deployment_crd(deployment_id)\n        # If we get here, the deployment exists, so ID is not available\n        return False\n    except ApiException as e:\n        if e.status == 404:\n            # Deployment doesn't exist, so ID is available\n            return True\n        else:\n            # Some other error occurred\n            logger.error(f\"Error checking deployment ID {deployment_id}: {e}\")\n            return False\n\n\nasync def validate_deployment_token(\n    deployment_id: str, token: str\n) -> None | LlamaDeploymentCRD:\n    \"\"\"Validate that the token belongs to the specified deployment (O(1) lookup)\"\"\"\n\n    try:\n        result = await get_deployment_crd(deployment_id)\n    except ApiException as e:\n        if e.status == 404:\n            # Deployment doesn't exist\n            return None\n        else:\n            raise\n\n    if result.status.authToken is None or result.status.authToken != token:\n        return None\n\n    return result\n\n\ndef _append_random_suffix(deployment_id: str, max_length: int) -> str:\n    randomness = 5\n    hex_suffix = \"\".join(random.choices(\"0123456789abcdef\", k=randomness))\n    if not deployment_id:\n        # DNS-1035: must start with an alphabetic character\n        if hex_suffix[0].isdigit():\n            hex_suffix = random.choice(\"abcdef\") + hex_suffix[1:]\n        return hex_suffix\n    else:\n        to_take = max_length - randomness - 1\n        return f\"{deployment_id[:to_take]}-{hex_suffix}\"\n\n\ndef _compute_secret_hash(secrets: dict[str, str]) -> str:\n    \"\"\"Compute a deterministic hash of secret contents to trigger rolling updates\"\"\"\n    # Sort keys to ensure consistent hash regardless of dict ordering\n    sorted_items = sorted(secrets.items())\n    content = \"|\".join(f\"{k}={v}\" for k, v in sorted_items)\n    return hashlib.sha256(content.encode()).hexdigest()[:16]\n\n\nasync def find_deployment_id(name: str, force_suffix: bool = False) -> str:\n    max_length = 63  # DNS-1035 label max length\n    deployment_id = name.lower()\n    deployment_id = re.sub(r\"[^a-z0-9]\", \"-\", deployment_id)\n    deployment_id = re.sub(r\"-+\", \"-\", deployment_id)\n    deployment_id = re.sub(r\"^-|-$\", \"\", deployment_id)\n    # DNS-1035: must start with an alphabetic character\n    if deployment_id and not deployment_id[0].isalpha():\n        deployment_id = \"d-\" + deployment_id\n    deployment_id = deployment_id[:max_length].rstrip(\"-\")\n    base_deployment_id = deployment_id\n    if len(deployment_id) < 3 or force_suffix:\n        deployment_id = _append_random_suffix(deployment_id, max_length)\n\n    # Try to find a deployment id that is not in use\n    for i in range(1, 100):\n        if await validate_deployment_id(deployment_id):\n            return deployment_id\n        deployment_id = _append_random_suffix(base_deployment_id, max_length)\n\n    raise ValueError(\n        f\"Deployment id {deployment_id} already in use. Could not find alternative\"\n    )\n\n\n# in order to not conflict in URI routes\nreserved_deployment_ids = [\n    \"validate-repository\",\n    \"list-projects\",\n    \"organizations\",\n    \"version\",\n]\n\n\nasync def create_deployment(\n    project_id: str,\n    display_name: str,\n    repo_url: str,\n    deployment_file_path: str | None = None,\n    git_ref: str | None = None,\n    git_sha: str | None = None,\n    pat: str | None = None,\n    secrets: dict[str, str] | None = None,\n    ui_build_output_path: Path | None = None,\n    image_tag: str | None = None,\n    explicit_id: str | None = None,\n) -> DeploymentResponse:\n    \"\"\"\n    Returns a tuple of a DeploymentResponse and a warning message if there were any issues identified\n    \"\"\"\n\n    deployment_file_path = deployment_file_path or DEFAULT_DEPLOYMENT_FILE_PATH\n\n    if explicit_id is not None:\n        # Already validated by DeploymentCreate schema; check reserved + uniqueness\n        if explicit_id.lower() in reserved_deployment_ids:\n            raise ValueError(\n                f\"Deployment ID {explicit_id!r} is reserved. \"\n                f\"Reserved IDs: {reserved_deployment_ids}\"\n            )\n        if not await validate_deployment_id(explicit_id):\n            raise ValueError(f\"Deployment ID {explicit_id!r} is already in use.\")\n        deployment_id = explicit_id\n    else:\n        is_reserved = display_name.lower() in reserved_deployment_ids\n        deployment_id = await find_deployment_id(display_name, force_suffix=is_reserved)\n\n    # Create the secret first if we have secrets\n    secret_name = None\n    secret_hash = None\n    all_secrets = {**({\"GITHUB_PAT\": pat} if pat else {}), **(secrets or {})}\n    if len(all_secrets) > 0:\n        secret_name = f\"{deployment_id}-secrets\"\n        await _create_k8s_secret(secret_name, all_secrets)\n        secret_hash = _compute_secret_hash(all_secrets)\n\n    # Create the LlamaDeployment custom resource\n    llama_metadata = LlamaDeploymentMetadata(\n        name=deployment_id,\n        namespace=_k8s_client.namespace,\n        annotations={}\n        if not secret_hash\n        else {\"deploy.llamaindex.ai/secret-hash\": secret_hash},\n        labels={\n            \"deploy.llamaindex.ai/project-id\": project_id,\n        },\n    )\n    spec = LlamaDeploymentSpec(\n        displayName=display_name,\n        projectId=project_id,\n        repoUrl=repo_url,\n        gitRef=git_ref,\n        gitSha=git_sha,\n        deploymentFilePath=deployment_file_path,\n        secretName=secret_name,\n        staticAssetsPath=str(ui_build_output_path) if ui_build_output_path else None,\n        imageTag=image_tag,\n    )\n    llamadeployment = {\n        \"apiVersion\": \"deploy.llamaindex.ai/v1\",\n        \"kind\": \"LlamaDeployment\",\n        \"metadata\": llama_metadata.model_dump(),\n        \"spec\": spec.model_dump(),\n    }\n\n    await asyncio.to_thread(\n        _k8s_client.k8s_custom_objects.create_namespaced_custom_object,\n        group=\"deploy.llamaindex.ai\",\n        version=\"v1\",\n        namespace=_k8s_client.namespace,\n        plural=\"llamadeployments\",\n        body=llamadeployment,\n    )\n    logger.info(f\"Created LlamaDeployment: {deployment_id}\")\n\n    # Create ingress if enabled\n    if _k8s_client.enable_ingress:\n        await _create_ingress(deployment_id)\n\n    deployment_response = _llamadeployment_to_response(\n        LlamaDeploymentCRD(\n            metadata=llama_metadata,\n            spec=spec,\n            status=LlamaDeploymentStatus(\n                phase=\"Pending\",\n            ),\n        ),\n        secret_names=list(all_secrets.keys()),\n    )\n    return deployment_response\n\n\nasync def _create_k8s_secret(secret_name: str, secrets: dict[str, str]) -> None:\n    \"\"\"Create or update a Kubernetes secret with the given secrets\"\"\"\n    # Encode secrets as base64 (Kubernetes requirement)\n    encoded_secrets = {}\n    for key, value in secrets.items():\n        encoded_secrets[key] = base64.b64encode(value.encode()).decode()\n\n    secret_manifest = client.V1Secret(\n        api_version=\"v1\",\n        kind=\"Secret\",\n        metadata=client.V1ObjectMeta(name=secret_name, namespace=_k8s_client.namespace),\n        type=\"Opaque\",\n        data=encoded_secrets,\n    )\n\n    try:\n        # Try to get existing secret\n        existing_secret = await asyncio.to_thread(\n            _k8s_client.k8s_core_v1.read_namespaced_secret,\n            name=secret_name,\n            namespace=_k8s_client.namespace,\n        )\n\n        # Update existing secret\n        if (\n            secret_manifest.metadata is not None\n            and existing_secret.metadata is not None\n        ):\n            secret_manifest.metadata.resource_version = (\n                existing_secret.metadata.resource_version\n            )\n        result = await asyncio.to_thread(\n            _k8s_client.k8s_core_v1.replace_namespaced_secret,\n            name=secret_name,\n            namespace=_k8s_client.namespace,\n            body=secret_manifest,\n        )\n        logger.debug(\n            f\"Updated secret: {result.metadata.name if result and result.metadata else 'unknown'}\"\n        )\n\n    except ApiException as e:\n        if e.status == 404:\n            # Secret doesn't exist, create it\n            try:\n                result = await asyncio.to_thread(\n                    _k8s_client.k8s_core_v1.create_namespaced_secret,\n                    namespace=_k8s_client.namespace,\n                    body=secret_manifest,\n                )\n                logger.debug(\n                    f\"Created secret: {result.metadata.name if result and result.metadata else 'unknown'}\"\n                )\n            except ApiException as create_e:\n                logger.error(f\"Failed to create secret {secret_name}: {create_e}\")\n                raise\n        else:\n            # Some other error occurred while trying to read existing secret\n            logger.error(f\"Failed to read existing secret {secret_name}: {e}\")\n            raise\n\n\nasync def _create_ingress(service_name: str) -> None:\n    \"\"\"Create or update an ingress for local development\"\"\"\n    host = f\"{service_name}.{_k8s_client.domain}\"\n\n    ingress_manifest = client.V1Ingress(\n        api_version=\"networking.k8s.io/v1\",\n        kind=\"Ingress\",\n        metadata=client.V1ObjectMeta(\n            name=service_name,\n            namespace=_k8s_client.namespace,\n            annotations={\"kubernetes.io/ingress.class\": \"nginx\"},\n        ),\n        spec=client.V1IngressSpec(\n            rules=[\n                client.V1IngressRule(\n                    host=host,\n                    http=client.V1HTTPIngressRuleValue(\n                        paths=[\n                            client.V1HTTPIngressPath(\n                                path=\"/\",\n                                path_type=\"Prefix\",\n                                backend=client.V1IngressBackend(\n                                    service=client.V1IngressServiceBackend(\n                                        name=service_name,\n                                        port=client.V1ServiceBackendPort(number=80),\n                                    )\n                                ),\n                            )\n                        ]\n                    ),\n                )\n            ]\n        ),\n    )\n\n    try:\n        # Try to get existing ingress\n        existing_ingress = await asyncio.to_thread(\n            _k8s_client.k8s_networking_v1.read_namespaced_ingress,\n            name=service_name,\n            namespace=_k8s_client.namespace,\n        )\n\n        # Update existing ingress\n        if (\n            ingress_manifest.metadata is not None\n            and existing_ingress.metadata is not None\n        ):\n            ingress_manifest.metadata.resource_version = (\n                existing_ingress.metadata.resource_version\n            )\n        result = await asyncio.to_thread(\n            _k8s_client.k8s_networking_v1.replace_namespaced_ingress,\n            name=service_name,\n            namespace=_k8s_client.namespace,\n            body=ingress_manifest,\n        )\n        logger.debug(\n            f\"Updated ingress: {result.metadata.name if result and result.metadata else 'unknown'} at {host}\"\n        )\n\n    except ApiException as e:\n        if e.status == 404:\n            # Ingress doesn't exist, create it\n            try:\n                result = await asyncio.to_thread(\n                    _k8s_client.k8s_networking_v1.create_namespaced_ingress,\n                    namespace=_k8s_client.namespace,\n                    body=ingress_manifest,\n                )\n                logger.info(\n                    f\"Created ingress: {result.metadata.name if result and result.metadata else 'unknown'} at {host}\"\n                )\n            except ApiException as create_e:\n                # Don't fail the whole deployment if ingress creation fails\n                logger.warning(f\"Failed to create ingress {service_name}: {create_e}\")\n        else:\n            # Some other error occurred while trying to read existing ingress\n            logger.warning(f\"Failed to read existing ingress {service_name}: {e}\")\n    except Exception as e:\n        # Don't fail the whole deployment if ingress operations fail\n        logger.warning(f\"Failed to create/update ingress {service_name}: {e}\")\n\n\nasync def delete_deployment(deployment_id: str) -> None:\n    \"\"\"Delete a LlamaDeployment and its associated secret and ingress.\n\n    Raises ApiException for non-404 errors so callers know the delete failed.\n    404 errors are swallowed (idempotent delete — resource already gone).\n    \"\"\"\n    # Delete the ingress if it exists (and if we create them)\n    if _k8s_client.enable_ingress:\n        try:\n            await asyncio.to_thread(\n                _k8s_client.k8s_networking_v1.delete_namespaced_ingress,\n                deployment_id,\n                _k8s_client.namespace,\n            )\n            logger.debug(f\"Deleted ingress: {deployment_id}\")\n        except ApiException as e:\n            if e.status != 404:\n                logger.warning(f\"Failed to delete ingress {deployment_id}: {e}\")\n\n    # Delete the LlamaDeployment (this will trigger the operator to clean up other resources)\n    try:\n        await asyncio.to_thread(\n            _k8s_client.k8s_custom_objects.delete_namespaced_custom_object,\n            group=\"deploy.llamaindex.ai\",\n            version=\"v1\",\n            namespace=_k8s_client.namespace,\n            plural=\"llamadeployments\",\n            name=deployment_id,\n        )\n        logger.info(f\"Deleted LlamaDeployment: {deployment_id}\")\n    except ApiException as e:\n        if e.status == 404:\n            logger.debug(f\"Deployment {deployment_id} already deleted\")\n        else:\n            raise\n\n\nasync def update_deployment(\n    deployment_id: str,\n    update: DeploymentUpdate,\n) -> DeploymentResponse | None:\n    \"\"\"Update an existing LlamaDeployment with the provided changes\n\n    Args:\n        deployment_id: The ID of the deployment to update\n        update: The changes to apply\n\n    Returns a tuple of a DeploymentResponse and a warning message if there were any issues identified\n    \"\"\"\n\n    # Get the existing deployment - fail if it doesn't exist\n    try:\n        existing_deployment = await get_deployment_crd(deployment_id)\n    except ApiException as e:\n        if e.status == 404:\n            return None\n        else:\n            raise\n\n    existing_spec = existing_deployment.spec\n\n    update_result = apply_deployment_update(update, existing_spec)\n\n    # Convert updated spec back to dict for K8s API\n    updated_spec = update_result.updated_spec\n\n    # Handle secret updates if there are any changes\n    if update_result.secret_adds or update_result.secret_removes:\n        secret_name = existing_spec.secretName\n\n        # Lazily create secret if none exists\n        if not secret_name:\n            secret_name = f\"{deployment_id}-secrets\"\n            updated_spec.secretName = secret_name\n\n        existing_secrets = {}\n        try:\n            # Get existing secrets\n            secret = await asyncio.to_thread(\n                _k8s_client.k8s_core_v1.read_namespaced_secret,\n                name=secret_name,\n                namespace=_k8s_client.namespace,\n            )\n\n            if secret.data:\n                for key, value in secret.data.items():\n                    existing_secrets[key] = base64.b64decode(value).decode()\n        except ApiException as e:\n            if e.status != 404:\n                raise\n            # Secret doesn't exist, start with empty secrets\n\n        # Apply secret changes\n        updated_secrets = existing_secrets.copy()\n\n        # Remove secrets\n        for secret_key in update_result.secret_removes:\n            updated_secrets.pop(secret_key, None)\n\n        # Add/update secrets\n        updated_secrets.update(update_result.secret_adds)\n\n        # Update the secret\n        await _create_k8s_secret(secret_name, updated_secrets)\n\n        # Add annotation to trigger rolling update when secrets change\n        secret_hash = _compute_secret_hash(updated_secrets)\n\n        if existing_deployment.metadata.annotations is None:\n            existing_deployment.metadata.annotations = {}\n        existing_deployment.metadata.annotations[\"deploy.llamaindex.ai/secret-hash\"] = (\n            secret_hash\n        )\n\n    # Update the LlamaDeployment CRD\n    existing_deployment.spec = updated_spec\n\n    # Construct the full Kubernetes object with required apiVersion and kind\n    k8s_object = {\n        \"apiVersion\": \"deploy.llamaindex.ai/v1\",\n        \"kind\": \"LlamaDeployment\",\n        **existing_deployment.model_dump(exclude_none=True),\n    }\n\n    await asyncio.to_thread(\n        _k8s_client.k8s_custom_objects.replace_namespaced_custom_object,\n        group=\"deploy.llamaindex.ai\",\n        version=\"v1\",\n        namespace=_k8s_client.namespace,\n        plural=\"llamadeployments\",\n        name=deployment_id,\n        body=k8s_object,\n    )\n    logger.info(f\"Updated LlamaDeployment: {deployment_id}\")\n\n    # Return the updated deployment and any warning\n    updated_deployment = await get_deployment(deployment_id)\n    return updated_deployment\n\n\n@to_async\ndef get_deployment_crd(\n    deployment_id: str,\n) -> LlamaDeploymentCRD:\n    \"\"\"Get the spec of a LlamaDeployment by ID\"\"\"\n    result = _k8s_client.k8s_custom_objects.get_namespaced_custom_object(\n        group=\"deploy.llamaindex.ai\",\n        version=\"v1\",\n        namespace=_k8s_client.namespace,\n        plural=\"llamadeployments\",\n        name=deployment_id,\n    )\n    return LlamaDeploymentCRD.model_validate(result)\n\n\nasync def get_deployment_events(deployment_id: str) -> list[DeploymentEvent]:\n    \"\"\"Get the kubernetes events for a LlamaDeployment by ID\"\"\"\n    result = await asyncio.to_thread(\n        _k8s_client.k8s_core_v1.list_namespaced_event,\n        namespace=_k8s_client.namespace,\n        field_selector=f\"involvedObject.name={deployment_id}\",\n    )\n\n    items: list[CoreV1Event] = result.items\n    return [_event_to_deployment_event(event) for event in items]\n\n\ndef _event_to_deployment_event(event: CoreV1Event) -> DeploymentEvent:\n    return DeploymentEvent(\n        message=event.message,\n        reason=event.reason,\n        type=event.type,\n        first_timestamp=event.first_timestamp,\n        last_timestamp=event.last_timestamp,\n        count=event.count,\n    )\n\n\nasync def get_deployment(deployment_id: str) -> DeploymentResponse | None:\n    \"\"\"Get a single LlamaDeployment by ID\"\"\"\n    try:\n        result = await get_deployment_crd(deployment_id)\n\n        # Get secret names if secret exists\n        secret_names = None\n        secret_name = result.spec.secretName\n        if secret_name:\n            secret_names = await get_secret_names(secret_name)\n\n        return _llamadeployment_to_response(result, secret_names)\n\n    except ApiException as e:\n        if e.status == 404:\n            return None\n        else:\n            raise\n\n\nasync def get_deployment_history(\n    deployment_id: str,\n) -> DeploymentHistoryResponse | None:\n    \"\"\"Return the recorded release history for a deployment from its CRD status.\"\"\"\n    try:\n        result = await get_deployment_crd(deployment_id)\n    except ApiException as e:\n        if e.status == 404:\n            return None\n        else:\n            raise\n\n    status = result.status\n    history_crd = status.releaseHistory if status is not None else None\n    items: list[ReleaseHistoryItem] = []\n    for entry in history_crd or []:\n        # Map camelCase to snake_case response\n        items.append(\n            ReleaseHistoryItem(\n                git_sha=entry.gitSha,\n                image_tag=entry.imageTag,\n                released_at=entry.releasedAt,\n            )\n        )\n    return DeploymentHistoryResponse(deployment_id=deployment_id, history=items)\n\n\nasync def get_deployments(project_id: str) -> List[DeploymentResponse]:\n    \"\"\"Get all LlamaDeployments for a project\"\"\"\n\n    # Use label selector to filter by project ID\n    label_selector = f\"deploy.llamaindex.ai/project-id={project_id}\"\n\n    result = await asyncio.to_thread(\n        _k8s_client.k8s_custom_objects.list_namespaced_custom_object,\n        group=\"deploy.llamaindex.ai\",\n        version=\"v1\",\n        namespace=_k8s_client.namespace,\n        plural=\"llamadeployments\",\n        label_selector=label_selector,\n    )\n\n    items = result.get(\"items\", [])\n    item_crds = [LlamaDeploymentCRD.model_validate(item) for item in items]\n\n    # Collect all unique secret names for batch fetching\n    secret_names_to_fetch = set()\n    for item in item_crds:\n        secret_name = item.spec.secretName\n        if secret_name:\n            secret_names_to_fetch.add(secret_name)\n\n    # Batch fetch secret names\n    secrets_data = await get_secret_names_batch(list(secret_names_to_fetch))\n\n    # Create deployment responses\n    deployments = []\n    for item in item_crds:\n        secret_name = item.spec.secretName\n        secret_names = secrets_data.get(secret_name) if secret_name else None\n        deployments.append(_llamadeployment_to_response(item, secret_names))\n\n    return deployments\n\n\nasync def get_projects_with_deployment_count() -> List[ProjectSummary]:\n    \"\"\"Get all unique projects with their deployment counts\"\"\"\n    try:\n        # Get all LlamaDeployments\n        result = await asyncio.to_thread(\n            _k8s_client.k8s_custom_objects.list_namespaced_custom_object,\n            \"deploy.llamaindex.ai\",\n            \"v1\",\n            _k8s_client.namespace,\n            \"llamadeployments\",\n        )\n\n        # Count deployments by project ID\n        project_counts: dict[str, int] = {}\n        for item in result.get(\"items\", []):\n            project_id = item.get(\"spec\", {}).get(\"projectId\")\n            if project_id:\n                project_counts[project_id] = project_counts.get(project_id, 0) + 1\n\n        # Convert to ProjectSummary objects and sort\n        projects = []\n        for project_id, count in sorted(project_counts.items()):\n            projects.append(\n                ProjectSummary(\n                    project_id=project_id,\n                    project_name=project_id,\n                    deployment_count=count,\n                )\n            )\n\n        return projects\n\n    except ApiException as e:\n        logger.error(f\"Failed to get projects with deployment count: {e}\")\n        return []\n\n\n@to_async\ndef get_secret_names(secret_name: str) -> list[str]:\n    \"\"\"Get the key names from a Kubernetes secret\"\"\"\n    try:\n        secret = _k8s_client.k8s_core_v1.read_namespaced_secret(\n            name=secret_name, namespace=_k8s_client.namespace\n        )\n        return list(secret.data.keys()) if secret.data else []\n    except ApiException as e:\n        if e.status == 404:\n            return []\n        raise\n\n\nasync def get_secret_names_batch(\n    secret_names: list[str],\n) -> dict[str, list[str] | None]:\n    \"\"\"Batch fetch secret names for multiple secrets\"\"\"\n    result: dict[str, list[str] | None] = {}\n\n    async def with_secret_names(secret_name: str) -> tuple[str, list[str] | None]:\n        return secret_name, await get_secret_names(secret_name)\n\n    results = await asyncio.gather(*[with_secret_names(name) for name in secret_names])\n    for secret_name, names in results:\n        result[secret_name] = names\n    return result\n\n\ndef _llamadeployment_to_response(\n    llamadeployment: LlamaDeploymentCRD, secret_names: list[str] | None = None\n) -> DeploymentResponse:\n    \"\"\"Convert a LlamaDeployment custom resource to a DeploymentResponse\"\"\"\n    metadata = llamadeployment.metadata\n    spec = llamadeployment.spec\n    status = llamadeployment.status\n\n    # Try to get the apiserver URL from the service or ingress\n    apiserver_url = None\n    service_name = metadata.name\n    if service_name:\n        if _k8s_client.enable_ingress:\n            # Use ingress URL for local development\n            apiserver_url = f\"http://{service_name}.{_k8s_client.domain}:8090\"\n        else:\n            # Use service URL for cluster access\n            apiserver_url = (\n                f\"http://{service_name}.{_k8s_client.namespace}.svc.cluster.local\"\n            )\n\n    # Check if PAT is configured (stored as GITHUB_PAT in the secret)\n    has_pat = secret_names is not None and \"GITHUB_PAT\" in secret_names\n\n    # Filter out GITHUB_PAT from secret names since we have has_personal_access_token flag\n    filtered_secret_names: list[str] | None = None\n    if secret_names:\n        filtered_secret_names = [name for name in secret_names if name != \"GITHUB_PAT\"]\n        if not filtered_secret_names:\n            filtered_secret_names = None\n\n    # Derive appserver_version from imageTag when tag follows appserver-X.Y.Z convention\n    derived_version = image_tag_to_version(spec.imageTag) if spec.imageTag else None\n\n    # Map internal operator phases to backward-compatible values for old clients\n    # that only know the original LlamaDeploymentPhase Literal values.\n    raw_phase = status.phase or \"Pending\"\n    warning = None\n    if raw_phase == \"Building\":\n        warning = status.message or \"Build phase: Building\"\n        phase: LlamaDeploymentPhase = \"Pending\"\n    elif raw_phase == \"BuildFailed\":\n        warning = status.message or \"Build phase: BuildFailed\"\n        phase = \"Failed\"\n    elif raw_phase == \"AwaitingCode\":\n        warning = status.message or \"Waiting for code push\"\n        phase = \"Pending\"\n    else:\n        phase = cast(LlamaDeploymentPhase, raw_phase)\n\n    return DeploymentResponse(\n        id=metadata.name,\n        display_name=spec.get_display_name(),\n        repo_url=spec.repoUrl,\n        deployment_file_path=spec.deploymentFilePath,\n        git_ref=spec.gitRef,\n        git_sha=spec.gitSha,\n        has_personal_access_token=has_pat,\n        project_id=spec.projectId,\n        secret_names=filtered_secret_names,\n        apiserver_url=HttpUrl(apiserver_url) if apiserver_url else None,\n        status=phase,\n        warning=warning,\n        appserver_version=derived_version,\n        suspended=spec.suspended,\n    )\n\n\nasync def has_deployment_pat(deployment_id: str) -> bool:\n    \"\"\"Check if a deployment has an existing PAT configured.\"\"\"\n    return await get_deployment_pat(deployment_id) is not None\n\n\nasync def get_deployment_pat(deployment_id: str) -> str | None:\n    \"\"\"Get the PAT from a deployment's secret if it exists.\"\"\"\n    try:\n        # Get the deployment\n        deployment = await get_deployment_crd(deployment_id)\n\n        # Get the secret\n        secret_name = deployment.spec.secretName\n        if not secret_name:\n            return None\n\n        secret = await asyncio.to_thread(\n            _k8s_client.k8s_core_v1.read_namespaced_secret,\n            secret_name,\n            _k8s_client.namespace,\n        )\n\n        if secret.data and \"GITHUB_PAT\" in secret.data:\n            return base64.b64decode(secret.data[\"GITHUB_PAT\"]).decode()\n\n        return None\n\n    except ApiException as e:\n        if e.status == 404:\n            return None\n        raise\n    except Exception:\n        return None\n\n\ndef _list_replicasets_for_deployment_sync(deployment_id: str) -> list[Any]:\n    \"\"\"List all ReplicaSets owned by a deployment (sync core).\n\n    Returns an empty list if the deployment is not found.\n    \"\"\"\n    try:\n        deployment = _k8s_client.k8s_apps_v1.read_namespaced_deployment(\n            name=deployment_id, namespace=_k8s_client.namespace\n        )\n    except ApiException as e:\n        if e.status == 404:\n            return []\n        raise\n\n    if not deployment or not deployment.metadata:\n        return []\n\n    deployment_uid = deployment.metadata.uid\n\n    rs_list = _k8s_client.k8s_apps_v1.list_namespaced_replica_set(\n        namespace=_k8s_client.namespace,\n        label_selector=f\"app={deployment_id}\",\n    )\n\n    result = []\n    for rs in rs_list.items or []:\n        if not rs.metadata or not rs.metadata.owner_references:\n            continue\n        for owner in rs.metadata.owner_references:\n            if owner.kind == \"Deployment\" and owner.uid == deployment_uid:\n                result.append(rs)\n                break\n\n    return result\n\n\n@to_async\ndef get_latest_replicaset_for_deployment(\n    deployment_id: str,\n) -> V1ReplicaSet | None:\n    \"\"\"Return the latest ReplicaSet object for a given apps/v1 Deployment name.\n\n    The operator names the Deployment the same as the LlamaDeployment metadata.name.\n    We determine the latest by the highest deployment.kubernetes.io/revision annotation.\n    \"\"\"\n    replicasets = _list_replicasets_for_deployment_sync(deployment_id)\n    if not replicasets:\n        return None\n\n    latest_rs = None\n    latest_revision = -1\n\n    for rs in replicasets:\n        revision_str = None\n        if rs.metadata and rs.metadata.annotations:\n            revision_str = rs.metadata.annotations.get(\n                \"deployment.kubernetes.io/revision\"\n            )\n        try:\n            revision = int(revision_str) if revision_str is not None else 0\n        except ValueError:\n            revision = 0\n\n        if revision > latest_revision:\n            latest_revision = revision\n            latest_rs = rs\n\n    return latest_rs\n\n\n@to_async\ndef list_replicasets_for_deployment(deployment_id: str) -> list[Any]:\n    \"\"\"List all ReplicaSets owned by a deployment.\"\"\"\n    return _list_replicasets_for_deployment_sync(deployment_id)\n\n\n@to_async\ndef list_all_deployments() -> list[Any]:\n    \"\"\"List all Deployments managed by the operator.\"\"\"\n    result = _k8s_client.k8s_apps_v1.list_namespaced_deployment(\n        namespace=_k8s_client.namespace,\n        label_selector=\"app.kubernetes.io/managed-by=llama-deploy-operator\",\n    )\n    return list(result.items)\n\n\n@dataclass\nclass LogLine:\n    pod: str\n    container: str\n    text: str\n    # Make timestamp optional and excluded from equality to simplify tests/consumers\n    timestamp: datetime | None = field(default=None, compare=False)\n\n\nasync def get_replicaset_pods_for_deployment(deployment_id: str) -> list[V1Pod]:\n    \"\"\"Return pods owned by the latest ReplicaSet for the given deployment ID.\"\"\"\n    latest_rs = await get_latest_replicaset_for_deployment(deployment_id)\n    if latest_rs is None or latest_rs.metadata is None:\n        return []\n    rs_uid = latest_rs.metadata.uid\n    if not rs_uid:\n        return []\n\n    pods = await asyncio.to_thread(\n        _k8s_client.k8s_core_v1.list_namespaced_pod,\n        namespace=_k8s_client.namespace,\n        label_selector=f\"app={deployment_id}\",\n    )\n\n    target_pods: list[V1Pod] = []\n    for pod in pods.items or []:\n        if pod.metadata and pod.metadata.owner_references:\n            for owner in pod.metadata.owner_references:\n                if owner.kind == \"ReplicaSet\" and owner.uid == rs_uid:\n                    target_pods.append(pod)\n                    break\n    return target_pods\n\n\nCancelFn = Callable[[], Coroutine[Any, Any, None]]\n\n\nasync def stream_container_logs(\n    pod_name: str,\n    container_name: str,\n    *,\n    since_seconds: int | None = None,\n    tail_lines: int | None = None,\n    follow: bool = True,\n) -> tuple[CancelFn, AsyncGenerator[str, None]]:\n    \"\"\"generator for a single container's logs.\n\n    When ``follow=False``, the underlying K8s read returns the currently\n    buffered log content and the generator ends naturally; no streaming.\n    \"\"\"\n\n    try:\n        read_pod_log = cast(\n            Callable[..., HTTPResponse | str],\n            _k8s_client.k8s_core_v1.read_namespaced_pod_log,\n        )\n        resp = await asyncio.to_thread(\n            read_pod_log,\n            name=pod_name,\n            namespace=_k8s_client.namespace,\n            container=container_name,\n            follow=follow,\n            since_seconds=since_seconds,\n            tail_lines=tail_lines,\n            timestamps=True,\n            _preload_content=False,\n        )\n\n        async def cancel() -> None:\n            if isinstance(resp, HTTPResponse):\n                if not resp.closed:\n                    await asyncio.to_thread(resp.shutdown)\n\n        return cancel, _to_generator(resp)\n    except ApiException as e:\n        # Non-fatal conditions when the container isn't ready yet\n        if e.status in (400, 404):\n            # In non-follow mode, just return an empty generator — the caller\n            # asked for \"what's available now\" and there's nothing yet.\n            if not follow:\n\n                async def empty() -> AsyncGenerator[str, None]:\n                    if False:\n                        yield \"\"  # marker to keep this an async generator\n                    return\n\n                async def noop_cancel() -> None:\n                    return None\n\n                return noop_cancel, empty()\n\n            async def wait_and_retry() -> tuple[CancelFn, AsyncGenerator[str, None]]:\n                await asyncio.sleep(5)\n\n                return await stream_container_logs(\n                    pod_name,\n                    container_name,\n                    since_seconds=since_seconds,\n                    tail_lines=tail_lines,\n                    follow=follow,\n                )\n\n            task = asyncio.create_task(wait_and_retry())\n\n            async def cancel() -> None:\n                if not task.done():\n                    task.cancel()\n                else:\n                    cancel, _ = task.result()\n                    await cancel()\n\n            async def gen() -> AsyncGenerator[str, None]:\n                _, gen = await task\n                async for line in gen:\n                    yield line\n\n            return cancel, gen()\n        raise\n\n\ndef _to_generator(resp: str | HTTPResponse) -> AsyncGenerator[str, None]:\n    # When _preload_content is not provided, the client returns a full string (or blocks with follow=True)\n    # For type-checking simplicity, handle both string and HTTPResponse at runtime.\n    if isinstance(resp, str):\n\n        async def gen_from_str() -> AsyncGenerator[str, None]:\n            for line in resp.splitlines():\n                yield line\n\n        return gen_from_str()\n\n    response: HTTPResponse = resp\n\n    # Use a background thread to read from the blocking HTTPResponse.stream\n    # and push decoded lines into a threadsafe queue consumed by an async generator.\n    q: thread_queue.Queue[str | None] = thread_queue.Queue(maxsize=100)\n    stop_event = threading.Event()\n\n    def reader_thread() -> None:\n        buffer = b\"\"\n        try:\n            for chunk in response.stream(amt=1024, decode_content=False):\n                if stop_event.is_set():\n                    break\n                if not chunk:\n                    continue\n                buffer += chunk\n                while b\"\\n\" in buffer:\n                    line_bytes, buffer = buffer.split(b\"\\n\", 1)\n                    try:\n                        q.put(line_bytes.decode(errors=\"ignore\"), timeout=0.1)\n                    except Exception:\n                        # Drop if the consumer is not keeping up\n                        pass\n        except (AttributeError, ProtocolError):\n            # Connection hung up or response object doesn't support stream\n            pass\n        finally:\n            if buffer:\n                try:\n                    q.put(buffer.decode(errors=\"ignore\"), timeout=0.1)\n                except Exception:\n                    pass\n            # Signal end of stream\n            try:\n                q.put(None, timeout=0.1)\n            except Exception:\n                pass\n\n    t = threading.Thread(target=reader_thread, daemon=True)\n    t.start()\n\n    async def gen_from_http() -> AsyncGenerator[str, None]:\n        try:\n            while True:\n                try:\n                    item = await asyncio.to_thread(q.get, True, 0.5)\n                except Exception:\n                    # Periodically wake to notice cancellation\n                    if stop_event.is_set():\n                        break\n                    continue\n                if item is None:\n                    break\n                yield item\n        except asyncio.CancelledError:\n            stop_event.set()\n            # Best-effort unblock the reader\n            try:\n                await asyncio.to_thread(q.put, None)\n            except Exception:\n                pass\n            raise\n        finally:\n            stop_event.set()\n            try:\n                t.join(timeout=0.2)\n            except Exception:\n                pass\n\n    return gen_from_http()\n\n\n_K8S_TIMESTAMP_RE = re.compile(r\"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{9}Z \")\n\n\nasync def _parse_raw_log_lines(\n    pod_name: str, container_name: str, raw_gen: AsyncGenerator[str, None]\n) -> AsyncGenerator[LogLine, None]:\n    \"\"\"Parse raw K8s log lines (with leading timestamps) into LogLine objects.\"\"\"\n    async for text in raw_gen:\n        timestamp_match = _K8S_TIMESTAMP_RE.match(text)\n        if timestamp_match:\n            raw_ts = timestamp_match.group(0).strip()\n            try:\n                timestamp = datetime.fromisoformat(raw_ts.replace(\"Z\", \"+00:00\"))\n            except Exception:\n                timestamp = datetime.now(timezone.utc)\n            text = text[len(timestamp_match.group(0)) :]\n        else:\n            timestamp = datetime.now(timezone.utc)\n        yield LogLine(\n            pod=pod_name,\n            container=container_name,\n            text=text,\n            timestamp=timestamp,\n        )\n\n\nasync def _stream_pod_container_logs(\n    pod_containers: list[tuple[str, str]],\n    since_seconds: int | None = None,\n    tail_lines: int | None = None,\n    stop_event: asyncio.Event | None = None,\n    follow: bool = True,\n) -> AsyncGenerator[LogLine, None]:\n    \"\"\"Stream and merge log lines from multiple pod/container pairs with shutdown support.\"\"\"\n    generators: list[AsyncGenerator[LogLine, None]] = []\n    cancel_fns: list[CancelFn] = []\n    for pod_name, container_name in pod_containers:\n        cancel, iterator = await stream_container_logs(\n            pod_name,\n            container_name,\n            since_seconds=since_seconds,\n            tail_lines=tail_lines,\n            follow=follow,\n        )\n        cancel_fns.append(cancel)\n        generators.append(_parse_raw_log_lines(pod_name, container_name, iterator))\n\n    async def when_shutdown() -> AsyncGenerator[Literal[\"__SHUTDOWN__\"], None]:\n        if stop_event is None:\n            return\n        await stop_event.wait()\n        yield \"__SHUTDOWN__\"\n        return\n\n    gen_args: list[AsyncGenerator[LogLine | Literal[\"__SHUTDOWN__\"], None]] = [\n        *generators\n    ]\n    if follow:\n        gen_args.append(when_shutdown())\n    merged = merge_generators(*gen_args)\n\n    try:\n        async for item in merged:\n            if item == \"__SHUTDOWN__\":\n                break\n            yield item\n    finally:\n        for cancel in cancel_fns:\n            await cancel()\n\n\nasync def stream_replicaset_logs(\n    deployment_id: str,\n    include_init_containers: bool = False,\n    since_seconds: int | None = None,\n    tail_lines: int | None = None,\n    stop_event: asyncio.Event | None = None,\n    follow: bool = True,\n) -> AsyncGenerator[LogLine, None]:\n    \"\"\"Blocking generator that streams log lines for all pods/containers in the latest ReplicaSet.\n\n    Yields `LogLine` objects until the consumer closes the generator (or, when\n    ``follow=False``, until the underlying K8s reads finish).\n    \"\"\"\n    target_pods = await get_replicaset_pods_for_deployment(deployment_id)\n\n    if not target_pods:\n        return\n\n    pod_containers: list[tuple[str, str]] = []\n    for pod in target_pods:\n        if not pod.metadata or not pod.spec:\n            continue\n        pod_name = pod.metadata.name or \"\"\n        containers = [c.name for c in (pod.spec.containers or [])]\n        if include_init_containers and pod.spec.init_containers:\n            containers.extend(c.name for c in pod.spec.init_containers)\n        for c in containers:\n            pod_containers.append((pod_name, c))\n\n    async for line in _stream_pod_container_logs(\n        pod_containers,\n        since_seconds=since_seconds,\n        tail_lines=tail_lines,\n        stop_event=stop_event,\n        follow=follow,\n    ):\n        yield line\n\n\n@to_async\ndef _list_pods_by_label(label_selector: str) -> list[Any]:\n    \"\"\"List pods matching a label selector.\"\"\"\n    result = _k8s_client.k8s_core_v1.list_namespaced_pod(\n        namespace=_k8s_client.namespace,\n        label_selector=label_selector,\n    )\n    return list(result.items)\n\n\nasync def stream_build_job_logs(\n    deployment_id: str,\n    build_id: str | None = None,\n    since_seconds: int | None = None,\n    tail_lines: int | None = None,\n    stop_event: asyncio.Event | None = None,\n    follow: bool = True,\n) -> AsyncGenerator[LogLine, None]:\n    \"\"\"Stream log lines from a build Job's pod.\n\n    If build_id is not provided, finds the most recent build Job for the deployment.\n    \"\"\"\n    label_selector = f\"deploy.llamaindex.ai/deployment={deployment_id}\"\n    if build_id:\n        label_selector += f\",deploy.llamaindex.ai/build-id={build_id}\"\n\n    pods = await _list_pods_by_label(label_selector)\n\n    if not pods:\n        return\n\n    pod_containers: list[tuple[str, str]] = []\n    for pod in pods:\n        if not pod.metadata or not pod.spec:\n            continue\n        pod_name = pod.metadata.name or \"\"\n        for c in pod.spec.containers or []:\n            pod_containers.append((pod_name, c.name))\n\n    async for line in _stream_pod_container_logs(\n        pod_containers,\n        since_seconds=since_seconds,\n        tail_lines=tail_lines,\n        stop_event=stop_event,\n        follow=follow,\n    ):\n        yield line\n\n\n# === Backup/Restore helpers ===\n\n\n@to_async\ndef get_secret_data(secret_name: str) -> dict[str, str] | None:\n    \"\"\"Read full secret data, base64-decoding values. Returns None if not found.\"\"\"\n    try:\n        secret = _k8s_client.k8s_core_v1.read_namespaced_secret(\n            name=secret_name, namespace=_k8s_client.namespace\n        )\n        if not secret.data:\n            return {}\n        return {k: base64.b64decode(v).decode() for k, v in secret.data.items()}\n    except ApiException as e:\n        if e.status == 404:\n            return None\n        raise\n\n\n@to_async\ndef get_all_deployment_crds() -> list[dict[str, Any]]:\n    \"\"\"List all LlamaDeployment CRDs as raw dicts.\"\"\"\n    result = _k8s_client.k8s_custom_objects.list_namespaced_custom_object(\n        group=\"deploy.llamaindex.ai\",\n        version=\"v1\",\n        namespace=_k8s_client.namespace,\n        plural=\"llamadeployments\",\n    )\n    return list(result.get(\"items\", []))\n\n\nasync def apply_deployment_crd(crd: dict[str, Any]) -> None:\n    \"\"\"Create or replace a LlamaDeployment CRD from a raw dict.\"\"\"\n    name = crd[\"metadata\"][\"name\"]\n    try:\n        await asyncio.to_thread(\n            _k8s_client.k8s_custom_objects.create_namespaced_custom_object,\n            group=\"deploy.llamaindex.ai\",\n            version=\"v1\",\n            namespace=_k8s_client.namespace,\n            plural=\"llamadeployments\",\n            body=crd,\n        )\n        logger.info(\"Created LlamaDeployment from backup: %s\", name)\n    except ApiException as e:\n        if e.status == 409:\n            # Already exists — fetch current resourceVersion and replace\n            existing = await asyncio.to_thread(\n                _k8s_client.k8s_custom_objects.get_namespaced_custom_object,\n                group=\"deploy.llamaindex.ai\",\n                version=\"v1\",\n                namespace=_k8s_client.namespace,\n                plural=\"llamadeployments\",\n                name=name,\n            )\n            crd[\"metadata\"][\"resourceVersion\"] = existing[\"metadata\"][\"resourceVersion\"]\n            await asyncio.to_thread(\n                _k8s_client.k8s_custom_objects.replace_namespaced_custom_object,\n                group=\"deploy.llamaindex.ai\",\n                version=\"v1\",\n                namespace=_k8s_client.namespace,\n                plural=\"llamadeployments\",\n                name=name,\n                body=crd,\n            )\n            logger.info(\"Replaced LlamaDeployment from backup: %s\", name)\n        else:\n            raise\n\n\nasync def apply_secret(name: str, data: dict[str, str]) -> None:\n    \"\"\"Create or replace a K8s secret with pre-decoded string values.\"\"\"\n    await _create_k8s_secret(name, data)\n\n\ndef get_namespace() -> str:\n    \"\"\"Return the namespace the control plane is running in.\"\"\"\n    return _k8s_client.namespace\n\n\n@to_async\ndef get_deployment_crd_raw(name: str) -> dict[str, Any] | None:\n    \"\"\"Get a LlamaDeployment CR as a raw dict, or None if not found.\"\"\"\n    try:\n        return _k8s_client.k8s_custom_objects.get_namespaced_custom_object(\n            group=\"deploy.llamaindex.ai\",\n            version=\"v1\",\n            namespace=_k8s_client.namespace,\n            plural=\"llamadeployments\",\n            name=name,\n        )\n    except ApiException as e:\n        if e.status == 404:\n            return None\n        raise\n\n\n@to_async\ndef delete_deployment_crd(name: str) -> None:\n    \"\"\"Delete a LlamaDeployment CR by name.\"\"\"\n    _k8s_client.k8s_custom_objects.delete_namespaced_custom_object(\n        group=\"deploy.llamaindex.ai\",\n        version=\"v1\",\n        namespace=_k8s_client.namespace,\n        plural=\"llamadeployments\",\n        name=name,\n    )\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/lifecycle.py",
    "content": "import asyncio\n\n# Global shutdown event used to signal long-running operations to stop\nshutdown_event = asyncio.Event()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/log_config.py",
    "content": "import logging\nimport sys\nfrom typing import Any, Literal\n\nfrom llama_agents.core._compat import get_logging_level_mapping\nfrom pythonjsonlogger.json import JsonFormatter\n\n\nclass UvicornStyleFormatter(logging.Formatter):\n    \"\"\"Formatter that mimics uvicorn's beautiful colored style\"\"\"\n\n    # ANSI color codes\n    COLORS = {\n        \"DEBUG\": \"\\033[36m\",  # Cyan\n        \"INFO\": \"\\033[32m\",  # Green\n        \"WARNING\": \"\\033[33m\",  # Yellow\n        \"ERROR\": \"\\033[31m\",  # Red\n        \"CRITICAL\": \"\\033[35m\",  # Magenta\n    }\n    RESET = \"\\033[0m\"\n    BOLD = \"\\033[1m\"\n\n    def format(self, record: logging.LogRecord) -> str:\n        # Get color for log level\n        level_color = self.COLORS.get(record.levelname, \"\")\n\n        # Format like uvicorn: \"INFO:     message\"\n        formatted = (\n            f\"{level_color}{record.levelname:<8}{self.RESET} {record.getMessage()}\"\n        )\n\n        return formatted\n\n\nclass CleanJsonFormatter(JsonFormatter):\n    \"\"\"JSON formatter that excludes redundant fields\"\"\"\n\n    def add_fields(\n        self,\n        log_data: dict[str, Any],\n        record: logging.LogRecord,\n        message_dict: dict[str, Any],\n    ) -> None:\n        super().add_fields(log_data, record, message_dict)\n        # Remove redundant fields\n        log_data.pop(\"color_message\", None)\n\n\ndef setup_logging(\n    log_level: str = \"info\",\n    log_format: Literal[\"standard\", \"json\"] = \"standard\",\n) -> None:\n    \"\"\"Configure application logging\"\"\"\n    level_mapping = get_logging_level_mapping()\n    level = level_mapping[log_level.upper()]\n\n    if log_format == \"json\":\n        # Configure JSON for application logs\n        handler = logging.StreamHandler(sys.stdout)\n        handler.setFormatter(\n            CleanJsonFormatter(\"%(asctime)s %(levelname)s %(name)s %(message)s\")\n        )\n        logging.basicConfig(level=level, handlers=[handler], force=True)\n    else:\n        # Use uvicorn-style formatting for standard format\n        handler = logging.StreamHandler(sys.stdout)\n        handler.setFormatter(UvicornStyleFormatter())\n        logging.basicConfig(level=level, handlers=[handler], force=True)\n\n\ndef get_uvicorn_log_config(log_level: str = \"info\") -> dict:\n    \"\"\"Strip uvicorn's default handlers so all logs flow through the root logger\n    configured by ``setup_logging``.\"\"\"\n    return {\n        \"version\": 1,\n        \"disable_existing_loggers\": False,\n        \"loggers\": {\n            \"uvicorn\": {\"handlers\": [], \"level\": log_level.upper()},\n            \"uvicorn.error\": {\"handlers\": [], \"level\": log_level.upper()},\n            \"uvicorn.access\": {\"handlers\": [], \"level\": log_level.upper()},\n        },\n    }\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/main.py",
    "content": "import argparse\nimport asyncio\nimport logging\nimport subprocess\nimport sys\nfrom typing import Literal\n\nimport uvicorn\nfrom dotenv import load_dotenv\n\nfrom .log_config import get_uvicorn_log_config, setup_logging\n\nlogger = logging.getLogger(__name__)\n\n\ndef run_server_subprocess(\n    app_path: str,\n    host: str,\n    port: int,\n    name: str,\n    log_level: str,\n    access_log: bool,\n) -> subprocess.Popen:\n    \"\"\"Run a server with uvicorn in a subprocess with reload\"\"\"\n    logger.info(f\"Starting {name} server on {host}:{port} with reload\")\n\n    cmd = [\n        sys.executable,\n        \"-m\",\n        \"uvicorn\",\n        app_path,\n        \"--host\",\n        host,\n        \"--port\",\n        str(port),\n        \"--reload\",\n        \"--log-level\",\n        log_level.lower(),\n    ]\n    if not access_log:\n        cmd.append(\"--no-access-log\")\n\n    return subprocess.Popen(cmd)\n\n\nasync def run_server_async(\n    app_path: str,\n    host: str,\n    port: int,\n    name: str,\n    log_level: str,\n    access_log: bool,\n    log_format: Literal[\"standard\", \"json\"] = \"standard\",\n) -> None:\n    \"\"\"Run a server with uvicorn async\"\"\"\n    logger.info(f\"Starting {name} server on {host}:{port}\")\n\n    config = uvicorn.Config(\n        app=app_path,\n        host=host,\n        port=port,\n        log_level=log_level.lower(),\n        access_log=access_log,\n        reload=False,\n        log_config=get_uvicorn_log_config(log_level),\n    )\n    server = uvicorn.Server(config)\n    await asyncio.create_task(server.serve())\n\n\nasync def run_servers(\n    host: str = \"0.0.0.0\",\n    manage_api_port: int = 8000,\n    build_api_port: int = 8001,\n    reload: bool = False,\n    log_level: str = \"info\",\n    log_format: Literal[\"standard\", \"json\"] = \"standard\",\n    access_log: bool = True,\n) -> None:\n    \"\"\"Run both main API and build API servers concurrently\"\"\"\n    setup_logging(log_level, log_format)\n    load_dotenv()\n\n    reload_str = \" with auto-reload\" if reload else \"\"\n    logger.info(\n        f\"Running servers on {host}:{manage_api_port} and {host}:{build_api_port}{reload_str}\"\n    )\n\n    if reload:\n        # Start both servers as subprocesses with reload\n        cp = run_server_subprocess(\n            app_path=\"llama_agents.control_plane.manage_api.manage_app:app\",\n            host=host,\n            port=manage_api_port,\n            name=\"Control Plane\",\n            log_level=log_level,\n            access_log=access_log,\n        )\n        ba = run_server_subprocess(\n            app_path=\"llama_agents.control_plane.build_api.build_app:build_app\",\n            host=host,\n            port=build_api_port,\n            name=\"Build API\",\n            log_level=log_level,\n            access_log=access_log,\n        )\n        # Wait for both processes\n        cp.wait()\n        ba.wait()\n    else:\n        # Start both servers as async tasks\n        cp_task = run_server_async(\n            app_path=\"llama_agents.control_plane.manage_api.manage_app:app\",\n            host=host,\n            port=manage_api_port,\n            name=\"Control Plane\",\n            log_level=log_level,\n            access_log=access_log,\n            log_format=log_format,\n        )\n        ba_task = run_server_async(\n            app_path=\"llama_agents.control_plane.build_api.build_app:build_app\",\n            host=host,\n            port=build_api_port,\n            name=\"Build API\",\n            log_level=log_level,\n            access_log=access_log,\n            log_format=log_format,\n        )\n        await asyncio.gather(cp_task, ba_task)\n\n\ndef main() -> None:\n    \"\"\"Main entry point for running both servers\"\"\"\n\n    parser = argparse.ArgumentParser(description=\"Run the control plane\")\n    parser.add_argument(\n        \"--host\", type=str, default=\"0.0.0.0\", help=\"Host to run the servers on\"\n    )\n    parser.add_argument(\n        \"--manage-api-port\",\n        type=int,\n        default=8000,\n        help=\"Port to run the manage API on\",\n    )\n    parser.add_argument(\n        \"--build-api-port\", type=int, default=8001, help=\"Port to run the build API on\"\n    )\n    parser.add_argument(\n        \"--reload\",\n        action=\"store_true\",\n        help=\"Reload the servers on code changes (for development)\",\n    )\n    parser.add_argument(\n        \"--log-level\",\n        type=str,\n        default=\"info\",\n        help=\"Log level to run the servers at\",\n    )\n    parser.add_argument(\n        \"--log-format\",\n        type=str,\n        default=\"standard\",\n        help=\"Log format to use (standard or json)\",\n    )\n    parser.add_argument(\n        \"--no-access-log\",\n        action=\"store_true\",\n        help=\"Disable access log for both servers\",\n    )\n    args = parser.parse_args()\n\n    try:\n        asyncio.run(\n            run_servers(\n                host=args.host,\n                manage_api_port=args.manage_api_port,\n                build_api_port=args.build_api_port,\n                reload=args.reload,\n                log_level=args.log_level,\n                log_format=args.log_format,\n                access_log=not args.no_access_log,\n            )\n        )\n    except KeyboardInterrupt:\n        logger.info(\"Received interrupt signal, exiting...\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/manage_api/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/manage_api/backup_service.py",
    "content": "\"\"\"Concrete backup/restore service implementation.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom datetime import datetime, timezone\n\nfrom llama_agents.core import schema\n\nfrom .. import k8s_client\nfrom ..backup.archive import (\n    BackupContents,\n    clean_crd_metadata,\n    create_backup_archive,\n    read_backup_archive,\n)\nfrom ..backup.storage import BackupInfo, S3BackupStorage, generate_backup_id\nfrom ..settings import settings\n\nlogger = logging.getLogger(__name__)\n\n\ndef _paired_secret_name(deployment_name: str) -> str:\n    return f\"{deployment_name}-secrets\"\n\n\nclass BackupService:\n    def __init__(self, storage: S3BackupStorage) -> None:\n        self._storage = storage\n\n    async def create_backup(self) -> schema.BackupResponse:\n        backup_id = generate_backup_id()\n        return await self._perform_backup(backup_id)\n\n    async def _perform_backup(self, backup_id: str) -> schema.BackupResponse:\n        try:\n            timestamp = datetime.now(timezone.utc).isoformat()\n\n            # Fetch all CRDs\n            raw_crds = await k8s_client.get_all_deployment_crds()\n\n            # Clean metadata and collect generations\n            cleaned_crds: list[dict] = []\n            generations: dict[str, int] = {}\n            names: list[str] = []\n\n            for crd in raw_crds:\n                name = crd.get(\"metadata\", {}).get(\"name\", \"\")\n                names.append(name)\n                gen = crd.get(\"metadata\", {}).get(\"generation\")\n                if gen is not None:\n                    generations[name] = int(gen)\n\n                cleaned = clean_crd_metadata(crd)\n                cleaned_crds.append(cleaned)\n\n            # Fetch secrets concurrently\n            secret_results = await asyncio.gather(\n                *(k8s_client.get_secret_data(_paired_secret_name(n)) for n in names)\n            )\n            secrets: dict[str, dict[str, str]] = {}\n            for name, secret_data in zip(names, secret_results):\n                if secret_data is not None:\n                    secrets[name] = secret_data\n\n            # Create archive\n            namespace = k8s_client.get_namespace()\n            archive_data = create_backup_archive(\n                deployments=cleaned_crds,\n                secrets=secrets,\n                namespace=namespace,\n                timestamp=timestamp,\n                encryption_password=settings.backup_encryption_password,\n                generations=generations,\n            )\n\n            # Upload to S3\n            await self._storage.upload(backup_id, archive_data)\n\n            return schema.BackupResponse(\n                backup_id=backup_id,\n                status=\"completed\",\n                timestamp=datetime.fromisoformat(timestamp),\n                deployment_count=len(cleaned_crds),\n                size_bytes=len(archive_data),\n            )\n        except Exception as e:\n            logger.exception(\"Backup %s failed\", backup_id)\n            return schema.BackupResponse(\n                backup_id=backup_id,\n                status=\"failed\",\n                error=str(e),\n            )\n\n    async def list_backups(self) -> schema.BackupListResponse:\n        infos = await self._storage.list_backups()\n        return schema.BackupListResponse(\n            backups=[self._info_to_response(info) for info in infos]\n        )\n\n    async def get_backup(self, backup_id: str) -> schema.BackupResponse:\n        info = await self._storage.get_info(backup_id)\n        if info is not None:\n            return self._info_to_response(info)\n        return schema.BackupResponse(\n            backup_id=backup_id,\n            status=\"failed\",\n            error=\"Backup not found\",\n        )\n\n    async def delete_backup(self, backup_id: str) -> schema.BackupResponse:\n        if await self._storage.get_info(backup_id) is None:\n            return schema.BackupResponse(\n                backup_id=backup_id,\n                status=\"failed\",\n                error=\"Backup not found\",\n            )\n        await self._storage.delete(backup_id)\n        return schema.BackupResponse(\n            backup_id=backup_id,\n            status=\"deleted\",\n        )\n\n    async def restore_backup(\n        self, request: schema.RestoreRequest\n    ) -> schema.RestoreResponse:\n        return await self._perform_restore(request)\n\n    async def _perform_restore(\n        self, request: schema.RestoreRequest\n    ) -> schema.RestoreResponse:\n        try:\n            # Download archive\n            archive_data = await self._storage.download(request.backup_id)\n            contents = read_backup_archive(\n                archive_data, settings.backup_encryption_password\n            )\n\n            # Safety check: refuse include_deletions with empty backup\n            if request.include_deletions and len(contents.entries) == 0:\n                return schema.RestoreResponse(\n                    backup_id=request.backup_id,\n                    status=\"failed\",\n                    error=\"Refusing to delete all deployments: backup contains zero entries\",\n                )\n\n            results = await self._restore_entries(contents, request)\n\n            # Handle deletions\n            if request.include_deletions:\n                deletion_results = await self._handle_deletions(contents)\n                results.extend(deletion_results)\n\n            return schema.RestoreResponse(\n                backup_id=request.backup_id,\n                status=\"completed\",\n                results=results,\n            )\n        except Exception as e:\n            logger.exception(\"Restore from %s failed\", request.backup_id)\n            return schema.RestoreResponse(\n                backup_id=request.backup_id,\n                status=\"failed\",\n                error=str(e),\n            )\n\n    async def _restore_entries(\n        self,\n        contents: BackupContents,\n        request: schema.RestoreRequest,\n    ) -> list[schema.RestoreDeploymentResult]:\n        results: list[schema.RestoreDeploymentResult] = []\n\n        for entry in contents.entries:\n            try:\n                existing = await k8s_client.get_deployment_crd_raw(entry.name)\n\n                if existing is not None:\n                    # Safety: refuse to overwrite a deployment belonging to a different project\n                    existing_project = existing.get(\"spec\", {}).get(\"projectId\")\n                    entry_project = entry.cr.get(\"spec\", {}).get(\"projectId\")\n                    if (\n                        existing_project\n                        and entry_project\n                        and existing_project != entry_project\n                    ):\n                        results.append(\n                            schema.RestoreDeploymentResult(\n                                name=entry.name,\n                                action=\"failed\",\n                                error=f\"Project mismatch: existing deployment belongs to project \"\n                                f\"'{existing_project}', backup entry belongs to '{entry_project}'\",\n                            )\n                        )\n                        continue\n\n                    if request.conflict_mode == \"skip\":\n                        results.append(\n                            schema.RestoreDeploymentResult(\n                                name=entry.name, action=\"skipped\"\n                            )\n                        )\n                        continue\n\n                    if request.conflict_mode == \"overwrite-if-newer\":\n                        existing_gen = existing.get(\"metadata\", {}).get(\"generation\")\n                        if (\n                            existing_gen is not None\n                            and entry.generation is not None\n                            and int(existing_gen) > entry.generation\n                        ):\n                            results.append(\n                                schema.RestoreDeploymentResult(\n                                    name=entry.name, action=\"skipped\"\n                                )\n                            )\n                            continue\n\n                # Apply secret first (if present)\n                if entry.secret is not None:\n                    secret_name = _paired_secret_name(entry.name)\n                    await k8s_client.apply_secret(secret_name, entry.secret)\n\n                # Apply CRD\n                await k8s_client.apply_deployment_crd(entry.cr)\n\n                action = \"updated\" if existing is not None else \"created\"\n                results.append(\n                    schema.RestoreDeploymentResult(name=entry.name, action=action)\n                )\n\n            except Exception as e:\n                logger.error(\"Failed to restore deployment %s: %s\", entry.name, e)\n                results.append(\n                    schema.RestoreDeploymentResult(\n                        name=entry.name, action=\"failed\", error=str(e)\n                    )\n                )\n\n        return results\n\n    async def _handle_deletions(\n        self, contents: BackupContents\n    ) -> list[schema.RestoreDeploymentResult]:\n        \"\"\"Delete deployments present in cluster but absent from backup.\"\"\"\n        results: list[schema.RestoreDeploymentResult] = []\n        backup_names = {entry.name for entry in contents.entries}\n\n        current_crds = await k8s_client.get_all_deployment_crds()\n        for crd in current_crds:\n            name = crd.get(\"metadata\", {}).get(\"name\", \"\")\n            if name not in backup_names:\n                try:\n                    await k8s_client.delete_deployment_crd(name)\n                    results.append(\n                        schema.RestoreDeploymentResult(name=name, action=\"deleted\")\n                    )\n                except Exception as e:\n                    logger.error(\"Failed to delete deployment %s: %s\", name, e)\n                    results.append(\n                        schema.RestoreDeploymentResult(\n                            name=name, action=\"failed\", error=str(e)\n                        )\n                    )\n\n        return results\n\n    @staticmethod\n    def _info_to_response(info: BackupInfo) -> schema.BackupResponse:\n        return schema.BackupResponse(\n            backup_id=info.backup_id,\n            status=\"completed\",\n            timestamp=info.timestamp,\n            size_bytes=info.size_bytes,\n        )\n\n\ndef create_backup_service() -> BackupService | None:\n    \"\"\"Create a BackupService if S3 is configured, else return None.\"\"\"\n    if not settings.s3_bucket:\n        return None\n\n    storage = S3BackupStorage(\n        bucket=settings.s3_bucket,\n        endpoint_url=settings.s3_endpoint_url,\n        region=settings.s3_region,\n        access_key=settings.s3_access_key,\n        secret_key=settings.s3_secret_key,\n        key_prefix=settings.backup_s3_key_prefix,\n        unsigned=settings.s3_unsigned,\n    )\n    return BackupService(storage)\n\n\nbackup_service = create_backup_service()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/manage_api/backup_v1beta1.py",
    "content": "\"\"\"Backup/restore API router (v1beta1).\"\"\"\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom llama_agents.core import schema\n\nfrom .backup_service import BackupService, backup_service\n\nbase_router = APIRouter(prefix=\"/api/v1beta1\")\n_router = APIRouter(prefix=\"/backups\")\n\n\ndef _require_service() -> BackupService:\n    if backup_service is None:\n        raise HTTPException(\n            status_code=503,\n            detail=\"Backup service not configured. Set BACKUP_S3_BUCKET to enable.\",\n        )\n    return backup_service\n\n\n@_router.post(\"\", response_model=schema.BackupResponse)\nasync def create_backup(\n    service: BackupService = Depends(_require_service),\n) -> schema.BackupResponse:\n    return await service.create_backup()\n\n\n@_router.get(\"\", response_model=schema.BackupListResponse)\nasync def list_backups(\n    service: BackupService = Depends(_require_service),\n) -> schema.BackupListResponse:\n    return await service.list_backups()\n\n\n@_router.get(\"/{backup_id}\", response_model=schema.BackupResponse)\nasync def get_backup(\n    backup_id: str,\n    service: BackupService = Depends(_require_service),\n) -> schema.BackupResponse:\n    return await service.get_backup(backup_id)\n\n\n@_router.delete(\"/{backup_id}\", response_model=schema.BackupResponse)\nasync def delete_backup(\n    backup_id: str,\n    service: BackupService = Depends(_require_service),\n) -> schema.BackupResponse:\n    return await service.delete_backup(backup_id)\n\n\n@_router.post(\"/restore\", response_model=schema.RestoreResponse)\nasync def restore_backup(\n    request: schema.RestoreRequest,\n    service: BackupService = Depends(_require_service),\n) -> schema.RestoreResponse:\n    return await service.restore_backup(request)\n\n\nbase_router.include_router(_router)\nrouter = base_router\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/manage_api/deployments_service.py",
    "content": "import asyncio\nimport logging\nfrom collections.abc import AsyncIterator\nfrom datetime import datetime, timezone\nfrom importlib.metadata import version as pkg_version\nfrom typing import AsyncGenerator, Literal, cast\n\nfrom fastapi import HTTPException, Request\nfrom fastapi.responses import Response\nfrom llama_agents.control_plane import k8s_client\nfrom llama_agents.control_plane.build_api.build_gc import (\n    delete_all_artifacts_for_deployment,\n)\nfrom llama_agents.control_plane.code_repo.git_server import (\n    handle_git_request as _handle_git_request,\n)\nfrom llama_agents.control_plane.code_repo.service import code_repo_storage\nfrom llama_agents.control_plane.git import git_service\nfrom llama_agents.control_plane.lifecycle import shutdown_event\nfrom llama_agents.control_plane.settings import settings\nfrom llama_agents.core import schema\nfrom llama_agents.core.iter_utils import (\n    debounced_sorted_prefix,\n    merge_generators,\n)\nfrom llama_agents.core.schema import LogEvent\nfrom llama_agents.core.schema.deployments import (\n    INTERNAL_CODE_REPO_SCHEME,\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    DeploymentUpdate,\n    RollbackRequest,\n    version_to_image_tag,\n)\nfrom llama_agents.core.server.manage_api import (\n    AbstractDeploymentsService,\n    AbstractPublicDeploymentsService,\n    DeploymentNotFoundError,\n)\nfrom overrides import override\n\nlogger = logging.getLogger(__name__)\n\n\nDEFAULT_ORG = schema.OrgSummary(org_id=\"default\", org_name=\"Default\", is_default=True)\n\n\nasync def _on_push_complete(\n    deployment_id: str,\n    new_sha: str | None,\n    git_ref: str | None,\n) -> None:\n    if new_sha is None:\n        logger.warning(\n            \"Push to deployment %s completed but could not determine HEAD SHA\",\n            deployment_id,\n        )\n        return\n\n    # Only update the CRD on the first push — when the deployment\n    # doesn't yet have an internal repo_url or is missing a git_sha.\n    # Subsequent pushes just upload code to S3; the user must use\n    # `llamactl deploy update` to explicitly advance the ref/sha.\n    current = await k8s_client.get_deployment(deployment_id)\n    if (\n        current is not None\n        and current.repo_url == INTERNAL_CODE_REPO_SCHEME\n        and current.git_sha\n    ):\n        logger.info(\n            \"Push to deployment %s uploaded to S3; skipping CRD update \"\n            \"(already has repo_url and git_sha)\",\n            deployment_id,\n        )\n        return\n\n    logger.info(\n        \"First push to deployment %s: setting sha=%s ref=%s\",\n        deployment_id,\n        new_sha,\n        git_ref,\n    )\n    update = DeploymentUpdate(\n        repo_url=INTERNAL_CODE_REPO_SCHEME,\n        git_sha=new_sha,\n        git_ref=git_ref,\n    )\n    result = await k8s_client.update_deployment(\n        deployment_id=deployment_id,\n        update=update,\n    )\n    if result is None:\n        logger.error(\n            \"Failed to update deployment %s after push (not found)\",\n            deployment_id,\n        )\n\n\nclass PublicDeploymentService(AbstractPublicDeploymentsService):\n    @override\n    async def get_version(self) -> schema.VersionResponse:\n        capabilities: list[schema.Capability] = [schema.Capabilities.ORGANIZATIONS]\n        if code_repo_storage is not None:\n            capabilities.append(schema.Capabilities.CODE_PUSH)\n        return schema.VersionResponse(\n            version=pkg_version(\"llama-agents-control-plane\"),\n            requires_auth=False,\n            min_llamactl_version=\"0.3.0a13\",\n            capabilities=capabilities,\n        )\n\n\nclass DeploymentService(AbstractDeploymentsService):\n    async def _get_deployment_or_raise(\n        self, project_id: str, deployment_id: str, include_events: bool = False\n    ) -> DeploymentResponse:\n        deployment = await k8s_client.get_deployment(deployment_id)\n        if deployment is None or deployment.project_id != project_id:\n            raise DeploymentNotFoundError(\n                f\"Deployment with id {deployment_id} not found\"\n            )\n        if include_events:\n            deployment.events = await k8s_client.get_deployment_events(deployment_id)\n        return deployment\n\n    @override\n    async def get_organizations(self) -> schema.OrganizationsListResponse:\n        return schema.OrganizationsListResponse(organizations=[DEFAULT_ORG])\n\n    @override\n    async def get_projects(\n        self, org_id: str | None = None\n    ) -> schema.ProjectsListResponse:\n        return schema.ProjectsListResponse(\n            projects=await k8s_client.get_projects_with_deployment_count()\n        )\n\n    @override\n    async def validate_repository(\n        self,\n        project_id: str,\n        request: schema.RepositoryValidationRequest,\n    ) -> schema.RepositoryValidationResponse:\n        \"\"\"Validate repository access and return unified response.\"\"\"\n        return await git_service.validate_repository(\n            repository_url=request.repository_url,\n            project_id=project_id,\n            deployment_id=request.deployment_id,\n            pat=request.pat,\n        )\n\n    @override\n    async def create_deployment(\n        self,\n        project_id: str,\n        deployment_data: schema.DeploymentCreate,\n    ) -> DeploymentResponse:\n        # Skip git validation for empty repo_url (pending deployment, code will be pushed later)\n        if not deployment_data.repo_url and code_repo_storage is None:\n            raise HTTPException(\n                status_code=503,\n                detail=\"Code repo storage not configured (S3_BUCKET not set).\",\n            )\n\n        if deployment_data.repo_url:\n            validated = await git_service.validate_git_application(\n                repository_url=deployment_data.repo_url,\n                git_ref=deployment_data.git_ref,\n                deployment_file_path=deployment_data.deployment_file_path,\n                pat=deployment_data.personal_access_token,\n            )\n        else:\n            validated = None\n\n        requested_version = deployment_data.appserver_version\n        if not settings.should_stamp_image_tag:\n            # Defer to operator's LLAMA_DEPLOY_IMAGE_TAG env var\n            requested_image_tag = None\n        elif requested_version:\n            requested_image_tag = version_to_image_tag(requested_version)\n        elif settings.default_appserver_image_tag:\n            requested_image_tag = settings.default_appserver_image_tag\n        else:\n            requested_image_tag = None\n\n        deployment_response = await k8s_client.create_deployment(\n            project_id=project_id,\n            display_name=deployment_data.display_name or \"\",\n            repo_url=deployment_data.repo_url,\n            deployment_file_path=(\n                deployment_data.deployment_file_path\n                or (validated.valid_deployment_file_path if validated else None)\n            ),\n            git_ref=(\n                deployment_data.git_ref or (validated.git_ref if validated else None)\n            ),\n            git_sha=validated.git_sha if validated else None,\n            pat=deployment_data.personal_access_token,\n            secrets=deployment_data.secrets,\n            ui_build_output_path=validated.ui_build_output_path if validated else None,\n            image_tag=requested_image_tag,\n            explicit_id=deployment_data.id,\n        )\n\n        # Propagate version in response for clients\n        deployment_response.appserver_version = requested_version\n\n        # Return deployment response with warning header if there are git issues\n        if validated is not None and validated.error_message:\n            deployment_response.warning = validated.error_message\n        return deployment_response\n\n    @override\n    async def get_deployments(\n        self,\n        project_id: str,\n    ) -> schema.DeploymentsListResponse:\n        deployments = await k8s_client.get_deployments(project_id=project_id)\n        return schema.DeploymentsListResponse(deployments=deployments)\n\n    @override\n    async def get_deployment(\n        self,\n        project_id: str,\n        deployment_id: str,\n        include_events: bool = False,\n    ) -> DeploymentResponse:\n        return await self._get_deployment_or_raise(\n            project_id, deployment_id, include_events\n        )\n\n    @override\n    async def delete_deployment(\n        self,\n        project_id: str,\n        deployment_id: str,\n    ) -> None:\n        await self._get_deployment_or_raise(project_id, deployment_id)\n        await k8s_client.delete_deployment(deployment_id=deployment_id)\n        await delete_all_artifacts_for_deployment(deployment_id)\n        if code_repo_storage is not None:\n            await code_repo_storage.delete_repo(deployment_id)\n\n    async def get_deployment_history(\n        self, project_id: str, deployment_id: str\n    ) -> DeploymentHistoryResponse:\n        await self._get_deployment_or_raise(project_id, deployment_id)\n        result = await k8s_client.get_deployment_history(deployment_id)\n        if result is None:\n            raise DeploymentNotFoundError(\n                f\"Deployment with id {deployment_id} not found\"\n            )\n        return result\n\n    async def rollback_deployment(\n        self, project_id: str, deployment_id: str, request: RollbackRequest\n    ) -> DeploymentResponse:\n        \"\"\"Rollback by updating the CRD's git_ref/git_sha to a previous sha.\"\"\"\n        await self._get_deployment_or_raise(project_id, deployment_id)\n        # Determine imageTag to restore: use explicit override, or look up from history\n        if not settings.should_stamp_image_tag:\n            # Defer to operator's LLAMA_DEPLOY_IMAGE_TAG env var\n            image_tag = None\n        else:\n            image_tag = request.image_tag\n            if image_tag is None:\n                history = await k8s_client.get_deployment_history(deployment_id)\n                if history is not None:\n                    for item in history.history:\n                        if item.git_sha == request.git_sha and item.image_tag:\n                            image_tag = item.image_tag\n                            break\n\n        # Build an update to set git_sha (and clear git_ref to pin exact commit)\n        update = schema.DeploymentUpdate(\n            git_ref=None,\n            git_sha=request.git_sha,\n            image_tag=image_tag,\n        )\n        updated = await k8s_client.update_deployment(\n            deployment_id=deployment_id, update=update\n        )\n        if updated is None:\n            raise DeploymentNotFoundError(\n                f\"Deployment with id {deployment_id} not found\"\n            )\n        return updated\n\n    @override\n    async def update_deployment(\n        self,\n        project_id: str,\n        deployment_id: str,\n        update_data: schema.DeploymentUpdate,\n    ) -> DeploymentResponse:\n        \"\"\"Update an existing deployment with patch-style changes\n\n        Args:\n            project_id: The project ID\n            deployment_id: The deployment ID to update\n            update_data: The patch-style update data\n        \"\"\"\n\n        current_deployment = await self._get_deployment_or_raise(\n            project_id, deployment_id\n        )\n\n        if not settings.should_stamp_image_tag:\n            # Defer to operator's LLAMA_DEPLOY_IMAGE_TAG env var\n            # Local-dev only: this does not clear an already pinned CRD spec.imageTag.\n            # Existing dev deployments may need one-time recreation/manual field clear.\n            update_data.appserver_version = None\n            update_data.image_tag = None\n        elif (\n            update_data.bump_to_latest_appserver\n            and not update_data.appserver_version\n            and not update_data.image_tag\n        ):\n            update_data.image_tag = settings.default_appserver_image_tag\n\n        # If the client sends an empty repo_url but the deployment already uses\n        # the internal code repo, drop the field so we don't overwrite it.\n        if (\n            update_data.repo_url is not None\n            and update_data.repo_url.strip() == \"\"\n            and current_deployment.repo_url == INTERNAL_CODE_REPO_SCHEME\n        ):\n            update_data.repo_url = None\n\n        # If the client is clearing repo_url (switching to push mode), skip\n        # git validation — the push callback will set repo_url to internal://\n        # after the first successful push.\n        clearing_repo_url = (\n            update_data.repo_url is not None and update_data.repo_url.strip() == \"\"\n        )\n\n        validated = None\n        needs_internal_ref_resolution = (\n            update_data.repo_url is not None or update_data.git_ref is not None\n        )\n        resolved_repo_url = update_data.repo_url or current_deployment.repo_url\n        if clearing_repo_url:\n            # Switching to push mode — no git validation needed\n            pass\n        elif (\n            needs_internal_ref_resolution\n            and resolved_repo_url == INTERNAL_CODE_REPO_SCHEME\n        ):\n            # Internal repo: resolve the ref from the S3-stored bare repo\n            git_ref_to_resolve = update_data.git_ref or current_deployment.git_ref\n            if git_ref_to_resolve:\n                storage = code_repo_storage\n                if storage is None:\n                    raise HTTPException(\n                        status_code=503,\n                        detail=\"Code repo storage not configured (S3_BUCKET not set).\",\n                    )\n                resolved_sha = await storage.resolve_ref(\n                    deployment_id, git_ref_to_resolve\n                )\n                if resolved_sha:\n                    update_data.git_sha = resolved_sha\n                else:\n                    raise HTTPException(\n                        status_code=400,\n                        detail=f\"Could not resolve ref '{git_ref_to_resolve}' in the internal repo for deployment {deployment_id}. \"\n                        \"Push the ref first or check for typos.\",\n                    )\n        elif (\n            update_data.has_git_fields()\n            and resolved_repo_url != INTERNAL_CODE_REPO_SCHEME\n        ):\n            # External repo: validate and resolve via git service\n            git_ref_to_resolve = update_data.git_ref or current_deployment.git_ref\n\n            validated = await git_service.validate_git_application(\n                repository_url=resolved_repo_url,\n                git_ref=git_ref_to_resolve,\n                deployment_file_path=update_data.deployment_file_path\n                or current_deployment.deployment_file_path,\n                pat=update_data.personal_access_token,\n                deployment_id=deployment_id,\n            )\n            update_data.git_sha = validated.git_sha\n            update_data.static_assets_path = validated.ui_build_output_path\n\n        updated_deployment = await k8s_client.update_deployment(\n            deployment_id=deployment_id,\n            update=update_data,\n        )\n        if updated_deployment is None:\n            raise DeploymentNotFoundError(\n                f\"Deployment with id {deployment_id} not found\"\n            )\n\n        # Return deployment response with warning header if there are git issues\n        if validated is not None and validated.error_message:\n            updated_deployment.warning = validated.error_message\n        return updated_deployment\n\n    @override\n    async def handle_git_request(\n        self,\n        request: Request,\n        project_id: str,\n        deployment_id: str,\n        git_path: str,\n    ) -> Response:\n        \"\"\"Handle git HTTP requests for a deployment's internal code repo.\"\"\"\n        deployment = await self._get_deployment_or_raise(project_id, deployment_id)\n\n        if deployment.repo_url not in (\"\", INTERNAL_CODE_REPO_SCHEME):\n            raise HTTPException(\n                status_code=409,\n                detail=(\n                    \"Deployment is configured with an external repository; \"\n                    \"the internal git endpoint is only available for push-mode \"\n                    \"deployments.\"\n                ),\n            )\n\n        if code_repo_storage is None:\n            return Response(\n                content=\"Code repo storage not configured (S3_BUCKET not set).\",\n                status_code=503,\n            )\n\n        return await _handle_git_request(\n            request=request,\n            deployment_id=deployment_id,\n            git_path=git_path,\n            storage=code_repo_storage,\n            on_push_complete=_on_push_complete,\n        )\n\n    async def _when_replicaset_changes(\n        self,\n        deployment_id: str,\n        interval_seconds: float,\n    ) -> AsyncIterator[Literal[\"__RS_CHANGED__\"]]:\n        initial_uid = await self._current_rs_uid(deployment_id)\n        while True:\n            current_uid = await self._current_rs_uid(deployment_id)\n            if current_uid != initial_uid:\n                yield \"__RS_CHANGED__\"\n                break\n            await asyncio.sleep(interval_seconds)\n\n    async def _current_rs_uid(\n        self,\n        deployment_id: str,\n    ) -> str | None:\n        rs = await k8s_client.get_latest_replicaset_for_deployment(deployment_id)\n        return rs.metadata.uid if rs is not None and rs.metadata is not None else None\n\n    async def _build_log_events(\n        self,\n        deployment_id: str,\n        since_seconds: int | None,\n        tail_lines: int | None,\n        follow: bool = True,\n    ) -> AsyncGenerator[LogEvent, None]:\n        \"\"\"Yield log events from the build Job, if one exists.\"\"\"\n        async for line in k8s_client.stream_build_job_logs(\n            deployment_id=deployment_id,\n            since_seconds=since_seconds,\n            tail_lines=tail_lines,\n            stop_event=shutdown_event,\n            follow=follow,\n        ):\n            timestamp = line.timestamp or datetime.now(timezone.utc)\n            yield LogEvent(\n                pod=line.pod,\n                container=line.container,\n                text=line.text,\n                timestamp=timestamp,\n            )\n\n    @override\n    async def stream_deployment_logs(\n        self,\n        project_id: str,\n        deployment_id: str,\n        include_init_containers: bool = False,\n        since_seconds: int | None = None,\n        tail_lines: int | None = None,\n        follow: bool = True,\n    ) -> AsyncGenerator[LogEvent, None]:\n        \"\"\"Stream logs for a deployment.\n\n        Build job logs (if any) are automatically merged into the stream.\n        App logs stream from the latest ReplicaSet. When ``follow`` is True\n        and the RS changes (e.g. build finishes and operator creates a\n        Deployment), the inner generators restart so logs continue seamlessly\n        without requiring the client to reconnect. When ``follow`` is False,\n        the generator returns whatever logs are currently available and ends.\n        \"\"\"\n\n        await self._get_deployment_or_raise(project_id, deployment_id)\n\n        include_build_logs = True\n        while True:\n            initial_rs_uid = await self._current_rs_uid(deployment_id)\n\n            app_logs = k8s_client.stream_replicaset_logs(\n                deployment_id=deployment_id,\n                include_init_containers=include_init_containers,\n                since_seconds=since_seconds,\n                tail_lines=tail_lines,\n                stop_event=shutdown_event,\n                follow=follow,\n            )\n\n            if include_build_logs:\n                build_logs: AsyncGenerator[LogEvent, None] = self._build_log_events(\n                    deployment_id=deployment_id,\n                    since_seconds=since_seconds,\n                    tail_lines=tail_lines,\n                    follow=follow,\n                )\n                include_build_logs = False\n            else:\n                build_logs = _empty_log_gen()\n\n            # Merge build + app logs; build logs are finite, app logs are ongoing.\n            # stop_on_first_completion=False so app logs continue after build finishes.\n            merged_logs = merge_generators(\n                build_logs,\n                app_logs,\n                stop_on_first_completion=False,\n            )\n\n            debounced_logs = debounced_sorted_prefix(\n                merged_logs,\n                key=lambda x: (x.timestamp, x.pod, x.container, x.text),\n                debounce_seconds=0.2,\n                max_window_seconds=0.5,\n            )\n\n            if follow:\n                when_changes = self._when_replicaset_changes(deployment_id, 0.05)\n\n                rs_changed = False\n                async for ev in merge_generators(\n                    debounced_logs,\n                    cast(AsyncGenerator[LogEvent | str, None], when_changes),\n                    stop_on_first_completion=True,\n                ):\n                    if isinstance(ev, str):\n                        # RS-change sentinel — restart inner generators\n                        rs_changed = True\n                        break\n                    timestamp = ev.timestamp or datetime.now(timezone.utc)\n                    yield LogEvent(\n                        pod=ev.pod,\n                        container=ev.container,\n                        text=ev.text,\n                        timestamp=timestamp,\n                    )\n\n                if rs_changed:\n                    # RS changed (e.g. build→deploy transition). Loop to pick up\n                    # the new RS's pods without dropping the SSE connection.\n                    continue\n\n                # All log generators completed without an RS change. Check if an\n                # RS appeared while we were streaming build logs (race window\n                # where debounced_logs finishes before _when_replicaset_changes\n                # polls).\n                if initial_rs_uid is None:\n                    current_uid = await self._current_rs_uid(deployment_id)\n                    if current_uid is not None:\n                        continue\n\n                # No new RS appeared — nothing more to stream.\n                break\n\n            # follow=False: drain whatever's available and exit. No RS-change\n            # watcher, no reconnect — the underlying read uses follow=False\n            # against k8s and terminates on its own.\n            async for ev in debounced_logs:\n                timestamp = ev.timestamp or datetime.now(timezone.utc)\n                yield LogEvent(\n                    pod=ev.pod,\n                    container=ev.container,\n                    text=ev.text,\n                    timestamp=timestamp,\n                )\n            break\n\n\nasync def _empty_log_gen() -> AsyncGenerator[LogEvent, None]:\n    return\n    yield  # make it a generator\n\n\ndeployments_service = DeploymentService()\npublic_service = PublicDeploymentService()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/manage_api/deployments_v1beta1.py",
    "content": "from llama_agents.core.server.manage_api import create_v1beta1_deployments_router\n\nfrom .deployments_service import deployments_service, public_service\n\nrouter = create_v1beta1_deployments_router(deployments_service, public_service)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/manage_api/manage_app.py",
    "content": "import logging\nimport os\nimport signal\nimport uuid\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\nfrom types import FrameType\nfrom typing import cast\n\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\nfrom prometheus_fastapi_instrumentator import Instrumentator\n\nfrom ..lifecycle import shutdown_event\nfrom .backup_v1beta1 import router as backup_v1beta1\nfrom .deployments_v1beta1 import router as deployments_v1beta1\n\nlogger = logging.getLogger(__name__)\n\n\n_PREV_SIGNAL_HANDLERS: dict[int, signal.Handlers] = {}\n\n\n# Register early signal handlers so long-running generators can exit promptly\ndef _handle_shutdown_signal(signum: int, frame: FrameType | None = None) -> None:\n    logger.info(f\"manage_api signal received: setting shutdown_event ({signum})\")\n    shutdown_event.set()\n\n    # Chain to any previously-registered handler so the server can still shut down\n    prev = _PREV_SIGNAL_HANDLERS.get(signum)\n    if callable(prev):\n        try:\n            prev(signum, frame)\n        except Exception:\n            # Let exceptions propagate to allow normal shutdown behavior\n            raise\n    elif prev == signal.SIG_DFL:\n        # Restore default then re-emit the signal to trigger default termination\n        signal.signal(signum, signal.SIG_DFL)\n        os.kill(os.getpid(), signum)\n    elif prev == signal.SIG_IGN:\n        # Respect ignore; nothing else to do\n        pass\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:\n    # Startup\n    global _PREV_SIGNAL_HANDLERS\n    shutdown_event.clear()\n    for _sig in (signal.SIGINT, signal.SIGTERM):\n        prev_handler = signal.getsignal(_sig)\n        if prev_handler is not None:\n            _PREV_SIGNAL_HANDLERS[_sig] = cast(signal.Handlers, prev_handler)\n        signal.signal(_sig, _handle_shutdown_signal)\n\n    yield\n    # Ensure shutdown flag is set during app shutdown as a fallback\n    shutdown_event.set()\n\n\napp = FastAPI(title=\"LlamaDeploy on Cloud\", lifespan=lifespan)\nInstrumentator().instrument(app).expose(app, include_in_schema=False)\n\n\n@app.exception_handler(Exception)\nasync def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:\n    correlation_id = uuid.uuid4().hex\n    logger.exception(\n        \"Unhandled error on %s %s [correlation_id=%s]\",\n        request.method,\n        request.url.path,\n        correlation_id,\n    )\n    return JSONResponse(\n        status_code=500,\n        content={\n            \"detail\": \"Internal server error\",\n            \"correlation_id\": correlation_id,\n        },\n    )\n\n\n# Include API routers\n\napp.include_router(deployments_v1beta1)\napp.include_router(backup_v1beta1)\n\n\n@app.get(\"/health\")\nasync def health() -> dict[str, str]:\n    return {\"status\": \"ok\"}\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/py.typed",
    "content": ""
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/settings.py",
    "content": "from pydantic import Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# When DEFAULT_APPSERVER_IMAGE_TAG is set to this value, the control plane will\n# not stamp any image tag on CRDs. The operator's LLAMA_DEPLOY_IMAGE_TAG env var\n# will be used instead. This is useful for local development with tilt, where\n# the operator's env var is kept in sync with locally-built images.\nOPERATOR_DEFAULT_IMAGE_TAG = \"operator-default\"\n\n\nclass ControlPlaneSettings(BaseSettings):\n    model_config = SettingsConfigDict()\n\n    # Kubernetes settings\n    kubernetes_namespace: str = Field(\n        default=\"\",\n        description=\"Kubernetes namespace to operate in (empty for auto-detection)\",\n        alias=\"KUBERNETES_NAMESPACE\",\n    )\n\n    # Default appserver image tag (set by Helm chart via DEFAULT_APPSERVER_IMAGE_TAG)\n    default_appserver_image_tag: str = Field(\n        default=\"\",\n        description=(\n            \"Default appserver image tag to stamp on new deployments. \"\n            \"Empty = don't stamp. \"\n            f\"'{OPERATOR_DEFAULT_IMAGE_TAG}' = defer to operator env var, \"\n            \"ignoring client-requested versions.\"\n        ),\n        alias=\"DEFAULT_APPSERVER_IMAGE_TAG\",\n    )\n\n    @property\n    def should_stamp_image_tag(self) -> bool:\n        \"\"\"Whether the control plane should stamp image tags on CRDs.\n\n        Returns False only when explicitly set to OPERATOR_DEFAULT_IMAGE_TAG.\n        Empty string or any real tag value means stamping is enabled.\n        \"\"\"\n        return self.default_appserver_image_tag != OPERATOR_DEFAULT_IMAGE_TAG\n\n    # Development settings\n    fastapi_env: str = Field(\n        default=\"production\",\n        description=\"FastAPI environment mode\",\n        alias=\"FASTAPI_ENV\",\n    )\n\n    # Local development ingress settings\n    local_dev_ingress: bool = Field(\n        default=False,\n        description=\"Enable ingress for local development\",\n        alias=\"LOCAL_DEV_INGRESS\",\n    )\n    local_dev_domain: str = Field(\n        default=\"127.0.0.1.nip.io\",\n        description=\"Domain for local development ingress\",\n        alias=\"LOCAL_DEV_DOMAIN\",\n    )\n\n    # Object storage settings\n    s3_endpoint_url: str | None = Field(\n        default=None,\n        description=\"S3 endpoint URL (for MinIO, R2, etc.)\",\n        alias=\"S3_ENDPOINT_URL\",\n    )\n    s3_bucket: str | None = Field(\n        default=None,\n        description=\"S3 bucket name\",\n        alias=\"S3_BUCKET\",\n    )\n    s3_region: str | None = Field(\n        default=None,\n        description=\"S3 region\",\n        alias=\"S3_REGION\",\n    )\n    s3_access_key: str | None = Field(\n        default=None,\n        description=\"S3 access key\",\n        alias=\"S3_ACCESS_KEY\",\n    )\n    s3_secret_key: str | None = Field(\n        default=None,\n        description=\"S3 secret key\",\n        alias=\"S3_SECRET_KEY\",\n    )\n    s3_unsigned: bool = Field(\n        default=False,\n        description=(\n            \"Send S3 requests unsigned (no Authorization header). \"\n            \"Enable for authless S3-compatible backends (s3proxy, LocalStack, \"\n            \"public buckets). Overrides any configured credentials.\"\n        ),\n        alias=\"S3_UNSIGNED\",\n    )\n\n    # Backup-specific settings\n    backup_s3_key_prefix: str = Field(\n        default=\"backups\",\n        description=\"S3 key prefix (path) for backup archives\",\n        alias=\"BACKUP_S3_KEY_PREFIX\",\n    )\n    backup_encryption_password: str | None = Field(\n        default=None,\n        description=\"Password for encrypting secrets in backups\",\n        alias=\"BACKUP_ENCRYPTION_PASSWORD\",\n    )\n\n    # Build artifact settings\n    build_s3_key_prefix: str = Field(\n        default=\"builds\",\n        description=\"S3 key prefix (path) for build artifacts\",\n        alias=\"BUILD_S3_KEY_PREFIX\",\n    )\n\n    # Must exceed the operator's build Job TTLSecondsAfterFinished (3600s) so\n    # an artifact whose Job is still within its TTL is guaranteed to exist.\n    build_artifact_gc_grace_seconds: int = Field(\n        default=4500,\n        description=\"Grace window (seconds) before an unreferenced build artifact is eligible for GC.\",\n        alias=\"BUILD_ARTIFACT_GC_GRACE_SECONDS\",\n    )\n\n    # Code repo settings\n    code_repo_s3_key_prefix: str = Field(\n        default=\"git\",\n        description=\"S3 key prefix (path) for code repository archives\",\n        alias=\"CODE_REPO_S3_KEY_PREFIX\",\n    )\n\n\n# Global settings instance\nsettings = ControlPlaneSettings()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_agents/control_plane/storage.py",
    "content": "\"\"\"S3-compatible object storage base class.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom contextlib import asynccontextmanager\nfrom typing import TYPE_CHECKING, Any, AsyncIterator, TypedDict\n\nimport aioboto3\nfrom botocore import UNSIGNED\nfrom botocore.client import Config\n\nif TYPE_CHECKING:\n    from types_aiobotocore_s3 import S3Client\n\nlogger = logging.getLogger(__name__)\n\n\nclass _S3ClientKwargs(TypedDict, total=False):\n    endpoint_url: str\n    region_name: str\n    aws_access_key_id: str\n    aws_secret_access_key: str\n    config: Any\n\n\nclass S3ObjectStorage:\n    \"\"\"Base class for S3-compatible object storage backends.\n\n    Provides shared session/client management. Subclasses define their own\n    key scheme and domain methods.\n    \"\"\"\n\n    def __init__(\n        self,\n        bucket: str,\n        endpoint_url: str | None = None,\n        region: str | None = None,\n        access_key: str | None = None,\n        secret_key: str | None = None,\n        key_prefix: str = \"\",\n        unsigned: bool = False,\n    ) -> None:\n        self._bucket = bucket\n        self._key_prefix = key_prefix.strip(\"/\")\n        self._session = aioboto3.Session()\n        self._client_kwargs: _S3ClientKwargs = {}\n        if endpoint_url:\n            self._client_kwargs[\"endpoint_url\"] = endpoint_url\n        if region:\n            self._client_kwargs[\"region_name\"] = region\n        if unsigned:\n            # UNSIGNED bypasses boto's credential chain entirely — any creds\n            # set via env/IRSA/etc. are ignored. Intended for authless\n            # S3-compatible backends (s3proxy, LocalStack, public buckets).\n            self._client_kwargs[\"config\"] = Config(signature_version=UNSIGNED)\n        elif access_key and secret_key:\n            self._client_kwargs[\"aws_access_key_id\"] = access_key\n            self._client_kwargs[\"aws_secret_access_key\"] = secret_key\n\n    @asynccontextmanager\n    async def _client(self) -> AsyncIterator[S3Client]:\n        async with self._session.client(\"s3\", **self._client_kwargs) as client:\n            yield client\n"
  },
  {
    "path": "packages/llama-agents-control-plane/src/llama_deploy/control_plane/__init__.py",
    "content": "# Backwards-compatibility shim: llama_deploy.control_plane -> llama_agents.control_plane\nfrom llama_agents.core._alias import install_alias_finder\n\ninstall_alias_finder()\n\nfrom llama_agents.control_plane import *  # noqa: E402, F403\nfrom llama_agents.control_plane import __all__  # noqa: E402, F401\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/conftest.py",
    "content": "\"\"\"Shared fixtures for backup tests.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\nfrom unittest.mock import AsyncMock\n\nimport httpx\nimport pytest\nfrom fastapi import FastAPI\nfrom llama_agents.control_plane.backup.storage import S3BackupStorage\nfrom llama_agents.control_plane.manage_api.backup_service import BackupService\nfrom llama_agents.core.client.manage_client import ControlPlaneClient\n\n\ndef make_deployment(name: str, project_id: str = \"proj-1\") -> dict[str, Any]:\n    \"\"\"Create a minimal CRD-like dict for testing.\"\"\"\n    return {\n        \"apiVersion\": \"deploy.llamaindex.ai/v1alpha1\",\n        \"kind\": \"LlamaDeployment\",\n        \"metadata\": {\"name\": name, \"namespace\": \"default\"},\n        \"spec\": {\"image\": f\"registry/{name}:latest\", \"projectId\": project_id},\n    }\n\n\ndef make_raw_crd(\n    name: str,\n    project_id: str = \"proj-1\",\n    generation: int = 1,\n) -> dict[str, Any]:\n    \"\"\"Create a raw CRD dict as returned by k8s (with cluster metadata).\"\"\"\n    return {\n        \"apiVersion\": \"deploy.llamaindex.ai/v1alpha1\",\n        \"kind\": \"LlamaDeployment\",\n        \"metadata\": {\n            \"name\": name,\n            \"namespace\": \"default\",\n            \"resourceVersion\": \"12345\",\n            \"uid\": \"abc-def\",\n            \"creationTimestamp\": \"2025-01-01T00:00:00Z\",\n            \"generation\": generation,\n        },\n        \"spec\": {\"image\": f\"registry/{name}:latest\", \"projectId\": project_id},\n        \"status\": {\"ready\": True},\n    }\n\n\n@pytest.fixture\ndef mock_storage() -> AsyncMock:\n    \"\"\"Return an AsyncMock of S3BackupStorage.\"\"\"\n    storage = AsyncMock(spec=S3BackupStorage)\n    storage.list_backups.return_value = []\n    return storage\n\n\n@pytest.fixture\ndef backup_service(mock_storage: AsyncMock) -> BackupService:\n    \"\"\"Return a BackupService with mock storage.\"\"\"\n    return BackupService(mock_storage)\n\n\ndef make_client(app: FastAPI) -> ControlPlaneClient:\n    \"\"\"Build a ControlPlaneClient that talks to *app* via ASGI transport.\"\"\"\n    client = ControlPlaneClient.__new__(ControlPlaneClient)\n    client.base_url = \"http://test\"\n    client.api_key = None\n    transport = httpx.ASGITransport(app=app)\n    client.client = httpx.AsyncClient(transport=transport, base_url=\"http://test\")\n    client.hookless_client = httpx.AsyncClient(\n        transport=transport, base_url=\"http://test\"\n    )\n    return client\n\n\n@pytest.fixture(autouse=True)\ndef _clear_aws_env(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Prevent host AWS config from leaking into moto tests.\"\"\"\n    monkeypatch.delenv(\"AWS_PROFILE\", raising=False)\n    monkeypatch.delenv(\"AWS_DEFAULT_PROFILE\", raising=False)\n    monkeypatch.delenv(\"AWS_CONFIG_FILE\", raising=False)\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/test_archive.py",
    "content": "\"\"\"Tests for backup archive create/read and metadata cleaning.\"\"\"\n\nfrom __future__ import annotations\n\nimport io\nimport tarfile\nfrom typing import Any\n\nimport pytest\nfrom llama_agents.control_plane.backup.archive import (\n    clean_crd_metadata,\n    clean_secret_metadata,\n    create_backup_archive,\n    read_backup_archive,\n)\n\nfrom .conftest import make_deployment\n\n# ---------------------------------------------------------------------------\n# Archive round-trip\n# ---------------------------------------------------------------------------\n\n\ndef test_round_trip() -> None:\n    deployments = [make_deployment(\"app1\"), make_deployment(\"app2\")]\n    secrets = {\"app1\": {\"API_KEY\": \"secret123\"}}\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets=secrets,\n        namespace=\"default\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n    )\n    contents = read_backup_archive(archive)\n    assert contents.manifest.version == 1\n    assert contents.manifest.namespace == \"default\"\n    assert contents.manifest.deployment_count == 2\n    assert contents.manifest.encrypted is False\n    assert len(contents.entries) == 2\n\n    by_name = {e.name: e for e in contents.entries}\n    assert by_name[\"app1\"].cr[\"spec\"][\"image\"] == \"registry/app1:latest\"\n    assert by_name[\"app1\"].secret == {\"API_KEY\": \"secret123\"}\n    assert by_name[\"app2\"].secret is None\n\n\ndef test_encrypted_archive_round_trip() -> None:\n    deployments = [make_deployment(\"secure\")]\n    secrets = {\"secure\": {\"TOKEN\": \"abc\"}}\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets=secrets,\n        namespace=\"ns\",\n        timestamp=\"2025-06-01T00:00:00Z\",\n        encryption_password=\"my-password\",\n    )\n    contents = read_backup_archive(archive, encryption_password=\"my-password\")\n    assert contents.manifest.encrypted is True\n    assert contents.entries[0].secret == {\"TOKEN\": \"abc\"}\n\n\ndef test_unencrypted_archive_round_trip() -> None:\n    deployments = [make_deployment(\"plain\")]\n    secrets = {\"plain\": {\"KEY\": \"val\"}}\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets=secrets,\n        namespace=\"ns\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n        encryption_password=None,\n    )\n    contents = read_backup_archive(archive)\n    assert contents.manifest.encrypted is False\n    assert contents.entries[0].secret == {\"KEY\": \"val\"}\n\n\ndef test_empty_deployments() -> None:\n    archive = create_backup_archive(\n        deployments=[],\n        secrets={},\n        namespace=\"ns\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n    )\n    contents = read_backup_archive(archive)\n    assert contents.manifest.deployment_count == 0\n    assert contents.entries == []\n\n\ndef test_archive_extractable_with_tarfile() -> None:\n    deployments = [make_deployment(\"d1\")]\n    secrets = {\"d1\": {\"K\": \"V\"}}\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets=secrets,\n        namespace=\"ns\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n    )\n    with tarfile.open(fileobj=io.BytesIO(archive), mode=\"r:gz\") as tar:\n        names = tar.getnames()\n    assert \"manifest.json\" in names\n    assert \"d1.yaml\" in names\n    assert \"d1.secret.yaml\" in names\n\n\ndef test_encrypted_archive_has_enc_file() -> None:\n    deployments = [make_deployment(\"d1\")]\n    secrets = {\"d1\": {\"K\": \"V\"}}\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets=secrets,\n        namespace=\"ns\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n        encryption_password=\"pw\",\n    )\n    with tarfile.open(fileobj=io.BytesIO(archive), mode=\"r:gz\") as tar:\n        names = tar.getnames()\n    assert \"d1.secret.enc\" in names\n    assert \"d1.secret.yaml\" not in names\n\n\ndef test_missing_manifest_raises() -> None:\n    buf = io.BytesIO()\n    with tarfile.open(fileobj=buf, mode=\"w:gz\") as tar:\n        data = b\"apiVersion: v1\\nkind: Dummy\\n\"\n        info = tarfile.TarInfo(name=\"something.yaml\")\n        info.size = len(data)\n        tar.addfile(info, io.BytesIO(data))\n    with pytest.raises(ValueError, match=\"missing manifest\"):\n        read_backup_archive(buf.getvalue())\n\n\ndef test_encrypted_archive_without_password_raises() -> None:\n    deployments = [make_deployment(\"secure\")]\n    secrets = {\"secure\": {\"S\": \"val\"}}\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets=secrets,\n        namespace=\"ns\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n        encryption_password=\"pw\",\n    )\n    with pytest.raises(ValueError, match=\"no password provided\"):\n        read_backup_archive(archive, encryption_password=None)\n\n\ndef test_generation_stored_in_archive() -> None:\n    deployments = [make_deployment(\"app1\"), make_deployment(\"app2\")]\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets={},\n        namespace=\"ns\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n        generations={\"app1\": 5, \"app2\": 3},\n    )\n    contents = read_backup_archive(archive)\n    by_name = {e.name: e for e in contents.entries}\n    assert by_name[\"app1\"].generation == 5\n    assert by_name[\"app2\"].generation == 3\n\n\ndef test_generation_none_when_not_provided() -> None:\n    deployments = [make_deployment(\"app1\")]\n    archive = create_backup_archive(\n        deployments=deployments,\n        secrets={},\n        namespace=\"ns\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n    )\n    contents = read_backup_archive(archive)\n    assert contents.entries[0].generation is None\n\n\n# ---------------------------------------------------------------------------\n# Metadata cleaning\n# ---------------------------------------------------------------------------\n\n\ndef test_clean_crd_metadata_strips_cluster_fields() -> None:\n    doc: dict[str, Any] = {\n        \"metadata\": {\n            \"name\": \"my-app\",\n            \"resourceVersion\": \"12345\",\n            \"uid\": \"abc-def\",\n            \"creationTimestamp\": \"2025-01-01T00:00:00Z\",\n            \"generation\": 3,\n            \"managedFields\": [{\"manager\": \"kubectl\"}],\n            \"selfLink\": \"/apis/foo\",\n            \"deletionTimestamp\": \"2025-01-02T00:00:00Z\",\n            \"deletionGracePeriodSeconds\": 30,\n            \"finalizers\": [\"some-finalizer\"],\n            \"annotations\": {\n                \"kubectl.kubernetes.io/last-applied\": \"{}\",\n                \"deploy.llamaindex.ai/secret-hash\": \"abc123\",\n                \"keep-this\": \"yes\",\n            },\n        },\n        \"status\": {\"ready\": True},\n        \"spec\": {\"image\": \"img\"},\n    }\n    cleaned = clean_crd_metadata(doc)\n    meta = cleaned[\"metadata\"]\n    assert meta[\"name\"] == \"my-app\"\n    for key in [\n        \"resourceVersion\",\n        \"uid\",\n        \"creationTimestamp\",\n        \"generation\",\n        \"managedFields\",\n        \"selfLink\",\n        \"deletionTimestamp\",\n        \"deletionGracePeriodSeconds\",\n        \"finalizers\",\n    ]:\n        assert key not in meta\n    assert \"status\" not in cleaned\n    assert \"keep-this\" in meta[\"annotations\"]\n    assert \"kubectl.kubernetes.io/last-applied\" not in meta[\"annotations\"]\n    assert \"deploy.llamaindex.ai/secret-hash\" not in meta[\"annotations\"]\n\n\ndef test_clean_crd_removes_empty_annotations() -> None:\n    doc: dict[str, Any] = {\n        \"metadata\": {\n            \"name\": \"x\",\n            \"annotations\": {\n                \"kubectl.kubernetes.io/something\": \"val\",\n            },\n        },\n    }\n    cleaned = clean_crd_metadata(doc)\n    assert \"annotations\" not in cleaned[\"metadata\"]\n\n\ndef test_clean_secret_metadata_strips_owner_references() -> None:\n    doc: dict[str, Any] = {\n        \"metadata\": {\n            \"name\": \"my-secret\",\n            \"ownerReferences\": [{\"kind\": \"Deployment\", \"name\": \"app\"}],\n            \"resourceVersion\": \"999\",\n            \"uid\": \"uid-1\",\n            \"creationTimestamp\": \"2025-01-01T00:00:00Z\",\n            \"generation\": 1,\n            \"managedFields\": [],\n            \"selfLink\": \"/api/v1/secrets/x\",\n            \"annotations\": {\n                \"kubectl.kubernetes.io/last-applied\": \"{}\",\n                \"custom-annotation\": \"keep\",\n            },\n        },\n    }\n    cleaned = clean_secret_metadata(doc)\n    meta = cleaned[\"metadata\"]\n    assert \"ownerReferences\" not in meta\n    assert \"resourceVersion\" not in meta\n    assert \"uid\" not in meta\n    assert meta[\"name\"] == \"my-secret\"\n    assert \"custom-annotation\" in meta[\"annotations\"]\n    assert \"kubectl.kubernetes.io/last-applied\" not in meta[\"annotations\"]\n\n\ndef test_clean_secret_does_not_strip_finalizers() -> None:\n    doc: dict[str, Any] = {\n        \"metadata\": {\n            \"name\": \"sec\",\n            \"finalizers\": [\"keep-me\"],\n        },\n    }\n    cleaned = clean_secret_metadata(doc)\n    assert \"finalizers\" in cleaned[\"metadata\"]\n\n\ndef test_clean_secret_strips_system_annotations() -> None:\n    \"\"\"Secret cleaning strips system annotation prefixes just like CRD cleaning.\"\"\"\n    doc: dict[str, Any] = {\n        \"metadata\": {\n            \"name\": \"sec\",\n            \"annotations\": {\n                \"deploy.llamaindex.ai/secret-hash\": \"hash123\",\n                \"custom-annotation\": \"keep\",\n            },\n        },\n    }\n    cleaned = clean_secret_metadata(doc)\n    assert \"deploy.llamaindex.ai/secret-hash\" not in cleaned[\"metadata\"].get(\n        \"annotations\", {}\n    )\n    assert cleaned[\"metadata\"][\"annotations\"][\"custom-annotation\"] == \"keep\"\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/test_backup_roundtrip.py",
    "content": "\"\"\"Round-trip tests: ControlPlaneClient -> FastAPI router -> BackupService -> S3.\n\nEach test stands up a real BackupService backed by moto S3, wires it into the\nFastAPI router, and drives it through ControlPlaneClient with a test transport.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport boto3\nimport pytest\nfrom aiomoto import mock_aws\nfrom fastapi import FastAPI\nfrom llama_agents.control_plane.backup.storage import S3BackupStorage\nfrom llama_agents.control_plane.manage_api.backup_service import BackupService\nfrom llama_agents.control_plane.manage_api.backup_v1beta1 import router\nfrom llama_agents.core.schema.backups import RestoreRequest\n\nfrom .conftest import make_client\n\nK8S = \"llama_agents.control_plane.manage_api.backup_service.k8s_client\"\nSETTINGS = \"llama_agents.control_plane.manage_api.backup_service.settings\"\nSERVICE = \"llama_agents.control_plane.manage_api.backup_v1beta1.backup_service\"\nGEN_ID = \"llama_agents.control_plane.manage_api.backup_service.generate_backup_id\"\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _make_storage() -> S3BackupStorage:\n    return S3BackupStorage(\n        bucket=\"test-bucket\",\n        region=\"us-east-1\",\n        access_key=\"testing\",\n        secret_key=\"testing\",\n    )\n\n\ndef _create_bucket() -> None:\n    s3 = boto3.client(\n        \"s3\",\n        region_name=\"us-east-1\",\n        aws_access_key_id=\"testing\",\n        aws_secret_access_key=\"testing\",\n    )\n    s3.create_bucket(Bucket=\"test-bucket\")\n\n\ndef _make_mock_k8s(crds: list[dict[str, Any]] | None = None) -> MagicMock:\n    m = MagicMock()\n    m.get_all_deployment_crds = AsyncMock(return_value=crds or [])\n    m.get_secret_data = AsyncMock(return_value=None)\n    m.get_deployment_crd_raw = AsyncMock(return_value=None)\n    m.apply_deployment_crd = AsyncMock()\n    m.apply_secret = AsyncMock()\n    m.delete_deployment_crd = AsyncMock()\n    m.get_namespace.return_value = \"default\"\n    return m\n\n\ndef _make_mock_settings() -> MagicMock:\n    m = MagicMock()\n    m.backup_encryption_password = None\n    m.s3_bucket = \"test-bucket\"\n    return m\n\n\ndef _make_deployment(name: str, project_id: str = \"proj-1\") -> dict[str, Any]:\n    return {\n        \"apiVersion\": \"deploy.llamaindex.ai/v1alpha1\",\n        \"kind\": \"LlamaDeployment\",\n        \"metadata\": {\"name\": name, \"namespace\": \"default\", \"generation\": 1},\n        \"spec\": {\"image\": f\"registry/{name}:latest\", \"projectId\": project_id},\n        \"status\": {\"ready\": True},\n    }\n\n\n# ---------------------------------------------------------------------------\n# Tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_create_and_get_backup() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        svc = BackupService(storage)\n        mock_k8s = _make_mock_k8s(crds=[_make_deployment(\"app1\")])\n        mock_settings = _make_mock_settings()\n\n        app = FastAPI()\n        app.include_router(router)\n\n        with patch(SERVICE, svc), patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n            cp = make_client(app)\n            created = await cp.create_backup()\n\n            assert created.status == \"completed\"\n            assert created.deployment_count == 1\n            assert created.size_bytes is not None and created.size_bytes > 0\n\n            fetched = await cp.get_backup(created.backup_id)\n\n            assert fetched.backup_id == created.backup_id\n            assert fetched.status == \"completed\"\n            assert fetched.size_bytes == created.size_bytes\n\n            await cp.aclose()\n\n\n@pytest.mark.asyncio\nasync def test_create_and_list_backups() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        svc = BackupService(storage)\n        mock_k8s = _make_mock_k8s(crds=[_make_deployment(\"app1\")])\n        mock_settings = _make_mock_settings()\n\n        app = FastAPI()\n        app.include_router(router)\n\n        # generate_backup_id has second-level granularity, so two calls in the\n        # same second produce the same ID.  Use deterministic IDs instead.\n        id_iter = iter([\"backup-20260101-000001\", \"backup-20260101-000002\"])\n        with (\n            patch(SERVICE, svc),\n            patch(K8S, mock_k8s),\n            patch(SETTINGS, mock_settings),\n            patch(GEN_ID, side_effect=lambda: next(id_iter)),\n        ):\n            cp = make_client(app)\n            b1 = await cp.create_backup()\n            b2 = await cp.create_backup()\n\n            listing = await cp.list_backups()\n\n            ids = {b.backup_id for b in listing.backups}\n            assert b1.backup_id in ids\n            assert b2.backup_id in ids\n            assert len(listing.backups) == 2\n\n            await cp.aclose()\n\n\n@pytest.mark.asyncio\nasync def test_create_and_delete_backup() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        svc = BackupService(storage)\n        mock_k8s = _make_mock_k8s(crds=[_make_deployment(\"app1\")])\n        mock_settings = _make_mock_settings()\n\n        app = FastAPI()\n        app.include_router(router)\n\n        with patch(SERVICE, svc), patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n            cp = make_client(app)\n            created = await cp.create_backup()\n            assert created.status == \"completed\"\n\n            deleted = await cp.delete_backup(created.backup_id)\n            assert deleted.status == \"deleted\"\n\n            fetched = await cp.get_backup(created.backup_id)\n            assert fetched.status == \"failed\"\n            assert fetched.error is not None\n\n            await cp.aclose()\n\n\n@pytest.mark.asyncio\nasync def test_create_and_restore_backup() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        svc = BackupService(storage)\n        mock_k8s = _make_mock_k8s(crds=[_make_deployment(\"app1\")])\n        mock_settings = _make_mock_settings()\n\n        app = FastAPI()\n        app.include_router(router)\n\n        with patch(SERVICE, svc), patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n            cp = make_client(app)\n            created = await cp.create_backup()\n\n            # Nothing exists in the cluster on restore\n            mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=None)\n\n            req = RestoreRequest(backup_id=created.backup_id, conflict_mode=\"skip\")\n            restored = await cp.restore_backup(created.backup_id, req)\n\n            assert restored.status == \"completed\"\n            assert restored.results is not None\n            assert len(restored.results) == 1\n            assert restored.results[0].action == \"created\"\n            assert restored.results[0].name == \"app1\"\n\n            await cp.aclose()\n\n\n@pytest.mark.asyncio\nasync def test_restore_overwrite_always() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        svc = BackupService(storage)\n        dep = _make_deployment(\"app1\")\n        mock_k8s = _make_mock_k8s(crds=[dep])\n        mock_settings = _make_mock_settings()\n\n        app = FastAPI()\n        app.include_router(router)\n\n        with patch(SERVICE, svc), patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n            cp = make_client(app)\n            created = await cp.create_backup()\n\n            # Simulate that the deployment exists in the cluster at restore time\n            mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=dep)\n\n            req = RestoreRequest(\n                backup_id=created.backup_id,\n                conflict_mode=\"overwrite-always\",\n            )\n            restored = await cp.restore_backup(created.backup_id, req)\n\n            assert restored.status == \"completed\"\n            assert restored.results is not None\n            assert len(restored.results) == 1\n            assert restored.results[0].action == \"updated\"\n            mock_k8s.apply_deployment_crd.assert_awaited()\n\n            await cp.aclose()\n\n\n@pytest.mark.asyncio\nasync def test_restore_with_deletions() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        svc = BackupService(storage)\n\n        # Backup only contains app1\n        mock_k8s = _make_mock_k8s(crds=[_make_deployment(\"app1\")])\n        mock_settings = _make_mock_settings()\n\n        app = FastAPI()\n        app.include_router(router)\n\n        with patch(SERVICE, svc), patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n            cp = make_client(app)\n            created = await cp.create_backup()\n\n            # On restore, cluster has app1 + app2\n            mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=None)\n            mock_k8s.get_all_deployment_crds = AsyncMock(\n                return_value=[\n                    _make_deployment(\"app1\"),\n                    _make_deployment(\"app2\"),\n                ]\n            )\n\n            req = RestoreRequest(\n                backup_id=created.backup_id,\n                include_deletions=True,\n            )\n            restored = await cp.restore_backup(created.backup_id, req)\n\n            assert restored.status == \"completed\"\n            assert restored.results is not None\n            actions = {r.name: r.action for r in restored.results}\n            assert actions[\"app1\"] == \"created\"\n            assert actions[\"app2\"] == \"deleted\"\n\n            await cp.aclose()\n\n\n@pytest.mark.asyncio\nasync def test_backup_not_configured_returns_503() -> None:\n    import httpx\n\n    app = FastAPI()\n    app.include_router(router)\n\n    with patch(SERVICE, None):\n        cp = make_client(app)\n        with pytest.raises(httpx.HTTPStatusError) as exc_info:\n            await cp.create_backup()\n        assert exc_info.value.response.status_code == 503\n\n        await cp.aclose()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/test_backup_service.py",
    "content": "\"\"\"Unit tests for BackupService.\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom llama_agents.control_plane.backup.archive import create_backup_archive\nfrom llama_agents.control_plane.backup.storage import BackupInfo\nfrom llama_agents.control_plane.manage_api.backup_service import BackupService\nfrom llama_agents.core.schema.backups import RestoreRequest\n\nfrom .conftest import make_deployment, make_raw_crd\n\nK8S = \"llama_agents.control_plane.manage_api.backup_service.k8s_client\"\nSETTINGS = \"llama_agents.control_plane.manage_api.backup_service.settings\"\n\n\n@pytest.fixture\ndef mock_k8s() -> MagicMock:\n    \"\"\"Return a mock of the k8s_client module with sensible defaults.\"\"\"\n    m = MagicMock()\n    m.get_all_deployment_crds = AsyncMock(return_value=[])\n    m.get_secret_data = AsyncMock(return_value=None)\n    m.get_deployment_crd_raw = AsyncMock(return_value=None)\n    m.apply_deployment_crd = AsyncMock()\n    m.apply_secret = AsyncMock()\n    m.delete_deployment_crd = AsyncMock()\n    m.get_namespace.return_value = \"default\"\n    return m\n\n\n@pytest.fixture\ndef mock_settings() -> MagicMock:\n    m = MagicMock()\n    m.backup_encryption_password = None\n    return m\n\n\n# ---------------------------------------------------------------------------\n# create_backup\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_create_backup_success(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    crds = [make_raw_crd(\"app1\", generation=2), make_raw_crd(\"app2\", generation=1)]\n    mock_k8s.get_all_deployment_crds = AsyncMock(return_value=crds)\n\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.create_backup()\n\n    assert resp.status == \"completed\"\n    assert resp.deployment_count == 2\n    assert resp.size_bytes is not None and resp.size_bytes > 0\n    mock_storage.upload.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_create_backup_with_secrets(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    mock_k8s.get_all_deployment_crds = AsyncMock(return_value=[make_raw_crd(\"app1\")])\n    mock_k8s.get_secret_data = AsyncMock(return_value={\"API_KEY\": \"secret\"})\n\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.create_backup()\n\n    assert resp.status == \"completed\"\n    mock_storage.upload.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_create_backup_failure(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    mock_k8s.get_all_deployment_crds = AsyncMock(side_effect=RuntimeError(\"k8s down\"))\n\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.create_backup()\n\n    assert resp.status == \"failed\"\n    assert resp.error is not None and \"k8s down\" in resp.error\n\n\n# ---------------------------------------------------------------------------\n# list_backups / get_backup\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_list_backups(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n) -> None:\n    mock_storage.list_backups.return_value = [\n        BackupInfo(\n            backup_id=\"b1\",\n            timestamp=datetime(2025, 1, 1, tzinfo=timezone.utc),\n            size_bytes=100,\n        ),\n    ]\n    resp = await backup_service.list_backups()\n    assert len(resp.backups) == 1\n    assert resp.backups[0].backup_id == \"b1\"\n\n\n@pytest.mark.asyncio\nasync def test_get_backup_exists(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n) -> None:\n    mock_storage.get_info.return_value = BackupInfo(\n        backup_id=\"b1\",\n        timestamp=datetime(2025, 1, 1, tzinfo=timezone.utc),\n        size_bytes=200,\n    )\n    resp = await backup_service.get_backup(\"b1\")\n    assert resp.status == \"completed\"\n    assert resp.size_bytes == 200\n\n\n@pytest.mark.asyncio\nasync def test_get_backup_not_found(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n) -> None:\n    mock_storage.get_info.return_value = None\n    resp = await backup_service.get_backup(\"nonexistent\")\n    assert resp.status == \"failed\"\n    assert resp.error is not None and \"not found\" in resp.error.lower()\n\n\n# ---------------------------------------------------------------------------\n# delete_backup\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_delete_backup_exists(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n) -> None:\n    mock_storage.get_info.return_value = BackupInfo(\n        backup_id=\"b1\",\n        timestamp=datetime(2025, 1, 1, tzinfo=timezone.utc),\n        size_bytes=100,\n    )\n    resp = await backup_service.delete_backup(\"b1\")\n    assert resp.status == \"deleted\"\n    mock_storage.delete.assert_awaited_once_with(\"b1\")\n\n\n@pytest.mark.asyncio\nasync def test_delete_backup_not_found(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n) -> None:\n    mock_storage.get_info.return_value = None\n    resp = await backup_service.delete_backup(\"no-such\")\n    assert resp.status == \"failed\"\n    assert resp.error is not None\n    mock_storage.delete.assert_not_awaited()\n\n\n# ---------------------------------------------------------------------------\n# restore_backup\n# ---------------------------------------------------------------------------\n\n\ndef _make_archive_bytes(\n    entries: list[dict],\n    secrets: dict | None = None,\n    generations: dict | None = None,\n) -> bytes:\n    \"\"\"Build a real archive for restore tests.\"\"\"\n    return create_backup_archive(\n        deployments=entries,\n        secrets=secrets or {},\n        namespace=\"default\",\n        timestamp=\"2025-01-01T00:00:00Z\",\n        generations=generations,\n    )\n\n\n@pytest.mark.asyncio\nasync def test_restore_skip_existing(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\")\n    mock_storage.download.return_value = _make_archive_bytes([dep])\n    mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=make_raw_crd(\"app1\"))\n\n    req = RestoreRequest(backup_id=\"b1\", conflict_mode=\"skip\")\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    assert resp.results is not None\n    assert resp.results[0].action == \"skipped\"\n    mock_k8s.apply_deployment_crd.assert_not_awaited()\n\n\n@pytest.mark.asyncio\nasync def test_restore_overwrite_always(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\")\n    mock_storage.download.return_value = _make_archive_bytes([dep])\n    mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=make_raw_crd(\"app1\"))\n\n    req = RestoreRequest(backup_id=\"b1\", conflict_mode=\"overwrite-always\")\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    assert resp.results is not None\n    assert resp.results[0].action == \"updated\"\n    mock_k8s.apply_deployment_crd.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_restore_overwrite_if_newer_skips_when_cluster_newer(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\")\n    # Backup has generation 2, cluster has generation 5 (newer)\n    mock_storage.download.return_value = _make_archive_bytes(\n        [dep], generations={\"app1\": 2}\n    )\n    mock_k8s.get_deployment_crd_raw = AsyncMock(\n        return_value=make_raw_crd(\"app1\", generation=5)\n    )\n\n    req = RestoreRequest(backup_id=\"b1\", conflict_mode=\"overwrite-if-newer\")\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    assert resp.results is not None\n    assert resp.results[0].action == \"skipped\"\n    mock_k8s.apply_deployment_crd.assert_not_awaited()\n\n\n@pytest.mark.asyncio\nasync def test_restore_overwrite_if_newer_overwrites_when_backup_newer(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\")\n    # Backup has generation 5, cluster has generation 2 (older)\n    mock_storage.download.return_value = _make_archive_bytes(\n        [dep], generations={\"app1\": 5}\n    )\n    mock_k8s.get_deployment_crd_raw = AsyncMock(\n        return_value=make_raw_crd(\"app1\", generation=2)\n    )\n\n    req = RestoreRequest(backup_id=\"b1\", conflict_mode=\"overwrite-if-newer\")\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    assert resp.results is not None\n    assert resp.results[0].action == \"updated\"\n    mock_k8s.apply_deployment_crd.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_restore_project_mismatch(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\", project_id=\"proj-A\")\n    mock_storage.download.return_value = _make_archive_bytes([dep])\n    mock_k8s.get_deployment_crd_raw = AsyncMock(\n        return_value=make_raw_crd(\"app1\", project_id=\"proj-B\")\n    )\n\n    req = RestoreRequest(backup_id=\"b1\", conflict_mode=\"overwrite-always\")\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    assert resp.results is not None\n    assert resp.results[0].action == \"failed\"\n    assert \"Project mismatch\" in (resp.results[0].error or \"\")\n\n\n@pytest.mark.asyncio\nasync def test_restore_create_new(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\")\n    mock_storage.download.return_value = _make_archive_bytes([dep])\n    mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=None)\n\n    req = RestoreRequest(backup_id=\"b1\")\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    assert resp.results is not None\n    assert resp.results[0].action == \"created\"\n    mock_k8s.apply_deployment_crd.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_restore_with_secrets(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\")\n    mock_storage.download.return_value = _make_archive_bytes(\n        [dep], secrets={\"app1\": {\"KEY\": \"val\"}}\n    )\n    mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=None)\n\n    req = RestoreRequest(backup_id=\"b1\")\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    mock_k8s.apply_secret.assert_awaited_once()\n    mock_k8s.apply_deployment_crd.assert_awaited_once()\n\n\n@pytest.mark.asyncio\nasync def test_restore_with_deletions(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    dep = make_deployment(\"app1\")\n    mock_storage.download.return_value = _make_archive_bytes([dep])\n    mock_k8s.get_deployment_crd_raw = AsyncMock(return_value=None)\n    # Cluster has app1 + app2, backup only has app1\n    mock_k8s.get_all_deployment_crds = AsyncMock(\n        return_value=[make_raw_crd(\"app1\"), make_raw_crd(\"app2\")]\n    )\n\n    req = RestoreRequest(backup_id=\"b1\", include_deletions=True)\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"completed\"\n    assert resp.results is not None\n    actions = {r.name: r.action for r in resp.results}\n    assert actions[\"app1\"] == \"created\"\n    assert actions[\"app2\"] == \"deleted\"\n\n\n@pytest.mark.asyncio\nasync def test_restore_empty_backup_with_deletions_refused(\n    mock_storage: AsyncMock,\n    backup_service: BackupService,\n    mock_k8s: MagicMock,\n    mock_settings: MagicMock,\n) -> None:\n    mock_storage.download.return_value = _make_archive_bytes([])\n\n    req = RestoreRequest(backup_id=\"b1\", include_deletions=True)\n    with patch(K8S, mock_k8s), patch(SETTINGS, mock_settings):\n        resp = await backup_service.restore_backup(req)\n\n    assert resp.status == \"failed\"\n    assert resp.error is not None and \"zero entries\" in resp.error.lower()\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/test_encryption.py",
    "content": "\"\"\"Tests for encryption round-trip.\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\nfrom cryptography.exceptions import InvalidTag\nfrom llama_agents.control_plane.backup.encryption import (\n    NONCE_LENGTH,\n    SALT_LENGTH,\n    decrypt,\n    encrypt,\n)\n\n\ndef test_round_trip() -> None:\n    plaintext = b\"hello world\"\n    password = \"test-password\"\n    ciphertext = encrypt(plaintext, password)\n    assert decrypt(ciphertext, password) == plaintext\n\n\ndef test_tampered_ciphertext_raises_invalid_tag() -> None:\n    ciphertext = encrypt(b\"some data\", \"pw\")\n    corrupted = bytearray(ciphertext)\n    corrupted[-1] ^= 0xFF\n    with pytest.raises(InvalidTag):\n        decrypt(bytes(corrupted), \"pw\")\n\n\ndef test_wrong_password_raises_invalid_tag() -> None:\n    ciphertext = encrypt(b\"secret\", \"correct-password\")\n    with pytest.raises(InvalidTag):\n        decrypt(ciphertext, \"wrong-password\")\n\n\ndef test_empty_plaintext_round_trip() -> None:\n    ciphertext = encrypt(b\"\", \"pw\")\n    assert decrypt(ciphertext, \"pw\") == b\"\"\n\n\ndef test_short_data_raises_value_error() -> None:\n    min_length = SALT_LENGTH + NONCE_LENGTH + 16\n    too_short = b\"\\x00\" * (min_length - 1)\n    with pytest.raises(ValueError, match=\"too short\"):\n        decrypt(too_short, \"pw\")\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/test_schema.py",
    "content": "\"\"\"Tests for backup/restore schema models.\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nfrom llama_agents.core.schema.backups import (\n    BackupListResponse,\n    BackupResponse,\n    RestoreDeploymentResult,\n    RestoreRequest,\n    RestoreResponse,\n)\n\n\ndef test_backup_response() -> None:\n    resp = BackupResponse(\n        backup_id=\"backup-20250101-000000\",\n        status=\"completed\",\n        timestamp=datetime(2025, 1, 1, tzinfo=timezone.utc),\n        deployment_count=3,\n        size_bytes=1024,\n    )\n    assert resp.backup_id == \"backup-20250101-000000\"\n    assert resp.status == \"completed\"\n    assert resp.deployment_count == 3\n\n\ndef test_backup_response_defaults() -> None:\n    resp = BackupResponse(backup_id=\"b1\", status=\"completed\")\n    assert resp.timestamp is None\n    assert resp.deployment_count is None\n    assert resp.size_bytes is None\n    assert resp.error is None\n\n\ndef test_backup_list_response() -> None:\n    resp = BackupListResponse(\n        backups=[\n            BackupResponse(backup_id=\"b1\", status=\"completed\"),\n            BackupResponse(backup_id=\"b2\", status=\"failed\", error=\"oops\"),\n        ]\n    )\n    assert len(resp.backups) == 2\n\n\ndef test_restore_request_defaults() -> None:\n    req = RestoreRequest(backup_id=\"b1\")\n    assert req.conflict_mode == \"skip\"\n    assert req.include_deletions is False\n\n\ndef test_restore_request_custom() -> None:\n    req = RestoreRequest(\n        backup_id=\"b1\",\n        conflict_mode=\"overwrite-always\",\n        include_deletions=True,\n    )\n    assert req.conflict_mode == \"overwrite-always\"\n    assert req.include_deletions is True\n\n\ndef test_restore_deployment_result() -> None:\n    result = RestoreDeploymentResult(name=\"app1\", action=\"created\")\n    assert result.error is None\n\n\ndef test_restore_response() -> None:\n    resp = RestoreResponse(\n        backup_id=\"b1\",\n        status=\"completed\",\n        results=[\n            RestoreDeploymentResult(name=\"app1\", action=\"created\"),\n            RestoreDeploymentResult(name=\"app2\", action=\"skipped\"),\n        ],\n    )\n    assert resp.results is not None and len(resp.results) == 2\n    assert resp.error is None\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/backup/test_storage.py",
    "content": "\"\"\"Tests for S3 backup storage.\"\"\"\n\nfrom __future__ import annotations\n\nimport boto3\nimport pytest\nfrom aiomoto import mock_aws\nfrom llama_agents.control_plane.backup.storage import (\n    S3BackupStorage,\n    generate_backup_id,\n)\n\n\ndef _make_storage() -> S3BackupStorage:\n    return S3BackupStorage(\n        bucket=\"test-bucket\",\n        region=\"us-east-1\",\n        access_key=\"testing\",\n        secret_key=\"testing\",\n    )\n\n\ndef _create_bucket() -> None:\n    s3 = boto3.client(\n        \"s3\",\n        region_name=\"us-east-1\",\n        aws_access_key_id=\"testing\",\n        aws_secret_access_key=\"testing\",\n    )\n    s3.create_bucket(Bucket=\"test-bucket\")\n\n\n@pytest.mark.asyncio\nasync def test_upload_download_round_trip() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        data = b\"archive-bytes-here\"\n        await storage.upload(\"backup-20250101-000000\", data)\n        downloaded = await storage.download(\"backup-20250101-000000\")\n        assert downloaded == data\n\n\n@pytest.mark.asyncio\nasync def test_list_returns_sorted_backups() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        await storage.upload(\"backup-20250101-000000\", b\"a\")\n        await storage.upload(\"backup-20250102-000000\", b\"b\")\n        await storage.upload(\"backup-20250103-000000\", b\"c\")\n\n        backups = await storage.list_backups()\n        ids = [b.backup_id for b in backups]\n        assert len(ids) == 3\n        assert set(ids) == {\n            \"backup-20250101-000000\",\n            \"backup-20250102-000000\",\n            \"backup-20250103-000000\",\n        }\n        timestamps = [b.timestamp for b in backups]\n        assert timestamps == sorted(timestamps, reverse=True)\n\n\n@pytest.mark.asyncio\nasync def test_delete_removes_backup() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        await storage.upload(\"backup-del\", b\"data\")\n        assert await storage.get_info(\"backup-del\") is not None\n\n        await storage.delete(\"backup-del\")\n        assert await storage.get_info(\"backup-del\") is None\n\n\n@pytest.mark.asyncio\nasync def test_get_info_returns_none_for_missing() -> None:\n    with mock_aws():\n        _create_bucket()\n        storage = _make_storage()\n        assert await storage.get_info(\"nonexistent\") is None\n\n\ndef test_generate_backup_id_format() -> None:\n    backup_id = generate_backup_id()\n    assert backup_id.startswith(\"backup-\")\n    parts = backup_id.split(\"-\")\n    assert len(parts) == 3\n    assert len(parts[1]) == 8  # YYYYMMDD\n    assert len(parts[2]) == 6  # HHMMSS\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/code_repo/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-control-plane/tests/code_repo/conftest.py",
    "content": "\"\"\"Shared test fixtures for code_repo tests.\"\"\"\n\nfrom __future__ import annotations\n\nimport time\nfrom pathlib import Path\n\nimport boto3\nfrom dulwich.objects import Blob, Commit, Tree\nfrom dulwich.refs import Ref\nfrom dulwich.repo import Repo\nfrom llama_agents.control_plane.code_repo.storage import CodeRepoStorage\n\nTEST_BUCKET = \"test-bucket\"\nTEST_REGION = \"us-east-1\"\nTEST_AWS_KEY = \"testing\"\n\n\ndef make_storage() -> CodeRepoStorage:\n    return CodeRepoStorage(\n        bucket=TEST_BUCKET,\n        region=TEST_REGION,\n        access_key=TEST_AWS_KEY,\n        secret_key=TEST_AWS_KEY,\n    )\n\n\ndef create_bucket() -> None:\n    s3 = boto3.client(\n        \"s3\",\n        region_name=TEST_REGION,\n        aws_access_key_id=TEST_AWS_KEY,\n        aws_secret_access_key=TEST_AWS_KEY,\n    )\n    s3.create_bucket(Bucket=TEST_BUCKET)\n\n\ndef create_test_repo(path: Path) -> Repo:\n    \"\"\"Create a bare repo with a single commit.\"\"\"\n    path.mkdir(parents=True, exist_ok=True)\n    repo = Repo.init_bare(str(path))\n    blob = Blob.from_string(b\"hello world\")\n    repo.object_store.add_object(blob)\n    tree = Tree()\n    tree.add(b\"test.txt\", 0o100644, blob.id)\n    repo.object_store.add_object(tree)\n    commit = Commit()\n    commit.tree = tree.id\n    commit.author = commit.committer = b\"Test User <test@example.com>\"\n    commit.commit_time = commit.author_time = int(time.time())\n    commit.commit_timezone = commit.author_timezone = 0\n    commit.encoding = b\"UTF-8\"\n    commit.message = b\"Initial commit\"\n    repo.object_store.add_object(commit)\n    repo.refs[Ref(b\"refs/heads/main\")] = commit.id\n    repo.refs.set_symbolic_ref(Ref(b\"HEAD\"), Ref(b\"refs/heads/main\"))\n    return repo\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/code_repo/test_git_server.py",
    "content": "\"\"\"Tests for the git HTTP server backed by dulwich and S3.\"\"\"\n\nfrom __future__ import annotations\n\nimport io\nimport shutil\nimport time\nfrom pathlib import Path\nfrom typing import Any, cast\nfrom unittest.mock import AsyncMock\n\nimport httpx\nimport pytest\nfrom aiomoto import mock_aws\nfrom dulwich.object_format import DEFAULT_OBJECT_FORMAT\nfrom dulwich.objects import Blob, Commit, ShaFile, Tree\nfrom dulwich.pack import UnpackedObject, write_pack_data\nfrom dulwich.protocol import ZERO_SHA\nfrom dulwich.refs import Ref\nfrom dulwich.repo import Repo\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import Response\nfrom httpx import ASGITransport\nfrom llama_agents.control_plane.code_repo import git_server as git_server_module\nfrom llama_agents.control_plane.code_repo.git_server import (\n    handle_git_request,\n    handle_git_request_readonly,\n)\nfrom llama_agents.control_plane.code_repo.storage import CodeRepoStorage\n\nfrom .conftest import create_bucket, create_test_repo, make_storage\n\n\ndef _add_commit_to_repo(repo: Repo, filename: bytes, content: bytes) -> Commit:\n    \"\"\"Add another commit to an existing repo.\"\"\"\n    blob = Blob.from_string(content)\n    repo.object_store.add_object(blob)\n\n    # Build tree from previous commit's tree plus new file\n    prev_commit = cast(Commit, repo[repo.refs[Ref(b\"refs/heads/main\")]])\n    old_tree = cast(Tree, repo[prev_commit.tree])\n    tree = Tree()\n    for item in old_tree.items():\n        tree.add(*item)\n    tree.add(filename, 0o100644, blob.id)\n    repo.object_store.add_object(tree)\n\n    commit = Commit()\n    commit.tree = tree.id\n    commit.parents = [prev_commit.id]\n    commit.author = commit.committer = b\"Test User <test@example.com>\"\n    commit.commit_time = commit.author_time = int(time.time())\n    commit.commit_timezone = commit.author_timezone = 0\n    commit.encoding = b\"UTF-8\"\n    commit.message = b\"Second commit\"\n    repo.object_store.add_object(commit)\n    repo.refs[Ref(b\"refs/heads/main\")] = commit.id\n    return commit\n\n\ndef _sha_file_to_unpacked(obj: ShaFile) -> UnpackedObject:\n    \"\"\"Convert a dulwich ShaFile to an UnpackedObject for pack writing.\"\"\"\n    raw = obj.as_raw_string()\n    return UnpackedObject(\n        obj.type_num,\n        decomp_chunks=[raw],\n        decomp_len=len(raw),\n        sha=obj.id,\n    )\n\n\ndef _collect_repo_objects(repo: Repo) -> list[ShaFile]:\n    \"\"\"Walk a repo and collect all objects reachable from refs.\"\"\"\n    objects: list[ShaFile] = []\n    seen: set[bytes] = set()\n    include = [sha for sha in repo.refs.allkeys() if not sha == b\"HEAD\"]\n    include_shas = [repo.refs[ref] for ref in include]\n    for entry in repo.get_walker(include=include_shas):\n        commit_obj = entry.commit\n        if commit_obj.id not in seen:\n            objects.append(commit_obj)\n            seen.add(commit_obj.id)\n        tree_obj = cast(Tree, repo[commit_obj.tree])\n        if tree_obj.id not in seen:\n            objects.append(tree_obj)\n            seen.add(tree_obj.id)\n        for item in tree_obj.items():\n            obj = repo[item.sha]\n            if obj.id not in seen:\n                objects.append(obj)\n                seen.add(obj.id)\n    return objects\n\n\ndef _build_receive_pack_body(\n    objects: list[ShaFile],\n    old_sha: bytes,\n    new_sha: bytes,\n    ref_name: str = \"refs/heads/main\",\n) -> bytes:\n    \"\"\"Build a git-receive-pack request body with ref update and pack data.\"\"\"\n    body = io.BytesIO()\n\n    # Write the ref update pkt-line\n    old_hex = old_sha.decode(\"ascii\")\n    new_hex = new_sha.decode(\"ascii\")\n    ref_line = (\n        f\"{old_hex} {new_hex} {ref_name}\\x00 report-status side-band-64k\"\n    ).encode(\"ascii\")\n    pkt_line = f\"{len(ref_line) + 4:04x}\".encode(\"ascii\") + ref_line\n    body.write(pkt_line)\n    body.write(b\"0000\")  # flush-pkt\n\n    # Write pack data\n    pack_buf = io.BytesIO()\n    unpacked = [_sha_file_to_unpacked(obj) for obj in objects]\n    write_pack_data(\n        pack_buf.write,\n        iter(unpacked),\n        DEFAULT_OBJECT_FORMAT,\n        num_records=len(unpacked),\n    )\n    body.write(pack_buf.getvalue())\n\n    return body.getvalue()\n\n\ndef _make_test_app(\n    storage: CodeRepoStorage,\n    on_push_complete: Any = None,\n    readonly: bool = False,\n) -> FastAPI:\n    \"\"\"Create a minimal FastAPI app wired to the git handlers.\"\"\"\n    app = FastAPI()\n\n    if readonly:\n\n        @app.api_route(\"/git/{git_path:path}\", methods=[\"GET\", \"POST\"])\n        async def git_readonly_handler(request: Request, git_path: str) -> Response:\n            return await handle_git_request_readonly(\n                request=request,\n                deployment_id=\"test-deploy\",\n                git_path=git_path,\n                storage=storage,\n            )\n\n    else:\n\n        @app.api_route(\"/git/{git_path:path}\", methods=[\"GET\", \"POST\"])\n        async def git_handler(request: Request, git_path: str) -> Response:\n            return await handle_git_request(\n                request=request,\n                deployment_id=\"test-deploy\",\n                git_path=git_path,\n                storage=storage,\n                on_push_complete=on_push_complete,\n            )\n\n    return app\n\n\n@pytest.mark.asyncio\nasync def test_info_refs_receive_pack_empty_repo() -> None:\n    \"\"\"GET /info/refs?service=git-receive-pack on empty repo returns valid discovery.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n        app = _make_test_app(storage)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-receive-pack\"}\n            )\n            assert response.status_code == 200\n            assert b\"service=git-receive-pack\" in response.content\n            content_type = response.headers.get(\"content-type\", \"\")\n            assert \"application/x-git-receive-pack-advertisement\" in content_type\n\n\n@pytest.mark.asyncio\nasync def test_info_refs_upload_pack_existing_repo(tmp_path: Path) -> None:\n    \"\"\"GET /info/refs?service=git-upload-pack returns refs from existing repo.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        # Upload a repo with a commit\n        repo_path = tmp_path / \"repo\"\n        repo = create_test_repo(repo_path)\n        head_sha = repo.refs[Ref(b\"refs/heads/main\")]\n        await storage.upload_repo(\"test-deploy\", repo_path)\n\n        app = _make_test_app(storage)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-upload-pack\"}\n            )\n            assert response.status_code == 200\n            assert b\"service=git-upload-pack\" in response.content\n            # The response should contain the commit SHA\n            assert head_sha in response.content\n            content_type = response.headers.get(\"content-type\", \"\")\n            assert \"application/x-git-upload-pack-advertisement\" in content_type\n\n\n@pytest.mark.asyncio\nasync def test_push_to_empty_repo_triggers_callback_and_uploads(\n    tmp_path: Path,\n) -> None:\n    \"\"\"Full push to empty repo: uploads to S3 and calls on_push_complete.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n        mock_callback = AsyncMock()\n\n        # Create a local repo with a commit to push from\n        local_repo_path = tmp_path / \"local\"\n        local_repo = create_test_repo(local_repo_path)\n        head_sha = local_repo.refs[Ref(b\"refs/heads/main\")]\n\n        app = _make_test_app(storage, on_push_complete=mock_callback)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            # Step 1: info/refs discovery\n            refs_response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-receive-pack\"}\n            )\n            assert refs_response.status_code == 200\n\n            # Step 2: Build and send the pack\n            objects = _collect_repo_objects(local_repo)\n            body = _build_receive_pack_body(objects, ZERO_SHA, head_sha)\n\n            pack_response = await client.post(\n                \"/git/git-receive-pack\",\n                content=body,\n                headers={\n                    \"Content-Type\": \"application/x-git-receive-pack-request\",\n                },\n            )\n            assert pack_response.status_code == 200\n\n        # Verify the repo was uploaded to S3\n        assert await storage.repo_exists(\"test-deploy\")\n\n        # Verify the callback was called\n        mock_callback.assert_called_once()\n        call_args = mock_callback.call_args\n        assert call_args[0][0] == \"test-deploy\"  # deployment_id\n        assert call_args[0][1] == head_sha.decode()  # new_sha\n        assert call_args[0][2] == \"main\"  # git_ref\n\n\n@pytest.mark.asyncio\nasync def test_push_with_chunked_transfer_encoding(tmp_path: Path) -> None:\n    \"\"\"Push with Transfer-Encoding: chunked succeeds (ASGI de-chunks the body).\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n        mock_callback = AsyncMock()\n\n        local_repo_path = tmp_path / \"local\"\n        local_repo = create_test_repo(local_repo_path)\n        head_sha = local_repo.refs[Ref(b\"refs/heads/main\")]\n\n        app = _make_test_app(storage, on_push_complete=mock_callback)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            refs_response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-receive-pack\"}\n            )\n            assert refs_response.status_code == 200\n\n            objects = _collect_repo_objects(local_repo)\n            body = _build_receive_pack_body(objects, ZERO_SHA, head_sha)\n\n            # Send with Transfer-Encoding: chunked header to simulate what\n            # dulwich's HTTP client does via urllib3. The ASGI server de-chunks\n            # the body, but the header is preserved in the WSGI environ.\n            pack_response = await client.post(\n                \"/git/git-receive-pack\",\n                content=body,\n                headers={\n                    \"Content-Type\": \"application/x-git-receive-pack-request\",\n                    \"Transfer-Encoding\": \"chunked\",\n                },\n            )\n            assert pack_response.status_code == 200\n\n        assert await storage.repo_exists(\"test-deploy\")\n        mock_callback.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_push_updates_existing_repo(tmp_path: Path) -> None:\n    \"\"\"Push to an existing repo updates S3 and fires callback with new SHA.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n        mock_callback = AsyncMock()\n\n        # Upload initial repo\n        repo_path = tmp_path / \"repo\"\n        repo = create_test_repo(repo_path)\n        first_sha = repo.refs[Ref(b\"refs/heads/main\")]\n        await storage.upload_repo(\"test-deploy\", repo_path)\n\n        # Add a second commit\n        second_commit = _add_commit_to_repo(repo, b\"file2.txt\", b\"second file\")\n        second_sha = second_commit.id\n\n        app = _make_test_app(storage, on_push_complete=mock_callback)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            # info/refs discovery\n            refs_response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-receive-pack\"}\n            )\n            assert refs_response.status_code == 200\n\n            # Build pack with all objects (server may already have some,\n            # but sending duplicates is safe in git protocol)\n            objects = _collect_repo_objects(repo)\n            body = _build_receive_pack_body(objects, first_sha, second_sha)\n\n            pack_response = await client.post(\n                \"/git/git-receive-pack\",\n                content=body,\n                headers={\n                    \"Content-Type\": \"application/x-git-receive-pack-request\",\n                },\n            )\n            assert pack_response.status_code == 200\n\n        # Verify callback was called with the new SHA\n        mock_callback.assert_called_once()\n        call_args = mock_callback.call_args\n        assert call_args[0][0] == \"test-deploy\"\n        assert call_args[0][1] == second_sha.decode()\n        assert call_args[0][2] == \"main\"\n\n        # Download from S3 and verify both commits are present\n        downloaded = await storage.download_repo(\"test-deploy\")\n        assert downloaded is not None\n        try:\n            dl_repo = Repo(str(downloaded))\n            assert dl_repo.refs[Ref(b\"refs/heads/main\")] == second_sha\n            # Walk history to verify both commits exist\n            walker = dl_repo.get_walker()\n            commit_shas = [entry.commit.id for entry in walker]\n            assert second_sha in commit_shas\n            assert first_sha in commit_shas\n        finally:\n            shutil.rmtree(downloaded.parent, ignore_errors=True)\n\n\n@pytest.mark.asyncio\nasync def test_readonly_rejects_receive_pack(tmp_path: Path) -> None:\n    \"\"\"The readonly handler returns 403 for git-receive-pack POST requests.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        # Need an existing repo so we don't get 404 first\n        repo_path = tmp_path / \"repo\"\n        create_test_repo(repo_path)\n        await storage.upload_repo(\"test-deploy\", repo_path)\n\n        app = _make_test_app(storage, readonly=True)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            # POST to git-receive-pack should be rejected\n            response = await client.post(\n                \"/git/git-receive-pack\",\n                content=b\"\",\n                headers={\n                    \"Content-Type\": \"application/x-git-receive-pack-request\",\n                },\n            )\n            assert response.status_code == 403\n            assert b\"Push not allowed\" in response.content\n\n            # info/refs for receive-pack also contains \"git-receive-pack\" in path\n            # but since the path is \"info/refs\", not \"git-receive-pack\",\n            # it passes through to dulwich. We verify upload-pack still works.\n            response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-upload-pack\"}\n            )\n            assert response.status_code == 200\n\n\n@pytest.mark.asyncio\nasync def test_readonly_returns_404_for_missing_repo() -> None:\n    \"\"\"The readonly handler returns 404 when no repo exists.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        app = _make_test_app(storage, readonly=True)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-upload-pack\"}\n            )\n            assert response.status_code == 404\n            assert b\"No code has been pushed\" in response.content\n\n\n@pytest.mark.asyncio\nasync def test_readonly_serves_upload_pack(tmp_path: Path) -> None:\n    \"\"\"The readonly handler successfully serves git-upload-pack requests.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        repo = create_test_repo(repo_path)\n        head_sha = repo.refs[Ref(b\"refs/heads/main\")]\n        await storage.upload_repo(\"test-deploy\", repo_path)\n\n        app = _make_test_app(storage, readonly=True)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-upload-pack\"}\n            )\n            assert response.status_code == 200\n            assert head_sha in response.content\n            content_type = response.headers.get(\"content-type\", \"\")\n            assert \"application/x-git-upload-pack-advertisement\" in content_type\n\n\n@pytest.mark.asyncio\nasync def test_handle_git_request_closes_repo_handles(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"The read/write handler closes each dulwich Repo it opens.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        create_test_repo(repo_path)\n        await storage.upload_repo(\"test-deploy\", repo_path)\n\n        close_calls = 0\n        original_close = git_server_module.Repo.close\n\n        def _spy_close(self: Repo) -> None:\n            nonlocal close_calls\n            close_calls += 1\n            original_close(self)\n\n        monkeypatch.setattr(git_server_module.Repo, \"close\", _spy_close)\n\n        app = _make_test_app(storage)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-upload-pack\"}\n            )\n\n        assert response.status_code == 200\n        assert close_calls == 1\n\n\n@pytest.mark.asyncio\nasync def test_handle_git_request_readonly_closes_repo_after_response(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"The readonly handler closes its dulwich Repo after streaming completes.\"\"\"\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        create_test_repo(repo_path)\n        await storage.upload_repo(\"test-deploy\", repo_path)\n\n        close_calls = 0\n        original_close = git_server_module.Repo.close\n\n        def _spy_close(self: Repo) -> None:\n            nonlocal close_calls\n            close_calls += 1\n            original_close(self)\n\n        monkeypatch.setattr(git_server_module.Repo, \"close\", _spy_close)\n\n        app = _make_test_app(storage, readonly=True)\n\n        async with httpx.AsyncClient(\n            transport=ASGITransport(app=app),\n            base_url=\"http://test\",\n        ) as client:\n            response = await client.get(\n                \"/git/info/refs\", params={\"service\": \"git-upload-pack\"}\n            )\n\n        assert response.status_code == 200\n        assert close_calls == 1\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/code_repo/test_storage.py",
    "content": "\"\"\"Tests for S3 code repo storage.\"\"\"\n\nfrom __future__ import annotations\n\nimport shutil\nfrom pathlib import Path\n\nimport pytest\nfrom aiomoto import mock_aws\nfrom dulwich.objects import Commit\nfrom dulwich.refs import Ref\nfrom dulwich.repo import Repo\nfrom llama_agents.control_plane.code_repo.storage import CodeRepoStorage\n\nfrom .conftest import create_bucket, create_test_repo, make_storage\n\n\n@pytest.mark.asyncio\nasync def test_round_trip_bare_repo(tmp_path: Path) -> None:\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        repo = create_test_repo(repo_path)\n        original_ref = repo.refs[Ref(b\"refs/heads/main\")]\n\n        await storage.upload_repo(\"deploy-1\", repo_path)\n\n        downloaded_path = await storage.download_repo(\"deploy-1\")\n        assert downloaded_path is not None\n        try:\n            downloaded_repo = Repo(str(downloaded_path))\n            assert downloaded_repo.refs[Ref(b\"refs/heads/main\")] == original_ref\n        finally:\n            shutil.rmtree(downloaded_path.parent, ignore_errors=True)\n\n\n@pytest.mark.asyncio\nasync def test_download_returns_none_when_no_repo() -> None:\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        result = await storage.download_repo(\"nonexistent-deploy\")\n        assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_repo_exists(tmp_path: Path) -> None:\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        create_test_repo(repo_path)\n\n        assert await storage.repo_exists(\"deploy-2\") is False\n\n        await storage.upload_repo(\"deploy-2\", repo_path)\n        assert await storage.repo_exists(\"deploy-2\") is True\n\n        await storage.delete_repo(\"deploy-2\")\n        assert await storage.repo_exists(\"deploy-2\") is False\n\n\n@pytest.mark.asyncio\nasync def test_init_bare_repo() -> None:\n    repo_path = CodeRepoStorage.init_bare_repo(\"deploy-3\")\n    try:\n        assert repo_path.exists()\n        assert (repo_path / \"HEAD\").exists()\n        assert (repo_path / \"objects\").is_dir()\n    finally:\n        shutil.rmtree(repo_path.parent, ignore_errors=True)\n\n\n@pytest.mark.asyncio\nasync def test_delete_repo(tmp_path: Path) -> None:\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        create_test_repo(repo_path)\n\n        await storage.upload_repo(\"deploy-4\", repo_path)\n        assert await storage.repo_exists(\"deploy-4\") is True\n\n        await storage.delete_repo(\"deploy-4\")\n\n        result = await storage.download_repo(\"deploy-4\")\n        assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_resolve_ref_supports_commit_sha_and_tags(tmp_path: Path) -> None:\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        repo = create_test_repo(repo_path)\n        commit_sha = repo.refs[Ref(b\"refs/heads/main\")].decode()\n        repo.refs[Ref(b\"refs/tags/v1.0.0\")] = repo.refs[Ref(b\"refs/heads/main\")]\n\n        await storage.upload_repo(\"deploy-5\", repo_path)\n\n        assert await storage.resolve_ref(\"deploy-5\", \"main\") == commit_sha\n        assert await storage.resolve_ref(\"deploy-5\", \"v1.0.0\") == commit_sha\n        assert await storage.resolve_ref(\"deploy-5\", commit_sha) == commit_sha\n        assert await storage.resolve_ref(\"deploy-5\", commit_sha[:8]) == commit_sha\n\n\n@pytest.mark.asyncio\nasync def test_resolve_ref_rejects_missing_or_non_commit_sha(tmp_path: Path) -> None:\n    with mock_aws():\n        create_bucket()\n        storage = make_storage()\n\n        repo_path = tmp_path / \"repo\"\n        repo = create_test_repo(repo_path)\n        commit = repo.get_object(repo.refs[Ref(b\"refs/heads/main\")])\n        assert isinstance(commit, Commit)\n        tree_sha = commit.tree.decode()\n\n        await storage.upload_repo(\"deploy-6\", repo_path)\n\n        assert await storage.resolve_ref(\"deploy-6\", \"missing-tag\") is None\n        assert await storage.resolve_ref(\"deploy-6\", tree_sha) is None\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/deployments/test_deployments_service.py",
    "content": "import types\nfrom collections.abc import AsyncGenerator\nfrom datetime import datetime, timezone\nfrom unittest import mock\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom fastapi import HTTPException\nfrom fastapi.responses import Response\nfrom llama_agents.control_plane.manage_api.deployments_service import (\n    DeploymentNotFoundError,\n    deployments_service,\n)\nfrom llama_agents.core import schema\nfrom llama_agents.core.schema.deployments import (\n    INTERNAL_CODE_REPO_SCHEME,\n    DeploymentResponse,\n)\n\n\ndef _make_deployment(\n    project_id: str = \"proj-1\", display_name: str = \"dep-1\"\n) -> DeploymentResponse:\n    return DeploymentResponse(\n        id=display_name,\n        display_name=display_name,\n        project_id=project_id,\n        repo_url=\"https://example.com/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"llama_deploy.yaml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n\ndef _rs(uid: str) -> types.SimpleNamespace:\n    obj = types.SimpleNamespace()\n    obj.metadata = types.SimpleNamespace(uid=uid)\n    return obj\n\n\ndef _line(pod: str, container: str, text: str) -> types.SimpleNamespace:\n    # Include timestamp to match real LogLine shape used by the service\n    return types.SimpleNamespace(\n        pod=pod,\n        container=container,\n        text=text,\n        timestamp=datetime.now(timezone.utc),\n    )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_stream_logs_missing_deployment(mock_get_deployment: AsyncMock) -> None:\n    mock_get_deployment.return_value = None\n    with pytest.raises(DeploymentNotFoundError):\n        _ = [\n            item\n            async for item in deployments_service.stream_deployment_logs(\n                project_id=\"proj-1\", deployment_id=\"dep-1\"\n            )\n        ]\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_stream_logs_project_mismatch(mock_get_deployment: AsyncMock) -> None:\n    mock_get_deployment.return_value = _make_deployment(project_id=\"other\")\n    with pytest.raises(DeploymentNotFoundError):\n        _ = [\n            item\n            async for item in deployments_service.stream_deployment_logs(\n                project_id=\"proj-1\", deployment_id=\"dep-1\"\n            )\n        ]\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.stream_build_job_logs\"\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_latest_replicaset_for_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_stream_logs_no_replicaset(\n    mock_get_deployment: AsyncMock,\n    mock_get_rs: AsyncMock,\n    mock_build_logs: MagicMock,\n) -> None:\n    mock_get_deployment.return_value = _make_deployment()\n    mock_get_rs.return_value = None\n\n    async def empty_gen() -> AsyncGenerator[types.SimpleNamespace, None]:\n        return\n        yield  # make it a generator\n\n    mock_build_logs.return_value = empty_gen()\n\n    items = [\n        item\n        async for item in deployments_service.stream_deployment_logs(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n    ]\n    assert len(items) == 0\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.stream_build_job_logs\"\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.stream_replicaset_logs\"\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_latest_replicaset_for_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_stream_logs_happy_path(\n    mock_get_deployment: AsyncMock,\n    mock_get_rs: AsyncMock,\n    mock_stream_logs: MagicMock,\n    mock_build_logs: MagicMock,\n) -> None:\n    mock_get_deployment.return_value = _make_deployment()\n    mock_get_rs.return_value = _rs(\"uid-1\")\n\n    async def agen() -> AsyncGenerator[types.SimpleNamespace, None]:\n        yield _line(\"pod-1\", \"c\", \"a\")\n        yield _line(\"pod-1\", \"c\", \"b\")\n\n    async def empty_gen() -> AsyncGenerator[types.SimpleNamespace, None]:\n        return\n        yield  # make it a generator\n\n    mock_stream_logs.return_value = agen()\n    mock_build_logs.return_value = empty_gen()\n\n    items = [\n        item\n        async for item in deployments_service.stream_deployment_logs(\n            project_id=\"proj-1\", deployment_id=\"dep-1\", include_init_containers=True\n        )\n    ]\n\n    assert len(items) == 2\n    assert items[0].pod == \"pod-1\" and items[0].text == \"a\"\n    assert items[1].pod == \"pod-1\" and items[1].text == \"b\"\n\n    mock_stream_logs.assert_called_once_with(\n        deployment_id=\"dep-1\",\n        include_init_containers=True,\n        since_seconds=None,\n        tail_lines=None,\n        stop_event=mock.ANY,\n        follow=True,\n    )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.stream_build_job_logs\"\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.stream_replicaset_logs\"\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_latest_replicaset_for_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_stream_logs_follow_false_threads_through_and_terminates(\n    mock_get_deployment: AsyncMock,\n    mock_get_rs: AsyncMock,\n    mock_stream_logs: MagicMock,\n    mock_build_logs: MagicMock,\n) -> None:\n    \"\"\"``follow=False`` skips the RS-change watcher and ends after one pass.\"\"\"\n    mock_get_deployment.return_value = _make_deployment()\n    mock_get_rs.return_value = _rs(\"uid-1\")\n\n    async def agen() -> AsyncGenerator[types.SimpleNamespace, None]:\n        yield _line(\"pod-1\", \"c\", \"a\")\n\n    async def empty_gen() -> AsyncGenerator[types.SimpleNamespace, None]:\n        return\n        yield  # make it a generator\n\n    mock_stream_logs.return_value = agen()\n    mock_build_logs.return_value = empty_gen()\n\n    items = [\n        item\n        async for item in deployments_service.stream_deployment_logs(\n            project_id=\"proj-1\", deployment_id=\"dep-1\", follow=False\n        )\n    ]\n\n    # Stream completes after one pass.\n    assert len(items) == 1\n    # ``follow=False`` should propagate to the K8s read.\n    mock_stream_logs.assert_called_once_with(\n        deployment_id=\"dep-1\",\n        include_init_containers=False,\n        since_seconds=None,\n        tail_lines=None,\n        stop_event=mock.ANY,\n        follow=False,\n    )\n    mock_build_logs.assert_called_once_with(\n        deployment_id=\"dep-1\",\n        since_seconds=None,\n        tail_lines=None,\n        stop_event=mock.ANY,\n        follow=False,\n    )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.stream_build_job_logs\"\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.stream_replicaset_logs\"\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_latest_replicaset_for_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_stream_logs_restarts_on_rs_change(\n    mock_get_deployment: AsyncMock,\n    mock_get_rs: AsyncMock,\n    mock_stream_logs: MagicMock,\n    mock_build_logs: MagicMock,\n) -> None:\n    \"\"\"When the RS changes, the stream restarts with the new RS's pods.\"\"\"\n    mock_get_deployment.return_value = _make_deployment()\n    # Provide enough RS values for both iterations:\n    # Iteration 1: initial_rs_uid check, _when_replicaset_changes initial + polls\n    # Iteration 2: initial_rs_uid check, _when_replicaset_changes initial + polls\n    mock_get_rs.side_effect = [\n        _rs(\"uid-1\"),  # iter 1: initial_rs_uid\n        _rs(\"uid-1\"),  # iter 1: _when_replicaset_changes initial\n        _rs(\"uid-1\"),  # iter 1: poll\n        _rs(\"uid-2\"),  # iter 1: poll → change detected, rs_changed=True\n        _rs(\"uid-2\"),  # iter 2: initial_rs_uid\n        _rs(\"uid-2\"),  # iter 2: _when_replicaset_changes initial\n    ] + [_rs(\"uid-2\")] * 50  # iter 2: polls (stable, no change)\n\n    async def gen1() -> AsyncGenerator[types.SimpleNamespace, None]:\n        import asyncio\n\n        for i in range(100):\n            yield _line(\"pod-1\", \"c\", f\"line-{i}\")\n            await asyncio.sleep(0)\n\n    async def gen2() -> AsyncGenerator[types.SimpleNamespace, None]:\n        yield _line(\"pod-2\", \"c\", \"after-rs-change\")\n\n    async def empty_gen() -> AsyncGenerator[types.SimpleNamespace, None]:\n        return\n        yield\n\n    # First call returns gen1 (cut short by RS change), second returns gen2\n    mock_stream_logs.side_effect = [gen1(), gen2()]\n    mock_build_logs.return_value = empty_gen()\n\n    items = [\n        item\n        async for item in deployments_service.stream_deployment_logs(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n    ]\n\n    # Should have some items from gen1 (cut short) + 1 item from gen2\n    first_rs_items = [i for i in items if i.pod == \"pod-1\"]\n    second_rs_items = [i for i in items if i.pod == \"pod-2\"]\n    assert len(first_rs_items) < 100\n    assert len(second_rs_items) == 1\n    assert second_rs_items[0].text == \"after-rs-change\"\n\n\n# --- Project mismatch / missing deployment tests ---\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_get_deployment_project_mismatch(mock_get_deployment: AsyncMock) -> None:\n    mock_get_deployment.return_value = _make_deployment(project_id=\"other\")\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.get_deployment(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_get_deployment_missing(mock_get_deployment: AsyncMock) -> None:\n    mock_get_deployment.return_value = None\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.get_deployment(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_delete_deployment_project_mismatch(\n    mock_get_deployment: AsyncMock,\n) -> None:\n    mock_get_deployment.return_value = _make_deployment(project_id=\"other\")\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.delete_deployment(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_delete_deployment_missing(mock_get_deployment: AsyncMock) -> None:\n    mock_get_deployment.return_value = None\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.delete_deployment(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n    new=None,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.delete_all_artifacts_for_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.delete_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_delete_deployment_success_no_code_repo(\n    mock_get_deployment: AsyncMock,\n    mock_k8s_delete: AsyncMock,\n    mock_delete_artifacts: AsyncMock,\n) -> None:\n    \"\"\"Happy path: deletes k8s resources and artifacts, skips repo when storage is None.\"\"\"\n    mock_get_deployment.return_value = _make_deployment()\n    await deployments_service.delete_deployment(\n        project_id=\"proj-1\", deployment_id=\"dep-1\"\n    )\n    mock_k8s_delete.assert_awaited_once_with(deployment_id=\"dep-1\")\n    mock_delete_artifacts.assert_awaited_once_with(\"dep-1\")\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.delete_all_artifacts_for_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.delete_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_delete_deployment_cleans_up_code_repo(\n    mock_get_deployment: AsyncMock,\n    mock_k8s_delete: AsyncMock,\n    mock_delete_artifacts: AsyncMock,\n    mock_code_repo_storage: MagicMock,\n) -> None:\n    \"\"\"When code_repo_storage is configured, delete_repo is called for the deployment.\"\"\"\n    mock_code_repo_storage.delete_repo = AsyncMock()\n    mock_get_deployment.return_value = _make_deployment()\n    await deployments_service.delete_deployment(\n        project_id=\"proj-1\", deployment_id=\"dep-1\"\n    )\n    mock_k8s_delete.assert_awaited_once_with(deployment_id=\"dep-1\")\n    mock_delete_artifacts.assert_awaited_once_with(\"dep-1\")\n    mock_code_repo_storage.delete_repo.assert_awaited_once_with(\"dep-1\")\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.delete_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_delete_deployment_k8s_error_propagates(\n    mock_get_deployment: AsyncMock,\n    mock_delete_deployment: AsyncMock,\n) -> None:\n    \"\"\"Non-404 K8s errors during delete must propagate, not be swallowed.\"\"\"\n    from kubernetes.client.exceptions import ApiException\n\n    mock_get_deployment.return_value = _make_deployment()\n    mock_delete_deployment.side_effect = ApiException(\n        status=500, reason=\"Internal Server Error\"\n    )\n    with pytest.raises(ApiException):\n        await deployments_service.delete_deployment(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_get_deployment_history_project_mismatch(\n    mock_get_deployment: AsyncMock,\n) -> None:\n    mock_get_deployment.return_value = _make_deployment(project_id=\"other\")\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.get_deployment_history(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_get_deployment_history_missing(\n    mock_get_deployment: AsyncMock,\n) -> None:\n    mock_get_deployment.return_value = None\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.get_deployment_history(\n            project_id=\"proj-1\", deployment_id=\"dep-1\"\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_rollback_deployment_project_mismatch(\n    mock_get_deployment: AsyncMock,\n) -> None:\n    mock_get_deployment.return_value = _make_deployment(project_id=\"other\")\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.rollback_deployment(\n            project_id=\"proj-1\",\n            deployment_id=\"dep-1\",\n            request=schema.RollbackRequest(git_sha=\"abc123\"),\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_rollback_deployment_missing(mock_get_deployment: AsyncMock) -> None:\n    mock_get_deployment.return_value = None\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.rollback_deployment(\n            project_id=\"proj-1\",\n            deployment_id=\"dep-1\",\n            request=schema.RollbackRequest(git_sha=\"abc123\"),\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_update_deployment_project_mismatch(\n    mock_get_deployment: AsyncMock,\n) -> None:\n    mock_get_deployment.return_value = _make_deployment(project_id=\"other\")\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.update_deployment(\n            project_id=\"proj-1\",\n            deployment_id=\"dep-1\",\n            update_data=schema.DeploymentUpdate(),\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_update_deployment_missing(mock_get_deployment: AsyncMock) -> None:\n    mock_get_deployment.return_value = None\n    with pytest.raises(DeploymentNotFoundError):\n        await deployments_service.update_deployment(\n            project_id=\"proj-1\",\n            deployment_id=\"dep-1\",\n            update_data=schema.DeploymentUpdate(),\n        )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_update_deployment_unresolvable_ref_returns_400(\n    mock_get_deployment: AsyncMock,\n    mock_code_repo_storage: MagicMock,\n) -> None:\n    \"\"\"Updating an internal deployment to a ref that can't be resolved returns 400.\"\"\"\n    dep = _make_deployment()\n    dep.repo_url = INTERNAL_CODE_REPO_SCHEME\n    mock_get_deployment.return_value = dep\n    mock_code_repo_storage.resolve_ref = AsyncMock(return_value=None)\n\n    with pytest.raises(HTTPException) as exc_info:\n        await deployments_service.update_deployment(\n            project_id=\"proj-1\",\n            deployment_id=\"dep-1\",\n            update_data=schema.DeploymentUpdate(git_ref=\"nonexistent-branch\"),\n        )\n    assert exc_info.value.status_code == 400\n    assert \"nonexistent-branch\" in str(exc_info.value.detail)\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n    new=None,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_update_deployment_internal_ref_requires_storage(\n    mock_get_deployment: AsyncMock,\n) -> None:\n    \"\"\"Updating an internal deployment ref requires configured code repo storage.\"\"\"\n    dep = _make_deployment()\n    dep.repo_url = INTERNAL_CODE_REPO_SCHEME\n    mock_get_deployment.return_value = dep\n\n    with pytest.raises(HTTPException) as exc_info:\n        await deployments_service.update_deployment(\n            project_id=\"proj-1\",\n            deployment_id=\"dep-1\",\n            update_data=schema.DeploymentUpdate(git_ref=\"main\"),\n        )\n\n    assert exc_info.value.status_code == 503\n    assert \"S3_BUCKET\" in str(exc_info.value.detail)\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n    new=None,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.update_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_update_deployment_internal_path_change_skips_storage_resolution(\n    mock_get_deployment: AsyncMock,\n    mock_update_deployment: AsyncMock,\n) -> None:\n    \"\"\"Changing only deployment_file_path should not resolve internal repo refs.\"\"\"\n    dep = _make_deployment()\n    dep.repo_url = INTERNAL_CODE_REPO_SCHEME\n    mock_get_deployment.return_value = dep\n    mock_update_deployment.return_value = dep.model_copy(\n        update={\"deployment_file_path\": \"new/deploy.yml\"}\n    )\n\n    response = await deployments_service.update_deployment(\n        project_id=\"proj-1\",\n        deployment_id=\"dep-1\",\n        update_data=schema.DeploymentUpdate(deployment_file_path=\"new/deploy.yml\"),\n    )\n\n    assert response.deployment_file_path == \"new/deploy.yml\"\n    mock_update_deployment.assert_awaited_once()\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n    new=None,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.create_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.git_service.validate_git_application\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_create_deployment_empty_repo_requires_internal_storage(\n    mock_validate_git_application: AsyncMock,\n    mock_create_deployment: AsyncMock,\n) -> None:\n    with pytest.raises(HTTPException) as exc_info:\n        await deployments_service.create_deployment(\n            project_id=\"proj-1\",\n            deployment_data=schema.DeploymentCreate(display_name=\"dep-1\", repo_url=\"\"),\n        )\n\n    assert exc_info.value.status_code == 503\n    assert \"S3_BUCKET\" in str(exc_info.value.detail)\n    mock_validate_git_application.assert_not_awaited()\n    mock_create_deployment.assert_not_awaited()\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service._handle_git_request\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_handle_git_request_rejects_external_repo_deployments(\n    mock_get_deployment: AsyncMock,\n    mock_code_repo_storage: MagicMock,\n    mock_handle_git_request: AsyncMock,\n) -> None:\n    mock_get_deployment.return_value = _make_deployment()\n\n    with pytest.raises(HTTPException) as exc_info:\n        await deployments_service.handle_git_request(\n            request=MagicMock(),\n            project_id=\"proj-1\",\n            deployment_id=\"dep-1\",\n            git_path=\"info/refs\",\n        )\n\n    assert exc_info.value.status_code == 409\n    assert \"external repository\" in str(exc_info.value.detail)\n    mock_handle_git_request.assert_not_awaited()\n    assert mock_code_repo_storage is not None\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service._handle_git_request\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n)\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_handle_git_request_allows_push_mode_deployments(\n    mock_get_deployment: AsyncMock,\n    mock_code_repo_storage: MagicMock,\n    mock_handle_git_request: AsyncMock,\n) -> None:\n    deployment = _make_deployment()\n    deployment.repo_url = \"\"\n    mock_get_deployment.return_value = deployment\n    mock_handle_git_request.return_value = Response(status_code=200)\n\n    response = await deployments_service.handle_git_request(\n        request=MagicMock(),\n        project_id=\"proj-1\",\n        deployment_id=\"dep-1\",\n        git_path=\"info/refs\",\n    )\n\n    assert response.status_code == 200\n    mock_handle_git_request.assert_awaited_once()\n    await_args = mock_handle_git_request.await_args\n    assert await_args is not None\n    assert await_args.kwargs[\"storage\"] is mock_code_repo_storage\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_build_app.py",
    "content": "from unittest.mock import MagicMock, patch\n\nimport httpx\nimport pytest\nfrom llama_agents.control_plane.build_api.build_app import build_app\n\n\n@pytest.mark.anyio\nasync def test_health_returns_503_when_s3_not_configured() -> None:\n    async with httpx.AsyncClient(\n        transport=httpx.ASGITransport(app=build_app), base_url=\"http://test\"\n    ) as client:\n        with patch(\n            \"llama_agents.control_plane.build_api.build_app.build_artifact_storage\",\n            None,\n        ):\n            response = await client.get(\"/health\")\n    assert response.status_code == 503\n    data = response.json()\n    assert data[\"status\"] == \"unhealthy\"\n    assert \"S3_BUCKET\" in data[\"reason\"]\n\n\n@pytest.mark.anyio\nasync def test_health_returns_200_when_s3_configured() -> None:\n    mock_storage = MagicMock()\n    async with httpx.AsyncClient(\n        transport=httpx.ASGITransport(app=build_app), base_url=\"http://test\"\n    ) as client:\n        with patch(\n            \"llama_agents.control_plane.build_api.build_app.build_artifact_storage\",\n            mock_storage,\n        ):\n            response = await client.get(\"/health\")\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"status\"] == \"ok\"\n    assert data[\"service\"] == \"build-api\"\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_build_app_ssrf.py",
    "content": "import socket\nfrom unittest.mock import patch\n\nimport pytest\nfrom fastapi import HTTPException\nfrom llama_agents.control_plane.build_api.build_app import (\n    _validate_git_path,\n    _validate_url_not_private,\n)\n\n# -- git path validation --\n\n\n@pytest.mark.parametrize(\n    \"path\",\n    [\n        \"info/refs\",\n        \"HEAD\",\n        \"objects/pack/pack-abc123.pack\",\n        \"git-upload-pack\",\n        \"git-receive-pack\",\n    ],\n)\ndef test_validate_git_path_allows_valid_paths(path: str) -> None:\n    _validate_git_path(path)\n\n\n@pytest.mark.parametrize(\n    \"path\",\n    [\n        \"../../etc/passwd\",\n        \"some/random/path\",\n    ],\n)\ndef test_validate_git_path_rejects_invalid_paths(path: str) -> None:\n    with pytest.raises(HTTPException) as exc:\n        _validate_git_path(path)\n    assert exc.value.status_code == 400\n\n\n# -- URL SSRF validation --\n\n\n@pytest.mark.parametrize(\n    (\"ip\", \"family\", \"addr\"),\n    [\n        (\"127.0.0.1\", socket.AF_INET, (\"127.0.0.1\", 0)),\n        (\"10.0.0.1\", socket.AF_INET, (\"10.0.0.1\", 0)),\n        (\"172.16.0.1\", socket.AF_INET, (\"172.16.0.1\", 0)),\n        (\"192.168.1.1\", socket.AF_INET, (\"192.168.1.1\", 0)),\n        (\"169.254.169.254\", socket.AF_INET, (\"169.254.169.254\", 0)),\n        (\"::1\", socket.AF_INET6, (\"::1\", 0, 0, 0)),\n    ],\n    ids=[\n        \"loopback\",\n        \"private-10\",\n        \"private-172\",\n        \"private-192\",\n        \"link-local\",\n        \"ipv6-loopback\",\n    ],\n)\ndef test_validate_url_blocks_private_ips(\n    ip: str, family: socket.AddressFamily, addr: tuple[str, ...]\n) -> None:\n    with patch(\n        \"socket.getaddrinfo\",\n        return_value=[(family, 0, 0, \"\", addr)],\n    ):\n        with pytest.raises(HTTPException) as exc:\n            _validate_url_not_private(\"http://example.com/repo.git\")\n        assert exc.value.status_code == 403\n\n\ndef test_validate_url_allows_public_ip() -> None:\n    with patch(\n        \"socket.getaddrinfo\",\n        return_value=[(socket.AF_INET, 0, 0, \"\", (\"140.82.121.3\", 0))],\n    ):\n        _validate_url_not_private(\"https://github.com/owner/repo.git\")\n\n\ndef test_validate_url_blocks_dns_failure() -> None:\n    with patch(\n        \"socket.getaddrinfo\", side_effect=socket.gaierror(\"Name resolution failed\")\n    ):\n        with pytest.raises(HTTPException) as exc:\n            _validate_url_not_private(\"http://nonexistent.example.com/repo.git\")\n        assert exc.value.status_code == 400\n\n\ndef test_validate_url_blocks_no_hostname() -> None:\n    with pytest.raises(HTTPException) as exc:\n        _validate_url_not_private(\"not-a-url\")\n    assert exc.value.status_code == 400\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_build_gc.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for build artifact garbage collection.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta, timezone\nfrom unittest.mock import AsyncMock, patch\n\nimport pytest\nfrom kubernetes.client import (\n    V1Container,\n    V1EnvVar,\n    V1LabelSelector,\n    V1PodSpec,\n    V1PodTemplateSpec,\n    V1ReplicaSet,\n    V1ReplicaSetSpec,\n)\nfrom llama_agents.control_plane import k8s_client\nfrom llama_agents.control_plane.build_api import build_gc\nfrom llama_agents.control_plane.build_api.build_storage import ArtifactInfo\n\n\n@dataclass\nclass FakeStorage:\n    \"\"\"Minimal fake of BuildArtifactStorage for GC tests.\n\n    Tracks list/delete calls; no real S3.\n    \"\"\"\n\n    artifacts: list[ArtifactInfo]\n    deleted: list[tuple[str, str]]\n\n    async def list_artifacts(self, deployment_name: str) -> list[ArtifactInfo]:\n        return [a for a in self.artifacts if a.deployment_name == deployment_name]\n\n    async def delete_artifact(self, deployment_name: str, build_id: str) -> None:\n        self.deleted.append((deployment_name, build_id))\n        self.artifacts = [\n            a\n            for a in self.artifacts\n            if not (a.deployment_name == deployment_name and a.build_id == build_id)\n        ]\n\n    async def delete_all_artifacts(self, deployment_name: str) -> int:\n        to_delete = [a for a in self.artifacts if a.deployment_name == deployment_name]\n        for a in to_delete:\n            self.deleted.append((a.deployment_name, a.build_id))\n        self.artifacts = [\n            a for a in self.artifacts if a.deployment_name != deployment_name\n        ]\n        return len(to_delete)\n\n\ndef _artifact(\n    deployment: str, build_id: str, *, age_seconds: int, now: datetime\n) -> ArtifactInfo:\n    return ArtifactInfo(\n        deployment_name=deployment,\n        build_id=build_id,\n        timestamp=now - timedelta(seconds=age_seconds),\n        size_bytes=1024,\n    )\n\n\ndef _replicaset_with_build_id(build_id: str) -> V1ReplicaSet:\n    \"\"\"Build a V1ReplicaSet whose pod template references the given build_id\n    via the LLAMA_DEPLOY_BUILD_ID env var — matching what the GC walks.\"\"\"\n    return V1ReplicaSet(\n        spec=V1ReplicaSetSpec(\n            selector=V1LabelSelector(match_labels={}),\n            template=V1PodTemplateSpec(\n                spec=V1PodSpec(\n                    containers=[\n                        V1Container(\n                            name=\"app\",\n                            env=[\n                                V1EnvVar(name=\"LLAMA_DEPLOY_BUILD_ID\", value=build_id)\n                            ],\n                        )\n                    ],\n                ),\n            ),\n        ),\n    )\n\n\n@pytest.fixture\ndef now() -> datetime:\n    return datetime(2026, 4, 8, 12, 0, 0, tzinfo=timezone.utc)\n\n\n@pytest.fixture\ndef fake_storage() -> FakeStorage:\n    return FakeStorage(artifacts=[], deleted=[])\n\n\n@pytest.fixture\ndef patched_gc(fake_storage: FakeStorage):\n    \"\"\"Patch module-level deps of gc_build_artifacts: storage + k8s client.\"\"\"\n    with (\n        patch.object(build_gc, \"build_artifact_storage\", fake_storage),\n        patch.object(\n            k8s_client,\n            \"list_replicasets_for_deployment\",\n            AsyncMock(return_value=[]),\n        ) as mock_list,\n    ):\n        yield fake_storage, mock_list\n\n\n@pytest.mark.asyncio\nasync def test_retains_two_recent_artifacts_for_same_deployment(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"Back-to-back uploads for different buildIds must both survive GC even\n    when no ReplicaSet references either yet.\"\"\"\n    storage, _ = patched_gc\n    storage.artifacts = [\n        _artifact(\"doc-extract\", \"build-a\", age_seconds=0, now=now),\n        _artifact(\"doc-extract\", \"build-b\", age_seconds=1, now=now),\n    ]\n\n    deleted = await build_gc.gc_build_artifacts(\"doc-extract\", now=now)\n\n    assert deleted == 0\n    assert storage.deleted == []\n    assert len(storage.artifacts) == 2\n\n\n@pytest.mark.asyncio\nasync def test_retains_artifact_within_grace_window(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"Artifact older than a few minutes but within the grace window is kept.\"\"\"\n    storage, _ = patched_gc\n    # 30 minutes old — well within default 4500s (75min) window\n    storage.artifacts = [\n        _artifact(\"app\", \"build-30m\", age_seconds=30 * 60, now=now),\n    ]\n\n    deleted = await build_gc.gc_build_artifacts(\"app\", now=now)\n\n    assert deleted == 0\n    assert storage.artifacts == [\n        _artifact(\"app\", \"build-30m\", age_seconds=30 * 60, now=now),\n    ]\n\n\n@pytest.mark.asyncio\nasync def test_deletes_unreferenced_artifact_past_grace_window(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"Artifact older than the grace window AND unreferenced by any RS is deleted.\"\"\"\n    storage, _ = patched_gc\n    # 2 hours old — well past the default 75min grace window\n    storage.artifacts = [\n        _artifact(\"app\", \"build-old\", age_seconds=2 * 60 * 60, now=now),\n    ]\n\n    deleted = await build_gc.gc_build_artifacts(\"app\", now=now)\n\n    assert deleted == 1\n    assert storage.deleted == [(\"app\", \"build-old\")]\n\n\n@pytest.mark.asyncio\nasync def test_retains_referenced_artifact_regardless_of_age(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"An artifact referenced by a live ReplicaSet must never be deleted, even if\n    it's arbitrarily old.\n    \"\"\"\n    storage, mock_list = patched_gc\n    storage.artifacts = [\n        _artifact(\"app\", \"build-ancient\", age_seconds=10 * 24 * 3600, now=now),\n    ]\n\n    mock_list.return_value = [_replicaset_with_build_id(\"build-ancient\")]\n\n    deleted = await build_gc.gc_build_artifacts(\"app\", now=now)\n\n    assert deleted == 0\n    assert storage.deleted == []\n\n\n@pytest.mark.asyncio\nasync def test_keep_build_ids_forces_retention(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"keep_build_ids is belt-and-suspenders: even for an aged-out artifact,\n    an explicit keep_build_ids entry prevents deletion.\"\"\"\n    storage, _ = patched_gc\n    storage.artifacts = [\n        _artifact(\"app\", \"build-old\", age_seconds=2 * 60 * 60, now=now),\n    ]\n\n    deleted = await build_gc.gc_build_artifacts(\n        \"app\", keep_build_ids={\"build-old\"}, now=now\n    )\n\n    assert deleted == 0\n    assert storage.deleted == []\n\n\n@pytest.mark.asyncio\nasync def test_mixed_cohort_only_aged_out_deleted(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"Given a mix of recent and aged-out artifacts, only the aged-out ones\n    that are also unreferenced get deleted.\"\"\"\n    storage, _ = patched_gc\n    storage.artifacts = [\n        _artifact(\"app\", \"build-recent\", age_seconds=60, now=now),\n        _artifact(\n            \"app\", \"build-middle\", age_seconds=60 * 60, now=now\n        ),  # 60m, within grace\n        _artifact(\"app\", \"build-old\", age_seconds=3 * 60 * 60, now=now),  # 3h\n        _artifact(\"app\", \"build-ancient\", age_seconds=10 * 3600, now=now),  # 10h\n    ]\n\n    deleted = await build_gc.gc_build_artifacts(\"app\", now=now)\n\n    assert deleted == 2\n    deleted_ids = {bid for _, bid in storage.deleted}\n    assert deleted_ids == {\"build-old\", \"build-ancient\"}\n\n\n@pytest.mark.asyncio\nasync def test_handles_naive_datetime_from_storage(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"Some S3 backends return naive datetimes; the GC must normalize before\n    comparison rather than raising a 'can't compare offset-naive to offset-aware'\n    TypeError.\"\"\"\n    storage, _ = patched_gc\n    naive_now = now.replace(tzinfo=None)\n    storage.artifacts = [\n        ArtifactInfo(\n            deployment_name=\"app\",\n            build_id=\"build-naive\",\n            timestamp=naive_now - timedelta(hours=3),  # aged out\n            size_bytes=100,\n        ),\n    ]\n\n    deleted = await build_gc.gc_build_artifacts(\"app\", now=now)\n\n    assert deleted == 1\n    assert storage.deleted == [(\"app\", \"build-naive\")]\n\n\n@pytest.mark.asyncio\nasync def test_returns_zero_when_storage_disabled(now: datetime) -> None:\n    with patch.object(build_gc, \"build_artifact_storage\", None):\n        deleted = await build_gc.gc_build_artifacts(\"app\", now=now)\n    assert deleted == 0\n\n\n@pytest.mark.asyncio\nasync def test_delete_all_artifacts_for_deployment(\n    patched_gc: tuple[FakeStorage, AsyncMock], now: datetime\n) -> None:\n    \"\"\"Deployment deletion path deletes everything regardless of age.\"\"\"\n    storage, _ = patched_gc\n    storage.artifacts = [\n        _artifact(\"app\", \"build-a\", age_seconds=1, now=now),\n        _artifact(\"app\", \"build-b\", age_seconds=100, now=now),\n    ]\n\n    count = await build_gc.delete_all_artifacts_for_deployment(\"app\")\n\n    assert count == 2\n    assert len(storage.deleted) == 2\n    assert storage.artifacts == []\n\n\n@pytest.mark.asyncio\nasync def test_partial_delete_failure_does_not_abort_remaining(now: datetime) -> None:\n    \"\"\"One failing delete in a concurrent batch must not prevent the others\n    from running; the returned count should reflect only successful deletes.\"\"\"\n    artifacts = [\n        _artifact(\"app\", \"build-good-1\", age_seconds=3 * 3600, now=now),\n        _artifact(\"app\", \"build-bad\", age_seconds=3 * 3600, now=now),\n        _artifact(\"app\", \"build-good-2\", age_seconds=3 * 3600, now=now),\n    ]\n    deleted: list[tuple[str, str]] = []\n\n    async def delete_artifact(deployment_name: str, build_id: str) -> None:\n        if build_id == \"build-bad\":\n            raise RuntimeError(\"simulated S3 failure\")\n        deleted.append((deployment_name, build_id))\n\n    fake = AsyncMock()\n    fake.list_artifacts = AsyncMock(return_value=artifacts)\n    fake.delete_artifact = AsyncMock(side_effect=delete_artifact)\n\n    with (\n        patch.object(build_gc, \"build_artifact_storage\", fake),\n        patch.object(\n            k8s_client,\n            \"list_replicasets_for_deployment\",\n            AsyncMock(return_value=[]),\n        ),\n    ):\n        count = await build_gc.gc_build_artifacts(\"app\", now=now)\n\n    assert count == 2\n    assert sorted(bid for _, bid in deleted) == [\"build-good-1\", \"build-good-2\"]\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_endpoints.py",
    "content": "\"\"\"Unit tests for API endpoints\"\"\"\n\nimport contextlib\nfrom collections.abc import AsyncGenerator\nfrom datetime import datetime\nfrom importlib.metadata import version as pkg_version\nfrom typing import Generator\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom llama_agents.control_plane.manage_api.manage_app import app\nfrom llama_agents.core.schema import LogEvent\nfrom llama_agents.core.schema.deployments import (\n    DeploymentEvent,\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n)\nfrom llama_agents.core.schema.git_validation import GitApplicationValidationResponse\nfrom llama_agents.core.server.manage_api import (\n    DeploymentNotFoundError,\n    ReplicaSetNotFoundError,\n)\nfrom packaging.version import Version\nfrom pydantic import HttpUrl\n\nclient = TestClient(app)\n\n\ndef test_public_version_endpoint_returns_extended_fields() -> None:\n    resp = client.get(\"/api/v1beta1/deployments-public/version\")\n    assert resp.status_code == 200\n    data = resp.json()\n    # Version is dynamic from package; just ensure it is present and non-empty\n    assert isinstance(data.get(\"version\"), str) and data[\"version\"]\n    assert data.get(\"requires_auth\") is False\n    assert Version(data.get(\"min_llamactl_version\")) <= Version(\n        pkg_version(\"llama-agents-control-plane\")\n    )\n\n\n@pytest.fixture\ndef mock_existing_deployment() -> Generator[MagicMock, None, None]:\n    \"\"\"Fixture that mocks k8s_client.get_deployment to return an existing deployment\"\"\"\n\n    existing_deployment = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=[\"DATABASE_URL\"],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    with patch(\"llama_agents.control_plane.k8s_client.get_deployment\") as mock_get:\n        mock_get.return_value = existing_deployment\n        yield mock_get\n\n\n@pytest.fixture(autouse=True)\ndef default_mock_git_ref() -> Generator[MagicMock, None, None]:\n    with mock_git_ref(\n        GitApplicationValidationResponse(\n            git_sha=\"12345678\",\n            git_ref=\"main\",\n            is_valid=True,\n            valid_deployment_file_path=\"llama_deploy.yaml\",\n        )\n    ) as mock:\n        yield mock\n\n\n@contextlib.contextmanager\ndef mock_git_ref(\n    value: GitApplicationValidationResponse,\n) -> Generator[MagicMock, None, None]:\n    \"\"\"Fixture that mocks git_service.validate_git_ref to return some git ref\"\"\"\n    with patch(\n        \"llama_agents.control_plane.manage_api.deployments_service.git_service.validate_git_application\"\n    ) as mock_validate:\n        mock_validate.return_value = value\n        yield mock_validate\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployments\")\ndef test_list_deployments(mock_get_deployments: MagicMock) -> None:\n    \"\"\"Test listing deployments for a project\"\"\"\n\n    mock_get_deployments.return_value = [\n        DeploymentResponse(\n            id=\"deploy1-123\",\n            display_name=\"deploy1\",\n            project_id=\"test-project\",\n            repo_url=\"https://github.com/user/repo.git\",\n            git_ref=\"abc123\",\n            deployment_file_path=\"deploy.yml\",\n            status=\"Running\",\n            has_personal_access_token=False,\n            secret_names=[\"DATABASE_URL\"],\n            apiserver_url=HttpUrl(\"http://deploy1.example.com\"),\n        )\n    ]\n\n    response = client.get(\n        \"/api/v1beta1/deployments\", params={\"project_id\": \"test-project\"}\n    )\n    assert response.status_code == 200\n\n    data = response.json()\n    assert len(data[\"deployments\"]) == 1\n    assert data[\"deployments\"][0][\"name\"] == \"deploy1\"\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\ndef test_get_deployment_success(mock_get_deployment: MagicMock) -> None:\n    \"\"\"Test getting a single deployment\"\"\"\n\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy-123\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=True,\n        secret_names=[\"API_KEY\", \"DATABASE_URL\"],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    response = client.get(\n        \"/api/v1beta1/deployments/test-deploy\", params={\"project_id\": \"test-project\"}\n    )\n    assert response.status_code == 200\n\n    data = response.json()\n    assert data[\"name\"] == \"test-deploy\"\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment_events\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\ndef test_get_deployment_with_events(\n    mock_get_deployment: MagicMock, mock_get_events: MagicMock\n) -> None:\n    \"\"\"Test getting a single deployment\"\"\"\n\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy-123\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=True,\n        secret_names=[\"API_KEY\", \"DATABASE_URL\"],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    mock_get_events.return_value = [\n        DeploymentEvent(\n            message=\"Deployment created\",\n            reason=\"Normal\",\n            type=\"Normal\",\n        )\n    ]\n\n    response = client.get(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\", \"include_events\": True},\n    )\n    assert response.status_code == 200\n\n    data = response.json()\n    assert data[\"name\"] == \"test-deploy\"\n    assert len(data[\"events\"]) == 1\n    assert data[\"events\"][0][\"message\"] == \"Deployment created\"\n    assert data[\"events\"][0][\"reason\"] == \"Normal\"\n    assert data[\"events\"][0][\"type\"] == \"Normal\"\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\ndef test_get_deployment_not_found(mock_get_deployment: MagicMock) -> None:\n    \"\"\"Test 404 when deployment doesn't exist\"\"\"\n    mock_get_deployment.return_value = None\n\n    response = client.get(\n        \"/api/v1beta1/deployments/nonexistent\", params={\"project_id\": \"test-project\"}\n    )\n    assert response.status_code == 404\n\n\n@patch(\"llama_agents.control_plane.k8s_client.create_deployment\")\ndef test_create_deployment_success(mock_create_deployment: MagicMock) -> None:\n    \"\"\"Test deployment creation\"\"\"\n\n    mock_create_deployment.return_value = DeploymentResponse(\n        id=\"new-deploy-123\",\n        display_name=\"new-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Pending\",\n        has_personal_access_token=True,\n        secret_names=[\"API_KEY\"],\n        apiserver_url=None,\n    )\n\n    request_data = {\n        \"name\": \"New Deploy\",\n        \"repo_url\": \"https://github.com/user/repo.git\",\n        \"personal_access_token\": \"ghp_token123\",\n        \"secrets\": {\"API_KEY\": \"secret_value\"},\n        \"appserver_version\": \"0.3.0\",\n    }\n\n    response = client.post(\n        \"/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n        json=request_data,\n    )\n    assert response.status_code == 201\n\n    data = response.json()\n    assert data[\"display_name\"] == \"new-deploy\"\n    assert data[\"has_personal_access_token\"] is True\n    assert data[\"secret_names\"] == [\"API_KEY\"]\n\n    # Verify the correct parameters were passed to create_deployment\n    mock_create_deployment.assert_called_once_with(\n        project_id=\"test-project\",\n        display_name=\"New Deploy\",\n        repo_url=\"https://github.com/user/repo.git\",\n        deployment_file_path=\"llama_deploy.yaml\",\n        git_ref=\"main\",\n        git_sha=\"12345678\",\n        pat=\"ghp_token123\",\n        secrets={\"API_KEY\": \"secret_value\"},\n        ui_build_output_path=None,\n        image_tag=\"0.3.0\",\n        explicit_id=None,\n    )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.code_repo_storage\",\n    new=None,\n)\n@patch(\"llama_agents.control_plane.k8s_client.create_deployment\")\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.git_service.validate_git_application\"\n)\ndef test_create_deployment_empty_repo_without_storage_fails_fast(\n    mock_validate_git_application: MagicMock,\n    mock_create_deployment: MagicMock,\n) -> None:\n    response = client.post(\n        \"/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n        json={\"name\": \"Push Mode Deploy\", \"repo_url\": \"\"},\n    )\n\n    assert response.status_code == 503\n    assert (\n        response.json()[\"detail\"]\n        == \"Code repo storage not configured (S3_BUCKET not set).\"\n    )\n    mock_validate_git_application.assert_not_called()\n    mock_create_deployment.assert_not_called()\n\n\n@patch(\"llama_agents.control_plane.k8s_client.create_deployment\")\ndef test_create_deployment_with_git_ref(mock_create_deployment: MagicMock) -> None:\n    \"\"\"Test deployment creation with git_ref parameter\"\"\"\n\n    mock_create_deployment.return_value = DeploymentResponse(\n        id=\"new-deploy-456\",\n        display_name=\"new-deploy-with-ref\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"feature-branch\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Pending\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    request_data = {\n        \"name\": \"New Deploy with Ref\",\n        \"repo_url\": \"https://github.com/user/repo.git\",\n        \"git_ref\": \"feature-branch\",\n        \"deployment_file_path\": \"deploy.yml\",\n    }\n\n    response = client.post(\n        \"/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n        json=request_data,\n    )\n    assert response.status_code == 201\n\n    data = response.json()\n    assert data[\"display_name\"] == \"new-deploy-with-ref\"\n    assert data[\"git_ref\"] == \"feature-branch\"\n    assert data[\"has_personal_access_token\"] is False\n\n    # Verify the correct parameters were passed to create_deployment\n    mock_create_deployment.assert_called_once_with(\n        project_id=\"test-project\",\n        display_name=\"New Deploy with Ref\",\n        repo_url=\"https://github.com/user/repo.git\",\n        deployment_file_path=\"deploy.yml\",\n        git_ref=\"feature-branch\",\n        git_sha=\"12345678\",\n        pat=None,\n        secrets=None,\n        ui_build_output_path=None,\n        image_tag=None,\n        explicit_id=None,\n    )\n\n\n@patch(\"llama_agents.control_plane.k8s_client.create_deployment\")\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.git_service.validate_git_application\"\n)\ndef test_create_deployment_git_ref_validation_error(\n    mock_validate_git_application: MagicMock,\n    mock_create_deployment: MagicMock,\n) -> None:\n    \"\"\"Test deployment creation with invalid git_ref returns 201 with warning header\"\"\"\n\n    # Create a deployment response to return with the error\n    deployment_response = DeploymentResponse(\n        id=\"new-deploy-123\",\n        display_name=\"New Deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"invalid-ref\",\n        deployment_file_path=\"llama_deploy.yaml\",\n        status=\"Pending\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    # Mock git validation error - return tuple with warning\n    mock_create_deployment.return_value = deployment_response\n    mock_validate_git_application.return_value = GitApplicationValidationResponse(\n        git_ref=\"invalid-ref\",\n        git_sha=None,\n        is_valid=False,\n        valid_deployment_file_path=None,\n        error_message=\"failed to resolve the git repository or reference\",\n    )\n\n    request_data = {\n        \"name\": \"New Deploy\",\n        \"repo_url\": \"https://github.com/user/repo.git\",\n        \"git_ref\": \"invalid-ref\",\n    }\n\n    response = client.post(\n        \"/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n        json=request_data,\n    )\n    assert response.status_code == 201\n\n    # Check that we get the deployment response\n    data = response.json()\n    assert data[\"display_name\"] == \"New Deploy\"\n    assert data[\"git_ref\"] == \"invalid-ref\"\n    # Check that warning is embedded in response\n    assert \"warning\" in data and data[\"warning\"]\n    assert \"failed to resolve the git repository or reference\" in data[\"warning\"]\n\n    # Verify the function was called with invalid git_ref\n    mock_create_deployment.assert_called_once_with(\n        project_id=\"test-project\",\n        display_name=\"New Deploy\",\n        repo_url=\"https://github.com/user/repo.git\",\n        deployment_file_path=None,\n        git_ref=\"invalid-ref\",\n        git_sha=None,\n        pat=None,\n        secrets=None,\n        ui_build_output_path=None,\n        image_tag=None,\n        explicit_id=None,\n    )\n\n\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\ndef test_update_deployment_success(\n    mock_update_deployment: MagicMock, mock_existing_deployment: MagicMock\n) -> None:\n    \"\"\"Test deployment update via PATCH\"\"\"\n\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy-123\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/updated-repo.git\",  # updated\n        git_ref=\"main\",  # updated\n        deployment_file_path=\"new_deploy.yml\",  # updated\n        status=\"Running\",\n        has_personal_access_token=True,\n        secret_names=[\"NEW_SECRET\"],  # updated\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n        appserver_version=\"0.3.1\",\n    )\n\n    update_data = {\n        \"repo_url\": \"https://github.com/user/updated-repo.git\",\n        \"deployment_file_path\": \"new_deploy.yml\",\n        \"personal_access_token\": \"ghp_newtoken\",\n        \"secrets\": {\n            \"NEW_SECRET\": \"new_value\",\n            \"OLD_SECRET\": None,  # remove this secret\n        },\n        \"appserver_version\": \"0.3.1\",\n    }\n\n    response = client.patch(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\"},\n        json=update_data,\n    )\n    assert response.status_code == 200\n\n    data = response.json()\n    assert data[\"repo_url\"] == \"https://github.com/user/updated-repo.git\"\n    assert data[\"deployment_file_path\"] == \"new_deploy.yml\"\n    assert data[\"git_ref\"] == \"main\"\n    assert data[\"has_personal_access_token\"] is True\n    assert data[\"secret_names\"] == [\"NEW_SECRET\"]\n    assert data[\"appserver_version\"] == \"0.3.1\"\n    # Old clients reading the response see the deprecated key populated\n    assert data[\"llama_deploy_version\"] == \"0.3.1\"\n\n    # Verify the update function was called with correct parameters\n    mock_update_deployment.assert_called_once()\n    call_args = mock_update_deployment.call_args\n    assert call_args[1][\"deployment_id\"] == \"test-deploy\"\n    update_arg = call_args[1][\"update\"]\n    assert update_arg.repo_url == \"https://github.com/user/updated-repo.git\"\n    assert update_arg.deployment_file_path == \"new_deploy.yml\"\n    assert update_arg.personal_access_token == \"ghp_newtoken\"\n    assert update_arg.secrets == {\"NEW_SECRET\": \"new_value\", \"OLD_SECRET\": None}\n    assert update_arg.appserver_version == \"0.3.1\"\n\n\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.git_service.validate_git_application\"\n)\ndef test_update_deployment_suspend_skips_git_validation(\n    mock_validate: MagicMock,\n    mock_get_deployment: MagicMock,\n    mock_update_deployment: MagicMock,\n) -> None:\n    \"\"\"Test that a suspend-only PATCH does not call git validation.\"\"\"\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=[\"DATABASE_URL\"],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=[\"DATABASE_URL\"],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n        suspended=True,\n    )\n\n    response = client.patch(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\"},\n        json={\"suspended\": True},\n    )\n    assert response.status_code == 200\n    mock_validate.assert_not_called()\n    mock_update_deployment.assert_called_once()\n\n\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.git_service.validate_git_application\"\n)\ndef test_update_deployment_internal_repo_skips_git_validation(\n    mock_validate: MagicMock,\n    mock_get_deployment: MagicMock,\n    mock_update_deployment: MagicMock,\n) -> None:\n    \"\"\"Test that updating a deployment with internal:// repo skips git validation.\"\"\"\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"internal://\",\n        git_ref=\"abc123\",\n        git_sha=\"deadbeef\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=[],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"internal://\",\n        git_ref=\"abc123\",\n        git_sha=\"deadbeef\",\n        deployment_file_path=\"new_path/deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=[],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    response = client.patch(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\"},\n        json={\"deployment_file_path\": \"new_path/deploy.yml\"},\n    )\n    assert response.status_code == 200\n    mock_validate.assert_not_called()\n    mock_update_deployment.assert_called_once()\n\n\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.git_service.validate_git_application\"\n)\ndef test_update_deployment_empty_repo_url_preserves_internal(\n    mock_validate: MagicMock,\n    mock_get_deployment: MagicMock,\n    mock_update_deployment: MagicMock,\n) -> None:\n    \"\"\"Empty repo_url in update should not overwrite an existing internal:// value.\"\"\"\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"internal://\",\n        git_ref=\"main\",\n        git_sha=\"deadbeef\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=[],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"internal://\",\n        git_ref=\"main\",\n        git_sha=\"deadbeef\",\n        deployment_file_path=\"new_path/deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=[],\n        apiserver_url=HttpUrl(\"http://test-deploy.example.com\"),\n    )\n\n    response = client.patch(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\"},\n        json={\"repo_url\": \"\", \"deployment_file_path\": \"new_path/deploy.yml\"},\n    )\n    assert response.status_code == 200\n    mock_validate.assert_not_called()\n    # Verify repo_url was dropped (set to None) so it doesn't overwrite internal://\n    update_arg = mock_update_deployment.call_args[1][\"update\"]\n    assert update_arg.repo_url is None\n\n\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\ndef test_update_deployment_partial(\n    mock_update_deployment: MagicMock, mock_existing_deployment: MagicMock\n) -> None:\n    \"\"\"Test partial deployment update (only some fields)\"\"\"\n    from llama_agents.core.schema.deployments import DeploymentResponse\n\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/new-repo.git\",  # only this changed\n        git_ref=\"abc123\",  # unchanged\n        deployment_file_path=\"deploy.yml\",  # unchanged\n        status=\"Running\",\n        has_personal_access_token=False,  # unchanged\n        secret_names=None,  # unchanged\n        apiserver_url=None,\n    )\n\n    # Only update repo_url\n    update_data = {\"repo_url\": \"https://github.com/user/new-repo.git\"}\n\n    response = client.patch(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\"},\n        json=update_data,\n    )\n    assert response.status_code == 200\n\n    data = response.json()\n    assert data[\"repo_url\"] == \"https://github.com/user/new-repo.git\"\n\n    # Verify partial update was called\n    mock_update_deployment.assert_called_once()\n    call_args = mock_update_deployment.call_args\n    update_arg = call_args[1][\"update\"]\n    assert update_arg.repo_url == \"https://github.com/user/new-repo.git\"\n    # Other fields should be None (not updated)\n    assert update_arg.deployment_file_path is None\n    assert update_arg.personal_access_token is None\n    assert update_arg.secrets is None\n\n\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\ndef test_update_deployment_secrets_only(\n    mock_update_deployment: MagicMock, mock_existing_deployment: MagicMock\n) -> None:\n    \"\"\"Test updating only secrets\"\"\"\n\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",  # unchanged\n        git_ref=\"abc123\",  # unchanged\n        deployment_file_path=\"deploy.yml\",  # unchanged\n        status=\"Running\",\n        has_personal_access_token=False,  # unchanged\n        secret_names=[\"API_KEY\"],  # updated\n        apiserver_url=None,\n    )\n\n    # Only update secrets\n    update_data = {\n        \"secrets\": {\n            \"API_KEY\": \"new_api_key_value\",\n            \"DATABASE_URL\": None,  # remove this\n        }\n    }\n\n    response = client.patch(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\"},\n        json=update_data,\n    )\n    assert response.status_code == 200\n\n    # Verify secrets-only update\n    mock_update_deployment.assert_called_once()\n    call_args = mock_update_deployment.call_args\n    update_arg = call_args[1][\"update\"]\n    assert update_arg.secrets == {\"API_KEY\": \"new_api_key_value\", \"DATABASE_URL\": None}\n    # Other fields should be None\n    assert update_arg.repo_url is None\n    assert update_arg.deployment_file_path is None\n    assert update_arg.personal_access_token is None\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_projects_with_deployment_count\")\ndef test_list_projects(mock_get_projects: MagicMock) -> None:\n    \"\"\"Test listing projects with deployment counts\"\"\"\n    mock_get_projects.return_value = [\n        {\"project_id\": \"project1\", \"project_name\": \"project1\", \"deployment_count\": 2},\n        {\"project_id\": \"project2\", \"project_name\": \"project2\", \"deployment_count\": 1},\n    ]\n\n    response = client.get(\"/api/v1beta1/deployments/list-projects\")\n    assert response.status_code == 200\n\n    data = response.json()\n    assert len(data[\"projects\"]) == 2\n    assert data[\"projects\"][0][\"project_id\"] == \"project1\"\n    assert data[\"projects\"][0][\"deployment_count\"] == 2\n\n\ndef test_create_deployment_validation_error() -> None:\n    \"\"\"Test validation error on missing required fields\"\"\"\n    # Missing required field: name\n    request_data = {\"repo_url\": \"https://github.com/example/repo\"}\n\n    response = client.post(\n        \"/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n        json=request_data,\n    )\n    assert response.status_code == 422  # Validation error\n\n\ndef test_create_deployment_rejects_invalid_appserver_version() -> None:\n    response = client.post(\n        \"/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n        json={\n            \"name\": \"New Deploy\",\n            \"repo_url\": \"\",\n            \"appserver_version\": \"tilt-dev\",\n        },\n    )\n\n    assert response.status_code == 422\n    detail = response.json()[\"detail\"]\n    assert detail[0][\"loc\"] == [\"body\", \"appserver_version\"]\n    assert \"invalid appserver_version\" in detail[0][\"msg\"]\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.deployments_service.stream_deployment_logs\"\n)\ndef test_stream_deployment_logs_success(mock_stream: MagicMock) -> None:\n    \"\"\"Stream logs endpoint returns SSE frames with LogEvent payloads and headers.\"\"\"\n\n    events = [\n        LogEvent(pod=\"pod-1\", container=\"c\", text=\"hello\", timestamp=datetime.now()),\n        LogEvent(pod=\"pod-1\", container=\"c\", text=\"world\", timestamp=datetime.now()),\n    ]\n\n    async def agen() -> AsyncGenerator[LogEvent, None]:\n        for e in events:\n            yield e\n\n    mock_stream.return_value = agen()\n\n    resp = client.get(\n        \"/api/v1beta1/deployments/deploy-1/logs\",\n        params={\n            \"project_id\": \"proj-1\",\n            \"include_init_containers\": True,\n            \"since_seconds\": 10,\n            \"tail_lines\": 5,\n        },\n    )\n\n    assert resp.status_code == 200\n    # content-type may include charset; just check prefix\n    assert resp.headers[\"content-type\"].startswith(\"text/event-stream\")\n    assert resp.headers.get(\"X-Accel-Buffering\") == \"no\"\n    body = resp.text\n    assert \"event: log\\n\" in body\n    # Verify payloads appear as JSON lines\n    assert \"\\n\\n\" in body\n    assert '\"pod\":\"pod-1\"' in body and '\"text\":\"hello\"' in body\n    mock_stream.assert_called_once_with(\n        project_id=\"proj-1\",\n        deployment_id=\"deploy-1\",\n        include_init_containers=True,\n        since_seconds=10,\n        tail_lines=5,\n        follow=True,\n    )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.deployments_service.stream_deployment_logs\"\n)\ndef test_stream_deployment_logs_follow_false_threads_through(\n    mock_stream: MagicMock,\n) -> None:\n    \"\"\"``?follow=false`` should be threaded into the service call.\"\"\"\n\n    async def empty_gen() -> AsyncGenerator[LogEvent, None]:\n        if False:\n            yield LogEvent(pod=\"x\", container=\"c\", text=\"\", timestamp=datetime.now())\n        return\n\n    mock_stream.return_value = empty_gen()\n\n    resp = client.get(\n        \"/api/v1beta1/deployments/deploy-1/logs\",\n        params={\"project_id\": \"proj-1\", \"follow\": \"false\"},\n    )\n    assert resp.status_code == 200\n    mock_stream.assert_called_once_with(\n        project_id=\"proj-1\",\n        deployment_id=\"deploy-1\",\n        include_init_containers=False,\n        since_seconds=None,\n        tail_lines=None,\n        follow=False,\n    )\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.deployments_service.stream_deployment_logs\",\n    side_effect=Exception(\"unexpected\"),\n)\ndef test_stream_deployment_logs_unexpected_error(mock_stream: MagicMock) -> None:\n    \"\"\"Unexpected errors from service should propagate as 500 from FastAPI.\"\"\"\n\n    local_client = TestClient(app, raise_server_exceptions=False)\n    resp = local_client.get(\n        \"/api/v1beta1/deployments/deploy-err/logs\",\n        params={\"project_id\": \"proj-1\"},\n    )\n    # FastAPI default for uncaught exceptions is 500\n    assert resp.status_code == 500\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.deployments_service.stream_deployment_logs\",\n    side_effect=DeploymentNotFoundError(\"not found\"),\n)\ndef test_stream_deployment_logs_not_found(mock_stream: MagicMock) -> None:\n    resp = client.get(\n        \"/api/v1beta1/deployments/deploy-missing/logs\",\n        params={\"project_id\": \"proj-1\"},\n    )\n    assert resp.status_code == 404\n    data = resp.json()\n    assert data[\"detail\"] == \"not found\"\n\n\n@patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.deployments_service.stream_deployment_logs\",\n    side_effect=ReplicaSetNotFoundError(\"no rs\"),\n)\ndef test_stream_deployment_logs_no_replicaset(mock_stream: MagicMock) -> None:\n    resp = client.get(\n        \"/api/v1beta1/deployments/deploy-nors/logs\",\n        params={\"project_id\": \"proj-1\"},\n    )\n    assert resp.status_code == 409\n    data = resp.json()\n    assert data[\"detail\"] == \"no rs\"\n\n\n@patch(\"llama_agents.control_plane.manage_api.deployments_service._handle_git_request\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\ndef test_git_endpoint_rejects_external_repo_deployments(\n    mock_get_deployment: MagicMock,\n    mock_handle_git_request: MagicMock,\n) -> None:\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"deploy-1\",\n        display_name=\"deploy-1\",\n        project_id=\"proj-1\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    resp = client.get(\n        \"/api/v1beta1/deployments/deploy-1/git/info/refs\",\n        params={\"project_id\": \"proj-1\", \"service\": \"git-receive-pack\"},\n    )\n\n    assert resp.status_code == 409\n    assert \"external repository\" in resp.json()[\"detail\"]\n    mock_handle_git_request.assert_not_called()\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment_history\")\ndef test_get_deployment_history(\n    mock_get_history: MagicMock, mock_get_deployment: MagicMock\n) -> None:\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"deploy-1\",\n        display_name=\"deploy-1\",\n        project_id=\"proj-1\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n    mock_get_history.return_value = DeploymentHistoryResponse(\n        deployment_id=\"deploy-1\",\n        history=[],\n    )\n\n    resp = client.get(\n        \"/api/v1beta1/deployments/deploy-1/history\",\n        params={\"project_id\": \"proj-1\"},\n    )\n    assert resp.status_code == 200\n    data = resp.json()\n    assert data[\"deployment_id\"] == \"deploy-1\"\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment_history\")\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\ndef test_rollback(\n    mock_update_deployment: MagicMock,\n    mock_get_history: MagicMock,\n    mock_get_deployment: MagicMock,\n) -> None:\n    from llama_agents.core.schema.deployments import ReleaseHistoryItem\n\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"deploy-1\",\n        display_name=\"deploy-1\",\n        project_id=\"proj-1\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n    mock_get_history.return_value = DeploymentHistoryResponse(\n        deployment_id=\"deploy-1\",\n        history=[\n            ReleaseHistoryItem(\n                git_sha=\"deadbeef\",\n                image_tag=\"appserver-0.4.1\",\n                released_at=datetime(2025, 1, 1),\n            ),\n        ],\n    )\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"deploy-1\",\n        display_name=\"deploy-1\",\n        project_id=\"proj-1\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=None,\n        git_sha=\"deadbeef\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    resp = client.post(\n        \"/api/v1beta1/deployments/deploy-1/rollback\",\n        params={\"project_id\": \"proj-1\"},\n        json={\"git_sha\": \"deadbeef\"},\n    )\n    assert resp.status_code == 200\n    data = resp.json()\n    assert data[\"git_sha\"] == \"deadbeef\"\n\n    # Verify that the update included the imageTag from history directly\n    update_arg = mock_update_deployment.call_args\n    update_data = update_arg.kwargs.get(\"update\") or update_arg[1].get(\"update\")\n    assert update_data.image_tag == \"appserver-0.4.1\"\n\n\nOPERATOR_DEFAULT_PATCH = patch(\n    \"llama_agents.control_plane.manage_api.deployments_service.settings\"\n    \".default_appserver_image_tag\",\n    \"operator-default\",\n)\n\n\n@OPERATOR_DEFAULT_PATCH\n@patch(\"llama_agents.control_plane.k8s_client.create_deployment\")\ndef test_create_deployment_operator_default_ignores_client_version(\n    mock_create_deployment: MagicMock,\n) -> None:\n    \"\"\"When DEFAULT_APPSERVER_IMAGE_TAG=operator-default, client-sent version is ignored.\"\"\"\n    mock_create_deployment.return_value = DeploymentResponse(\n        id=\"new-deploy-123\",\n        display_name=\"new-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Pending\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    response = client.post(\n        \"/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n        json={\n            \"name\": \"New Deploy\",\n            \"repo_url\": \"https://github.com/user/repo.git\",\n            \"llama_deploy_version\": \"0.3.0\",\n        },\n    )\n    assert response.status_code == 201\n\n    mock_create_deployment.assert_called_once()\n    call_kwargs = mock_create_deployment.call_args[1]\n    assert call_kwargs[\"image_tag\"] is None\n\n\n@OPERATOR_DEFAULT_PATCH\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\ndef test_update_deployment_operator_default_ignores_client_version(\n    mock_update_deployment: MagicMock, mock_existing_deployment: MagicMock\n) -> None:\n    \"\"\"When DEFAULT_APPSERVER_IMAGE_TAG=operator-default, update strips version/tag.\"\"\"\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy-123\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    response = client.patch(\n        \"/api/v1beta1/deployments/test-deploy\",\n        params={\"project_id\": \"test-project\"},\n        json={\n            \"repo_url\": \"https://github.com/user/repo.git\",\n            \"llama_deploy_version\": \"0.3.1\",\n        },\n    )\n    assert response.status_code == 200\n\n    update_arg = mock_update_deployment.call_args[1][\"update\"]\n    assert update_arg.appserver_version is None\n    assert update_arg.image_tag is None\n\n\n@OPERATOR_DEFAULT_PATCH\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment_history\")\n@patch(\"llama_agents.control_plane.k8s_client.update_deployment\")\ndef test_rollback_operator_default_ignores_history_tag(\n    mock_update_deployment: MagicMock,\n    mock_get_history: MagicMock,\n    mock_get_deployment: MagicMock,\n) -> None:\n    \"\"\"When DEFAULT_APPSERVER_IMAGE_TAG=operator-default, rollback doesn't restore imageTag.\"\"\"\n    from llama_agents.core.schema.deployments import ReleaseHistoryItem\n\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"deploy-1\",\n        display_name=\"deploy-1\",\n        project_id=\"proj-1\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n    mock_get_history.return_value = DeploymentHistoryResponse(\n        deployment_id=\"deploy-1\",\n        history=[\n            ReleaseHistoryItem(\n                git_sha=\"deadbeef\",\n                image_tag=\"appserver-0.4.1\",\n                released_at=datetime(2025, 1, 1),\n            ),\n        ],\n    )\n    mock_update_deployment.return_value = DeploymentResponse(\n        id=\"deploy-1\",\n        display_name=\"deploy-1\",\n        project_id=\"proj-1\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=None,\n        git_sha=\"deadbeef\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    resp = client.post(\n        \"/api/v1beta1/deployments/deploy-1/rollback\",\n        params={\"project_id\": \"proj-1\"},\n        json={\"git_sha\": \"deadbeef\"},\n    )\n    assert resp.status_code == 200\n\n    update_data = mock_update_deployment.call_args[1][\"update\"]\n    assert update_data.image_tag is None\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__])\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_find_deployment_id.py",
    "content": "\"\"\"Unit tests to validate find_deployment_id behavior and expose potential bugs\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\nfrom kubernetes.client.exceptions import ApiException\nfrom llama_agents.control_plane import k8s_client\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_no_suffix_when_available() -> None:\n    \"\"\"Test that no suffix is added when base deployment ID is available\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True  # Base ID is available\n\n        result = await k8s_client.find_deployment_id(\"my-service\")\n\n        # Should return the cleaned name without any suffix\n        assert result == \"my-service\"\n        # Should only call validate once with the base ID\n        mock_validate.assert_called_once_with(\"my-service\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_suffix_when_base_taken() -> None:\n    \"\"\"Test that suffix is added when base deployment ID is taken\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        # First call (base ID) returns False (taken), second call returns True (available)\n        mock_validate.side_effect = [False, True]\n\n        result = await k8s_client.find_deployment_id(\"my-service\")\n\n        # Should return the base name with a suffix\n        assert result.startswith(\"my-service-\")\n        assert len(result) == len(\"my-service-\") + 5  # 5 char hex suffix\n        # Should call validate twice - once for base, once for suffixed version\n        assert mock_validate.call_count == 2\n\n        # First call should be with base ID\n        assert mock_validate.call_args_list[0][0][0] == \"my-service\"\n        # Second call should be with suffixed version\n        assert mock_validate.call_args_list[1][0][0].startswith(\"my-service-\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_short_name_gets_suffix() -> None:\n    \"\"\"Test that names shorter than 3 characters always get a suffix\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True  # Available\n\n        result = await k8s_client.find_deployment_id(\"ab\")\n\n        # Should have a suffix added since name is < 3 chars\n        assert len(result) == 8  # \"ab-\" + 5 char hex suffix\n        assert result.startswith(\"ab-\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_three_char_name_no_suffix() -> None:\n    \"\"\"Test that names with 3 characters don't get a suffix\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True  # Available\n\n        result = await k8s_client.find_deployment_id(\"abc\")\n\n        # Should NOT have a suffix added since name is >= 3 chars\n        assert result == \"abc\"\n        mock_validate.assert_called_once_with(\"abc\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_name_cleaning() -> None:\n    \"\"\"Test that special characters are properly cleaned\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True  # Available\n\n        result = await k8s_client.find_deployment_id(\"My Service!@#$%\")\n\n        # Should clean the name and return without suffix if available\n        assert result == \"my-service\"\n        mock_validate.assert_called_once_with(\"my-service\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_real_validation() -> None:\n    \"\"\"Test find_deployment_id with real validation to check for bugs\"\"\"\n    # Use real validate_deployment_id but mock the K8s client\n    with patch(\"llama_agents.control_plane.k8s_client._k8s_client\") as mock_k8s_client:\n        # Mock a 404 response (deployment doesn't exist, so ID is available)\n\n        mock_k8s_client.k8s_custom_objects.get_namespaced_custom_object.side_effect = (\n            ApiException(status=404)\n        )\n        mock_k8s_client.namespace = \"test-namespace\"\n\n        result = await k8s_client.find_deployment_id(\"my-service\")\n\n        # If validation works correctly, should return base name without suffix\n        assert result == \"my-service\"\n\n        # Should have called get_namespaced_custom_object once with base name\n        mock_k8s_client.k8s_custom_objects.get_namespaced_custom_object.assert_called_once()\n        call_args = (\n            mock_k8s_client.k8s_custom_objects.get_namespaced_custom_object.call_args\n        )\n        assert call_args[1][\"name\"] == \"my-service\"\n\n\n@pytest.mark.asyncio\nasync def test_create_deployment_id_behavior() -> None:\n    \"\"\"Test create_deployment to verify deployment ID behavior end-to-end\"\"\"\n\n    # Mock all the dependencies\n    with (\n        patch(\"llama_agents.core.git.git_util.clone_repo\") as mock_clone,\n        patch(\n            \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n        ) as mock_validate,\n        patch(\"llama_agents.control_plane.k8s_client._k8s_client\") as mock_k8s_client,\n        patch(\"tempfile.mkdtemp\") as mock_temp,\n    ):\n        # Setup mocks\n        mock_clone.return_value = \"abc123\"\n        mock_validate.return_value = True  # Base deployment ID is available\n        mock_temp.return_value = \"/tmp/test\"\n\n        # Mock K8s client methods\n        mock_k8s_client.namespace = \"test-namespace\"\n        mock_k8s_client.enable_ingress = False\n        mock_k8s_client.k8s_custom_objects.create_namespaced_custom_object.return_value = {}\n\n        # Call create_deployment\n        result = await k8s_client.create_deployment(\n            project_id=\"test-project\",\n            display_name=\"Test Service\",\n            repo_url=\"https://github.com/test/repo.git\",\n        )\n\n        # The deployment ID should be the cleaned name without suffix\n        assert result.id == \"test-service\"\n\n        # Verify validate_deployment_id was called with the clean base name\n        mock_validate.assert_called_once_with(\"test-service\")\n\n        # Verify the K8s object was created with the correct name\n        create_call = mock_k8s_client.k8s_custom_objects.create_namespaced_custom_object\n        create_call.assert_called_once()\n        created_object = create_call.call_args[1][\"body\"]\n        assert created_object[\"metadata\"][\"name\"] == \"test-service\"\n\n\n@pytest.mark.asyncio\nasync def test_create_deployment_with_collision() -> None:\n    \"\"\"Test create_deployment when there's a name collision\"\"\"\n\n    with (\n        patch(\"llama_agents.core.git.git_util.clone_repo\") as mock_clone,\n        patch(\n            \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n        ) as mock_validate,\n        patch(\"llama_agents.control_plane.k8s_client._k8s_client\") as mock_k8s_client,\n        patch(\"tempfile.mkdtemp\") as mock_temp,\n    ):\n        # Setup mocks\n        mock_clone.return_value = \"abc123\"\n        # First call (base ID) returns False, second call returns True\n        mock_validate.side_effect = [False, True]\n        mock_temp.return_value = \"/tmp/test\"\n\n        # Mock K8s client methods\n        mock_k8s_client.namespace = \"test-namespace\"\n        mock_k8s_client.enable_ingress = False\n        mock_k8s_client.k8s_custom_objects.create_namespaced_custom_object.return_value = {}\n\n        # Call create_deployment\n        result = await k8s_client.create_deployment(\n            project_id=\"test-project\",\n            display_name=\"Test Service\",\n            repo_url=\"https://github.com/test/repo.git\",\n        )\n\n        # The deployment ID should have a suffix since base was taken\n        assert result.id.startswith(\"test-service-\")\n        assert len(result.id) == len(\"test-service-\") + 5  # 5 char hex suffix\n\n        # Verify validate_deployment_id was called twice\n        assert mock_validate.call_count == 2\n        # First call with base name\n        assert mock_validate.call_args_list[0][0][0] == \"test-service\"\n        # Second call with suffixed name\n        assert mock_validate.call_args_list[1][0][0].startswith(\"test-service-\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_numeric_name_gets_prefix() -> None:\n    \"\"\"Test that all-numeric names get a 'd-' prefix for DNS-1035 compliance\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True\n\n        result = await k8s_client.find_deployment_id(\"10101010\")\n\n        assert result == \"d-10101010\"\n        mock_validate.assert_called_once_with(\"d-10101010\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_digit_start_gets_prefix() -> None:\n    \"\"\"Test that names starting with a digit get a 'd-' prefix\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True\n\n        result = await k8s_client.find_deployment_id(\"123-service\")\n\n        assert result == \"d-123-service\"\n        mock_validate.assert_called_once_with(\"d-123-service\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_alpha_start_no_prefix() -> None:\n    \"\"\"Test that names already starting with a letter don't get a prefix\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True\n\n        result = await k8s_client.find_deployment_id(\"service-123\")\n\n        assert result == \"service-123\"\n        mock_validate.assert_called_once_with(\"service-123\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_empty_name_suffix_starts_with_letter() -> None:\n    \"\"\"Test that empty names (all special chars) produce DNS-compliant suffixes\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True\n\n        result = await k8s_client.find_deployment_id(\"!!!\")\n\n        # Name becomes empty after sanitization, so gets a random suffix\n        # The suffix must start with a letter for DNS-1035 compliance\n        assert len(result) == 5\n        assert result[0].isalpha()\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_max_length_63() -> None:\n    \"\"\"Test that deployment IDs are truncated to 63 chars (DNS-1035 max)\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = True\n\n        long_name = \"a\" * 100\n        result = await k8s_client.find_deployment_id(long_name)\n\n        assert len(result) <= 63\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_exhaustion() -> None:\n    \"\"\"Test behavior when all deployment IDs are taken\"\"\"\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate:\n        mock_validate.return_value = False  # All IDs are taken\n\n        with pytest.raises(ValueError) as exc_info:\n            await k8s_client.find_deployment_id(\"test\")\n\n        assert \"already in use\" in str(exc_info.value)\n        # Should call validate 99 times (range(1, 100))\n        assert mock_validate.call_count == 99\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_git_service.py",
    "content": "from __future__ import annotations\n\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nimport base64\nfrom pathlib import Path\nfrom typing import Generator, cast\nfrom unittest.mock import patch\n\nimport llama_agents.control_plane.git._git_service as git_service_module\nimport pytest\nimport respx\nfrom llama_agents.control_plane.git import GitService, git_service\nfrom llama_agents.control_plane.git._github_auth import GitHubAppAuth\nfrom llama_agents.control_plane.git.github_api_client import GitHubApiClient\nfrom llama_agents.core.git.git_util import GitAccessError\n\nGIT_SERVICE = \"llama_agents.control_plane.git._git_service\"\n\n\n@pytest.fixture(autouse=True)\ndef mock_github_api() -> Generator[respx.Router, None, None]:\n    \"\"\"Just a safeguard to mock GitHub API calls to prevent real HTTP requests during tests.\"\"\"\n    with respx.mock as mock_router:\n        # Mock get_owner_info API calls\n        yield mock_router\n\n\n@pytest.fixture(autouse=True)\ndef default_public_probe_false() -> Generator[None, None, None]:\n    \"\"\"Default to not-public for git probe unless a test overrides it.\"\"\"\n    with patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=False):\n        yield\n\n\n@pytest.fixture\ndef service() -> GitService:\n    return git_service\n\n\n@pytest.fixture\ndef project_id() -> str:\n    return \"test-project\"\n\n\n@pytest.fixture\ndef github_app_auth() -> Generator[GitHubAppAuth, None, None]:\n    # fake key for testing. Somewhat mangled to avoid the pre-commit hook.\n    private_key = (\n        \"-----BEGIN RSA PRIV\"\n        \"ATE KEY-----\\n\"\n        \"MIIEowIBAAKCAQEAlbOWExC3QtsEKSArcrogbO8L39M5Mn0fLtEmtzg5RSQ4FtIY\\n\"\n        \"ehHc3TAxcCi6haUqsdht2P7rT2Q0aDPhQgG9m+8BpS9LOFOh4cfd99q0lQSlMVCC\\n\"\n        \"zV25NuCljdKzT52n0byzVNIUMW9+knrswPNQy/LpEyxLvBzFyskdsoIyOKwT4Nl0\\n\"\n        \"L5T0S1pw2fxkhoGBsmIzfkU53k4yIAJ5heVBRJSwa/+RkfXY+7i8/PIixue5tMnJ\\n\"\n        \"kA6gcfG9xW7bnCL169wgN3NmM14/wr+3Aysnia0cyBUF/KO1v3bWp/lrjPOpL3uo\\n\"\n        \"hcDWdpZot5qt77GkoWZZNimRA6TjBaWCG0O1LwIDAQABAoIBADmcAUp3+dZ4G3tK\\n\"\n        \"Hn5Jo3XYbmzlx9KmtQvawDftIpj5jb42fuXnHuReCgB8I/+PZsVHIUrLGzoTuVlK\\n\"\n        \"ccrpiZLLIQp1D1DvWlJdjI239BuOzJWUQqoOgdrdC8jux0OBy9XadPbU26GEoyRy\\n\"\n        \"us6sNDEwW0KeHs0XE4Ts7YlHMlV9SCyx2kqoicb7vHiDJLhytAJAXNgSbQgWI05a\\n\"\n        \"c+gSknN1b4DBumb9x6MST9JBH4FobGtzCkswSNNsyrPZsax3env7bRDYYQosjRaA\\n\"\n        \"7Xc243bdkDyD9oAOKffLdCYxuloLxkPlVKXUrTTVGcHKxOoK5oRpuKRBKMZJVrKu\\n\"\n        \"aCbS0KkCgYEA0UdDNizillqLKbv+lAczzxOYhcXqaXsX2WhracDyYa8ycppoe6YU\\n\"\n        \"5MPlOL1f/R8HCncuwoXcfGdQqsTnrjsaMZDCFR37pCZeSvy7GxPMdsDB6x7bEk/L\\n\"\n        \"qQWKfPtAujhdfzdfJQ88cZjIhb1Pg2+lFBnpyLVK8fUxtBHR2LOhcPkCgYEAtx9e\\n\"\n        \"rb3jb1vQ2gHX0aDNA/tZ7Q3/Ika/pzDHbliaULm38Ts/yjojE/Mwc0lFLbHTa2vq\\n\"\n        \"4kXCDyJuTN5TurZsbg/gowoGQGNao0emR/Vvr4s9NXJFCoTEqX2W8d1DI70azqIt\\n\"\n        \"L2ukiPyeMuS6ijx9Sj/PyaK1TwAOL7VdY98qiWcCgYEAtw7svcC5WudMf28QGo/K\\n\"\n        \"Q8JSUgFzMF0Z2XQ7MMAzxDqpmBF0f2QhNpIcOWt9QT4YvJDP+Bt7Z94/c4DVX1QX\\n\"\n        \"b2++NRaK/WUKafF0ARVqbh3iAjZ1Tik6bliIcRad4cZYEmVu9k3Dg2IvVLzphoDs\\n\"\n        \"Fw8rrgLW0Zq2pVpJApLuDpECgYA1dJ/TwfmxWTEXYrBYjkMqpWXz0EEpBVQO/ytI\\n\"\n        \"Z+7sH7q1XaFabCwvN69uB/Z8x0s7MW6IjOqANoHSSJhSicwPOO1PSq7WfupHfbPp\\n\"\n        \"j5kBunissGW9E1LBU1sL0ZY2yY4YwbjE/fwyzON1YdWeYtgEI6qJZsjcfdymSqAv\\n\"\n        \"dkbZgwKBgCeH06jqN8d7+UrO/T8LM60RTRwJoUz5p8MY/qY863//5c79nB+zwGim\\n\"\n        \"L4wvm+sCW/GDzIEH+SjVLZXK1SALuQuzoUhfvINbgWqjsBDBzRptA4hSxM0UIHkd\\n\"\n        \"hn+lZmeXrFUJJa7VrO+6BlZWm20567cNZbjUKjKeuyI20t/9hVGP\\n\"\n        \"-----END RSA PRIV\"\n        \"ATE KEY-----\"\n    )\n\n    auth = GitHubAppAuth(\n        client_id=\"12345\",\n        private_key=private_key,\n        app_name=\"TestApp\",\n    )\n\n    with (\n        patch.object(git_service_module, \"get_github_app_auth\", return_value=auth),\n        patch(\n            \"llama_agents.control_plane.git.github_api_client.get_github_app_auth\",\n            return_value=auth,\n        ),\n    ):\n        yield auth\n\n\n@pytest.fixture(autouse=True)\ndef no_github_app_auth() -> Generator[GitHubAppAuth | None, None, None]:\n    with (\n        patch.object(git_service_module, \"get_github_app_auth\", return_value=None),\n        patch(\n            \"llama_agents.control_plane.git.github_api_client.get_github_app_auth\",\n            return_value=None,\n        ),\n    ):\n        yield None\n\n\ndef mock_github_repo_and_owner(\n    router: respx.Router,\n    owner: str = \"owner\",\n    repo: str = \"repo\",\n    installation_id: int | None = None,\n    is_org: bool = False,\n    owner_exists: bool = True,\n    repo_private: bool = True,\n    repository_selection: str | None = None,\n    repo_accessible: bool | None = None,\n) -> None:\n    \"\"\"\n    Mocks github apis given a scenario of a repo and owner. Mocks full https requests. If this gets\n    to be too much, we should add separate tests for the api client, and mock the api client functions\n    \"\"\"\n    owner_type = \"Organization\" if is_org else \"User\"\n    if owner_exists:\n        router.get(f\"https://api.github.com/users/{owner}\").mock(\n            return_value=respx.MockResponse(\n                200, json={\"id\": 12345, \"login\": owner, \"type\": owner_type}\n            )\n        )\n        if is_org:\n            router.get(f\"https://api.github.com/orgs/{owner}\").mock(\n                return_value=respx.MockResponse(\n                    200, json={\"id\": 12345, \"login\": owner, \"type\": \"Organization\"}\n                )\n            )\n    else:\n        router.get(f\"https://api.github.com/users/{owner}\").mock(\n            return_value=respx.MockResponse(404)\n        )\n        if is_org:\n            router.get(f\"https://api.github.com/orgs/{owner}\").mock(\n                return_value=respx.MockResponse(404)\n            )\n\n    if repo_accessible is None:\n        repo_accessible = not repo_private\n\n    if repo_accessible:\n        router.get(f\"https://api.github.com/repos/{owner}/{repo}\").mock(\n            return_value=respx.MockResponse(\n                200, json={\"private\": repo_private, \"name\": repo}\n            )\n        )\n    else:\n        router.get(f\"https://api.github.com/repos/{owner}/{repo}\").mock(\n            return_value=respx.MockResponse(404)\n        )\n    if installation_id and is_org:\n        org_installation_payload: dict[str, int | str] = {\"id\": installation_id}\n        if repository_selection is not None:\n            org_installation_payload[\"repository_selection\"] = repository_selection\n        router.get(f\"https://api.github.com/orgs/{owner}/installation\").mock(\n            return_value=respx.MockResponse(200, json=org_installation_payload)\n        )\n    else:\n        router.get(f\"https://api.github.com/orgs/{owner}/installation\").mock(\n            return_value=respx.MockResponse(404)\n        )\n    if installation_id and not is_org:\n        router.get(f\"https://api.github.com/repos/{owner}/{repo}/installation\").mock(\n            return_value=respx.MockResponse(200, json={\"id\": installation_id})\n        )\n    else:\n        router.get(f\"https://api.github.com/repos/{owner}/{repo}/installation\").mock(\n            return_value=respx.MockResponse(404)\n        )\n\n    if installation_id:\n        router.post(\n            f\"https://api.github.com/app/installations/{installation_id}/access_tokens\"\n        ).mock(\n            return_value=respx.MockResponse(\n                201, json={\"token\": f\"token-{installation_id}\"}\n            )\n        )\n\n\ndef mock_pat_access(\n    router: respx.Router, owner: str, repo: str, pat: str, accessible: bool\n) -> None:\n    router.get(\n        f\"https://api.github.com/repos/{owner}/{repo}\",\n        headers={\"Authorization\": f\"token {pat}\"},\n    ).mock(\n        return_value=respx.MockResponse(\n            200 if accessible else 404,\n            json={\"name\": repo} if accessible else {\"message\": \"Not Found\"},\n        )\n    )\n\n\ndef mock_api_throttled_public_repo(\n    router: respx.Router, owner: str, repo: str, status_code: int\n) -> None:\n    router.get(f\"https://api.github.com/repos/{owner}/{repo}\").mock(\n        return_value=respx.MockResponse(status_code)\n    )\n    router.get(f\"https://api.github.com/users/{owner}\").mock(\n        return_value=respx.MockResponse(status_code)\n    )\n\n\n@pytest.mark.asyncio\nasync def test_public_github_repo(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Test validation of public GitHub repository.\"\"\"\n    mock_github_repo_and_owner(\n        mock_github_api, owner=\"public\", repo=\"repo\", repo_private=False\n    )\n\n    # Override default to simulate public probe success via git\n    with patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=True):\n        result = await service.validate_repository(\n            \"https://github.com/public/repo\", project_id=project_id\n        )\n\n    assert result.accessible is True\n    assert \"public repository\" in result.message\n    # App name and installation URL are always returned for GitHub repos when GitHub App is configured\n    assert result.github_app_name == \"TestApp\"\n    assert (\n        result.github_app_installation_url\n        == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n    )\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"status_code\", [403, 429], ids=[\"forbidden\", \"rate-limited\"])\nasync def test_public_github_repo_uses_git_probe_instead_of_anonymous_api(\n    service: GitService,\n    project_id: str,\n    mock_github_api: respx.Router,\n    status_code: int,\n) -> None:\n    \"\"\"Public GitHub detection should rely on the git transport probe, not the REST API.\"\"\"\n    mock_api_throttled_public_repo(\n        mock_github_api, owner=\"public\", repo=\"repo\", status_code=status_code\n    )\n\n    with patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=True):\n        result = await service.validate_repository(\n            \"https://github.com/public/repo\", project_id=project_id\n        )\n\n    assert result.accessible is True\n    assert \"public repository\" in result.message\n    assert result.github_app_name is None\n\n\n@pytest.mark.asyncio\nasync def test_private_github_repo_with_app_access(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Test private repo with GitHub App installation.\"\"\"\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"private\",\n        repo=\"app-repo\",\n        repo_private=True,\n        installation_id=12345,\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/private/app-repo\", project_id=project_id\n    )\n\n    assert result.accessible is True\n    assert \"GitHub App installation\" in result.message\n    assert result.github_app_name == \"TestApp\"\n    assert (\n        result.github_app_installation_url\n        == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n    )\n    # Settings URL for user account\n    assert (\n        result.github_app_settings_url\n        == \"https://github.com/settings/installations/12345\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_private_github_repo_with_valid_pat(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    mock_pat_access(mock_github_api, \"private\", \"pat-valid-repo\", \"valid_token\", True)\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"private\",\n        repo=\"pat-valid-repo\",\n        repo_private=True,\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/private/pat-valid-repo\",\n        project_id=project_id,\n        pat=\"valid_token\",\n    )\n\n    assert result.accessible is True\n    assert \"Personal Access Token\" in result.message\n    # App name and installation URL are always returned for GitHub repos when GitHub App is configured\n    assert result.github_app_name == \"TestApp\"\n    assert (\n        result.github_app_installation_url\n        == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_private_github_repo_with_invalid_pat(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Test private repo with invalid PAT.\"\"\"\n    mock_pat_access(\n        mock_github_api, \"private\", \"pat-invalid-repo\", \"invalid_token\", False\n    )\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"private\",\n        repo=\"pat-invalid-repo\",\n        repo_private=True,\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/private/pat-invalid-repo\",\n        project_id=project_id,\n        pat=\"invalid_token\",\n    )\n\n    assert result.accessible is False\n    assert \"does not have access\" in result.message\n    assert result.github_app_name == \"TestApp\"\n\n\n@pytest.mark.asyncio\nasync def test_inaccessible_github_repo(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Test completely inaccessible GitHub repository.\"\"\"\n\n    mock_github_api.get(\"https://api.github.com/users/inaccessible\").mock(\n        return_value=respx.MockResponse(\n            200, json={\"id\": 12345, \"login\": \"inaccessible\", \"type\": \"User\"}\n        )\n    )\n\n    mock_github_api.get(\"https://api.github.com/repos/inaccessible/repo\").mock(\n        return_value=respx.MockResponse(404)\n    )\n    mock_github_api.get(\n        \"https://api.github.com/repos/inaccessible/repo/installation\"\n    ).mock(return_value=respx.MockResponse(404))\n    mock_github_api.get(\"https://api.github.com/orgs/inaccessible/installation\").mock(\n        return_value=respx.MockResponse(404)\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/inaccessible/repo\", project_id=project_id\n    )\n\n    assert result.accessible is False\n    assert \"Unable to access GitHub repository 'inaccessible/repo'\" in result.message\n    assert result.github_app_name == \"TestApp\"\n    assert (\n        result.github_app_installation_url\n        == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_public_generic_repo(\n    service: GitService, project_id: str, github_app_auth: GitHubAppAuth\n) -> None:\n    \"\"\"Test validation of public non-GitHub repository.\"\"\"\n    with patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=True):\n        result = await service.validate_repository(\n            \"https://gitlab.com/public/repo.git\", project_id=project_id\n        )\n\n        assert result.accessible is True\n        assert \"public repository\" in result.message\n        assert result.github_app_name is None  # No GitHub App for non-GitHub repos\n\n\n@pytest.mark.asyncio\nasync def test_private_generic_repo_with_valid_pat(\n    service: GitService, project_id: str, github_app_auth: GitHubAppAuth\n) -> None:\n    \"\"\"Test private non-GitHub repo with valid credentials.\"\"\"\n    with (\n        patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=False),\n        patch(f\"{GIT_SERVICE}.validate_git_credential_access\", return_value=True),\n    ):\n        result = await service.validate_repository(\n            \"https://gitlab.com/private/valid-pat-repo.git\",\n            project_id=project_id,\n            pat=\"valid_token\",\n        )\n\n        assert result.accessible is True\n        assert \"Personal Access Token\" in result.message\n        assert result.github_app_installation_url is None\n\n\n@pytest.mark.asyncio\nasync def test_private_generic_repo_with_invalid_pat(\n    service: GitService, project_id: str, github_app_auth: GitHubAppAuth\n) -> None:\n    \"\"\"Test private non-GitHub repo with invalid credentials.\"\"\"\n    with (\n        patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=False),\n        patch(f\"{GIT_SERVICE}.validate_git_credential_access\", return_value=False),\n    ):\n        result = await service.validate_repository(\n            \"https://gitlab.com/private/invalid-pat-repo.git\",\n            project_id=project_id,\n            pat=\"invalid_token\",\n        )\n\n        assert result.accessible is False\n        assert \"does not have access\" in result.message\n        assert result.github_app_installation_url is None\n\n\n@pytest.mark.asyncio\nasync def test_inaccessible_generic_repo(\n    service: GitService, project_id: str, github_app_auth: GitHubAppAuth\n) -> None:\n    \"\"\"Test completely inaccessible non-GitHub repository.\"\"\"\n    with patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=False):\n        result = await service.validate_repository(\n            \"https://gitlab.com/inaccessible/repo.git\", project_id=project_id\n        )\n\n        assert result.accessible is False\n        assert \"private or does not exist\" in result.message\n\n\n@pytest.mark.asyncio\nasync def test_github_detection(\n    service: GitService, github_app_auth: GitHubAppAuth\n) -> None:\n    \"\"\"Test GitHub repository detection.\"\"\"\n    assert service._is_github_repository(\"https://github.com/owner/repo\") is True\n    assert service._is_github_repository(\"git@github.com:owner/repo.git\") is True\n    assert service._is_github_repository(\"github.com/owner/repo\") is True\n\n\n@pytest.mark.asyncio\nasync def test_non_github_detection(\n    service: GitService, github_app_auth: GitHubAppAuth\n) -> None:\n    \"\"\"Test non-GitHub repository detection.\"\"\"\n    assert service._is_github_repository(\"https://gitlab.com/owner/repo\") is False\n    assert service._is_github_repository(\"https://bitbucket.org/owner/repo\") is False\n    assert service._is_github_repository(\"https://git.example.com/repo\") is False\n\n\n@pytest.mark.asyncio\nasync def test_is_github_repository_rejects_spoofed_urls(\n    service: GitService, github_app_auth: GitHubAppAuth\n) -> None:\n    \"\"\"Test that URLs with github.com in the path but not the host are rejected.\"\"\"\n    assert (\n        service._is_github_repository(\"https://evil.com/github.com/owner/repo\") is False\n    )\n    assert service._is_github_repository(\"https://evil.com?github.com\") is False\n    assert service._is_github_repository(\"https://notgithub.com/owner/repo\") is False\n\n\n@pytest.mark.asyncio\nasync def test_existing_deployment_with_pat(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Test using existing deployment's PAT.\"\"\"\n    with patch(\n        f\"{GIT_SERVICE}.k8s_client.get_deployment_pat\",\n        return_value=\"existing_token\",\n    ):\n        mock_pat_access(\n            mock_github_api, \"existing\", \"deployment-repo\", \"existing_token\", True\n        )\n        mock_github_repo_and_owner(\n            mock_github_api,\n            owner=\"existing\",\n            repo=\"deployment-repo\",\n            repo_private=True,\n        )\n        result = await service.validate_repository(\n            \"https://github.com/existing/deployment-repo\",\n            project_id=project_id,\n            deployment_id=\"test-deployment\",\n        )\n\n        assert result.accessible is True\n        assert \"existing Personal Access Token\" in result.message\n        # App name and installation URL are always returned for GitHub repos when GitHub App is configured\n        assert result.github_app_name == \"TestApp\"\n        assert (\n            result.github_app_installation_url\n            == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n        )\n\n\n@pytest.mark.asyncio\nasync def test_pat_obsolete_detection(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Test PAT obsolescence detection.\"\"\"\n    with patch(\n        f\"{GIT_SERVICE}.k8s_client.get_deployment_pat\",\n        return_value=\"existing_token\",\n    ):\n        mock_github_repo_and_owner(\n            mock_github_api,\n            owner=\"obsolete\",\n            repo=\"pat-repo\",\n            repo_private=False,\n        )\n        result = await service.validate_repository(\n            \"https://github.com/obsolete/pat-repo\",\n            project_id=project_id,\n            deployment_id=\"test-deployment-obsolete\",\n        )\n\n        assert result.accessible is True\n        assert result.pat_is_obsolete is True\n        # App name and installation URL are always returned for GitHub repos when GitHub App is configured\n        assert result.github_app_name == \"TestApp\"\n        assert (\n            result.github_app_installation_url\n            == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n        )\n\n\n@pytest.mark.asyncio\nasync def test_invalid_github_repository_url_single_segment(\n    service: GitService,\n    project_id: str,\n) -> None:\n    \"\"\"Ensure we surface a helpful error when repository path lacks a repo segment.\"\"\"\n    result = await service.validate_repository(\n        \"https://github.com/just-owner\", project_id=project_id\n    )\n\n    assert result.accessible is False\n    assert \"Invalid GitHub repository URL\" in result.message\n\n\n@pytest.mark.asyncio\nasync def test_github_owner_missing(\n    service: GitService,\n    project_id: str,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Return a precise error when the GitHub owner cannot be found.\"\"\"\n    mock_github_api.get(\"https://api.github.com/users/missing-owner\").mock(\n        return_value=respx.MockResponse(404)\n    )\n    mock_github_api.get(\"https://api.github.com/repos/missing-owner/repo\").mock(\n        return_value=respx.MockResponse(404)\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/missing-owner/repo\", project_id=project_id\n    )\n\n    assert result.accessible is False\n    assert \"GitHub owner 'missing-owner' does not exist\" in result.message\n\n\n@pytest.mark.asyncio\nasync def test_github_org_installation_all_repos_accessible(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Org-wide installation with all repositories should authenticate successfully.\"\"\"\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"org-owner\",\n        repo=\"visible-repo\",\n        repo_private=True,\n        repo_accessible=True,\n        owner_exists=True,\n        installation_id=500,\n        is_org=True,\n        repository_selection=\"all\",\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/org-owner/visible-repo\", project_id=project_id\n    )\n\n    assert result.accessible is True\n    assert \"GitHub App installation\" in result.message\n    assert result.github_app_name == \"TestApp\"\n    assert (\n        result.github_app_installation_url\n        == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n    )\n    # Settings URL for org account\n    assert (\n        result.github_app_settings_url\n        == \"https://github.com/organizations/org-owner/settings/installations/500\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_github_org_installation_repo_missing(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Org-wide GitHub App installs should not mark non-existent repos as accessible.\"\"\"\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"org-owner\",\n        repo=\"ghost-repo\",\n        repo_private=True,\n        owner_exists=True,\n        installation_id=999,\n        is_org=True,\n        repository_selection=\"all\",\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/org-owner/ghost-repo\", project_id=project_id\n    )\n\n    assert result.accessible is False\n    assert \"GitHub repository 'org-owner/ghost-repo' does not exist\" in result.message\n    assert result.github_app_name is None\n    assert result.github_app_installation_url is None\n    assert result.github_app_settings_url is None\n\n\n@pytest.mark.asyncio\nasync def test_github_org_installation_selected_repos_without_access(\n    service: GitService,\n    project_id: str,\n    github_app_auth: GitHubAppAuth,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"Provide targeted guidance when the GitHub App installation omits the requested repo.\"\"\"\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"org-owner\",\n        repo=\"restricted-repo\",\n        repo_private=True,\n        owner_exists=True,\n        installation_id=1001,\n        is_org=True,\n        repository_selection=\"selected\",\n    )\n\n    result = await service.validate_repository(\n        \"https://github.com/org-owner/restricted-repo\", project_id=project_id\n    )\n\n    assert result.accessible is False\n    assert \"does not currently include 'org-owner/restricted-repo'\" in result.message\n    assert result.github_app_name == \"TestApp\"\n    assert (\n        result.github_app_installation_url\n        == \"https://github.com/apps/TestApp/installations/new/permissions?target_id=12345\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_validate_github_application_uses_contents_api_not_clone(\n    service: GitService,\n    project_id: str,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"validate_git_application for a GitHub URL must not call clone_repo.\"\"\"\n    # Repo is public so the access lookup short-circuits.\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"acme\",\n        repo=\"agent-app\",\n        repo_private=False,\n    )\n\n    # Resolve a branch ref to a SHA via the Commits API.\n    mock_github_api.get(\n        \"https://api.github.com/repos/acme/agent-app/commits/main\"\n    ).mock(\n        return_value=respx.MockResponse(\n            200,\n            json={\"sha\": \"abc123def4567890abc123def4567890abc12345\"},\n        )\n    )\n\n    # Provide a pyproject.toml with a [tool.llamadeploy] block via Contents API.\n    pyproject_toml = (\n        b\"[project]\\nname = 'agent-app'\\n\\n\"\n        b\"[tool.llamadeploy]\\nname = 'agent-app'\\n\"\n        b\"workflows = { default = 'agent_app.workflow:app' }\\n\"\n    )\n    encoded = base64.b64encode(pyproject_toml).decode()\n    mock_github_api.get(\n        \"https://api.github.com/repos/acme/agent-app/contents/pyproject.toml\"\n    ).mock(\n        return_value=respx.MockResponse(\n            200,\n            json={\n                \"type\": \"file\",\n                \"name\": \"pyproject.toml\",\n                \"content\": encoded,\n                \"encoding\": \"base64\",\n            },\n        )\n    )\n    # Other config candidates return 404.\n    for filename in (\n        \"llama_agents.toml\",\n        \"llama_deploy.toml\",\n        \"llama_agents.yaml\",\n        \"llama_deploy.yaml\",\n    ):\n        mock_github_api.get(\n            f\"https://api.github.com/repos/acme/agent-app/contents/{filename}\"\n        ).mock(return_value=respx.MockResponse(404))\n\n    with (\n        patch(\n            f\"{GIT_SERVICE}.clone_repo\",\n            side_effect=AssertionError(\"clone_repo must not be called for GitHub URLs\"),\n        ),\n        patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=True),\n    ):\n        result = await service.validate_git_application(\n            repository_url=\"https://github.com/acme/agent-app\",\n            git_ref=\"main\",\n            deployment_file_path=\".\",\n        )\n\n    assert result.is_valid is True\n    assert result.git_sha == \"abc123def4567890abc123def4567890abc12345\"\n    assert result.git_ref == \"main\"\n\n\n@pytest.mark.asyncio\nasync def test_validate_github_application_missing_config(\n    service: GitService,\n    project_id: str,\n    mock_github_api: respx.Router,\n) -> None:\n    \"\"\"A repo with no recognized config files should report a clear error.\"\"\"\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"acme\",\n        repo=\"empty-app\",\n        repo_private=False,\n    )\n    mock_github_api.get(\n        \"https://api.github.com/repos/acme/empty-app/commits/main\"\n    ).mock(return_value=respx.MockResponse(200, json={\"sha\": \"f\" * 40}))\n    for filename in (\n        \"llama_agents.toml\",\n        \"llama_deploy.toml\",\n        \"pyproject.toml\",\n        \"llama_agents.yaml\",\n        \"llama_deploy.yaml\",\n    ):\n        mock_github_api.get(\n            f\"https://api.github.com/repos/acme/empty-app/contents/{filename}\"\n        ).mock(return_value=respx.MockResponse(404))\n\n    with (\n        patch(\n            f\"{GIT_SERVICE}.clone_repo\",\n            side_effect=AssertionError(\"clone_repo must not be called for GitHub URLs\"),\n        ),\n        patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=True),\n    ):\n        result = await service.validate_git_application(\n            repository_url=\"https://github.com/acme/empty-app\",\n            git_ref=\"main\",\n            deployment_file_path=\".\",\n        )\n\n    assert result.is_valid is False\n    assert \"No deployment config found\" in (result.error_message or \"\")\n\n\n@pytest.mark.asyncio\nasync def test_validate_github_application_encodes_slash_ref(\n    service: GitService,\n    project_id: str,\n    mock_github_api: respx.Router,\n) -> None:\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"acme\",\n        repo=\"agent-app\",\n        repo_private=False,\n    )\n\n    mock_github_api.get(\n        \"https://api.github.com/repos/acme/agent-app/commits/feature%2Fui\"\n    ).mock(\n        return_value=respx.MockResponse(\n            200,\n            json={\"sha\": \"1234567890abcdef1234567890abcdef12345678\"},\n        )\n    )\n\n    pyproject_toml = (\n        b\"[project]\\nname = 'agent-app'\\n\\n\"\n        b\"[tool.llamadeploy]\\nname = 'agent-app'\\n\"\n        b\"workflows = { default = 'agent_app.workflow:app' }\\n\"\n    )\n    encoded = base64.b64encode(pyproject_toml).decode()\n    mock_github_api.get(\n        \"https://api.github.com/repos/acme/agent-app/contents/pyproject.toml\"\n    ).mock(\n        return_value=respx.MockResponse(\n            200,\n            json={\n                \"type\": \"file\",\n                \"name\": \"pyproject.toml\",\n                \"content\": encoded,\n                \"encoding\": \"base64\",\n            },\n        )\n    )\n    for filename in (\n        \"llama_agents.toml\",\n        \"llama_deploy.toml\",\n        \"llama_agents.yaml\",\n        \"llama_deploy.yaml\",\n    ):\n        mock_github_api.get(\n            f\"https://api.github.com/repos/acme/agent-app/contents/{filename}\"\n        ).mock(return_value=respx.MockResponse(404))\n\n    with patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=True):\n        result = await service.validate_git_application(\n            repository_url=\"https://github.com/acme/agent-app\",\n            git_ref=\"feature/ui\",\n            deployment_file_path=\".\",\n        )\n\n    assert result.is_valid is True\n    assert result.git_ref == \"feature/ui\"\n    assert result.git_sha == \"1234567890abcdef1234567890abcdef12345678\"\n\n\n@pytest.mark.asyncio\nasync def test_validate_github_repository_ssh_url_handles_public_probe_error(\n    service: GitService,\n    project_id: str,\n    mock_github_api: respx.Router,\n) -> None:\n    mock_github_api.get(\"https://api.github.com/repos/acme/private-repo\").mock(\n        return_value=respx.MockResponse(404)\n    )\n    mock_github_api.get(\"https://api.github.com/users/acme\").mock(\n        return_value=respx.MockResponse(\n            200,\n            json={\"id\": 123, \"login\": \"acme\", \"type\": \"Organization\"},\n        )\n    )\n\n    with patch(\n        f\"{GIT_SERVICE}.validate_git_public_access\",\n        side_effect=GitAccessError(\"Only HTTP(S) URLs are supported for probe\"),\n    ):\n        result = await service.validate_repository(\n            \"git@github.com:acme/private-repo.git\",\n            project_id=project_id,\n        )\n\n    assert result.accessible is False\n    assert \"Unable to access GitHub repository 'acme/private-repo'\" in result.message\n\n\n@pytest.mark.asyncio\nasync def test_validate_github_application_rejects_traversal_path(\n    service: GitService,\n    project_id: str,\n    mock_github_api: respx.Router,\n) -> None:\n    mock_github_repo_and_owner(\n        mock_github_api,\n        owner=\"acme\",\n        repo=\"agent-app\",\n        repo_private=False,\n    )\n    mock_github_api.get(\n        \"https://api.github.com/repos/acme/agent-app/commits/main\"\n    ).mock(return_value=respx.MockResponse(200, json={\"sha\": \"a\" * 40}))\n\n    with (\n        patch(\n            f\"{GIT_SERVICE}.clone_repo\",\n            side_effect=AssertionError(\"clone_repo must not be called for GitHub URLs\"),\n        ),\n        patch(f\"{GIT_SERVICE}.validate_git_public_access\", return_value=True),\n    ):\n        result = await service.validate_git_application(\n            repository_url=\"https://github.com/acme/agent-app\",\n            git_ref=\"main\",\n            deployment_file_path=\"../../../../tmp/owned/llama_agents.yaml\",\n        )\n\n    assert result.is_valid is False\n    assert result.error_message == (\n        \"Invalid deployment path: Path must stay within the repository: \"\n        \"../../../../tmp/owned/llama_agents.yaml\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_fetch_ui_package_json_rejects_path_traversal() -> None:\n    class UnexpectedClient:\n        async def get_file_contents(\n            self, owner: str, repo: str, path: str, ref: str\n        ) -> bytes | None:\n            raise AssertionError(f\"unexpected GitHub contents fetch for {path}\")\n\n    with pytest.raises(ValueError, match=\"Path must stay within the repository\"):\n        await GitService._fetch_ui_package_json(\n            cast(GitHubApiClient, UnexpectedClient()),\n            \"acme\",\n            \"agent-app\",\n            \"main\",\n            \"configs/llama_agents.yaml\",\n            \"../../outside\",\n            Path(\"/tmp/repo\"),\n        )\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_k8s_client.py",
    "content": "\"\"\"Unit tests for k8s_client.py\"\"\"\n\nimport asyncio\nimport base64\nimport sys\nfrom collections.abc import Iterator\nfrom typing import Generator, TypedDict\nfrom unittest.mock import AsyncMock, MagicMock, Mock, patch\n\nimport pytest\nfrom kubernetes.client import (\n    V1Container,\n    V1ObjectMeta,\n    V1OwnerReference,\n    V1Pod,\n    V1PodSpec,\n    V1ReplicaSet,\n)\nfrom kubernetes.client.exceptions import ApiException\nfrom llama_agents.control_plane import k8s_client\nfrom llama_agents.control_plane.k8s_client import (\n    LogLine,\n    _append_random_suffix,\n    get_replicaset_pods_for_deployment,\n    stream_container_logs,\n)\nfrom llama_agents.core.schema.deployments import (\n    DeploymentResponse,\n    DeploymentUpdate,\n    LlamaDeploymentCRD,\n)\n\nif sys.version_info >= (3, 11):\n    from typing import Unpack\nelse:\n    from typing_extensions import Unpack\n\n\n@pytest.fixture\ndef mock_k8s() -> Generator[MagicMock, None, None]:\n    with patch(\"llama_agents.control_plane.k8s_client._k8s_client\") as mock_k8s:\n        yield mock_k8s\n\n\n@pytest.fixture\ndef mock_validate() -> Generator[MagicMock, None, None]:\n    with patch(\n        \"llama_agents.control_plane.k8s_client.validate_deployment_id\"\n    ) as mock_validate_deployment_id:\n        yield mock_validate_deployment_id\n\n\nclass DeploymentMockParams(TypedDict, total=False):\n    name: str\n    namespace: str\n    deployment_id: str\n    project_id: str\n    repo_url: str\n    git_ref: str\n    deployment_file_path: str\n    secret_name: str | None\n    status: str\n    auth_token: str | None\n\n\ndef create_deployment_mock(\n    name: str = \"test-deploy\",\n    namespace: str = \"llama-agents\",\n    deployment_id: str = \"test-deploy\",\n    project_id: str = \"test-project\",\n    repo_url: str = \"https://github.com/user/repo.git\",\n    git_ref: str = \"main\",\n    deployment_file_path: str = \"llama_deploy.yaml\",\n    secret_name: str | None = None,\n    status: str = \"Running\",\n    auth_token: str | None = None,\n) -> dict[str, object]:\n    return {\n        \"metadata\": {\"name\": name, \"namespace\": namespace},\n        \"spec\": {\n            \"name\": deployment_id,\n            \"projectId\": project_id,\n            \"repoUrl\": repo_url,\n            \"gitRef\": git_ref,\n            \"deploymentFilePath\": deployment_file_path,\n            \"secretName\": secret_name,\n        },\n        \"status\": {\"phase\": status, \"authToken\": auth_token},\n    }\n\n\ndef create_deployment_mock_crd(\n    **kwargs: Unpack[DeploymentMockParams],\n) -> LlamaDeploymentCRD:\n    return LlamaDeploymentCRD.model_validate(create_deployment_mock(**kwargs))\n\n\ndef test_append_random_suffix() -> None:\n    \"\"\"Test random suffix generation\"\"\"\n    result = _append_random_suffix(\"test\", 20)\n    assert result.startswith(\"test-\")\n    assert len(result) == 10  # \"test-\" + 5 chars\n\n    # Test with empty string\n    result = _append_random_suffix(\"\", 10)\n    assert len(result) == 5  # Just the random part\n\n    # Test truncation - takes max_length - 5 - 1 chars, then dash, then 5 hex chars\n    long_name = \"a\" * 50\n    result = _append_random_suffix(long_name, 20)\n    assert len(result) == 20\n    assert result == \"aaaaaaaaaaaaaa-\" + result[-5:]  # 14 a's + dash + 5 hex chars\n    assert len(result.split(\"-\")[-1]) == 5  # Last part is 5 hex chars\n\n\n# Integration-style tests for the main functions\n@pytest.mark.asyncio\nasync def test_validate_deployment_id_available(mock_k8s: MagicMock) -> None:\n    \"\"\"Test deployment ID validation when available\"\"\"\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.side_effect = ApiException(\n        status=404\n    )\n\n    result = await k8s_client.validate_deployment_id(\"test-deploy\")\n    assert result is True\n\n\n@pytest.mark.asyncio\nasync def test_validate_deployment_id_taken(mock_k8s: MagicMock) -> None:\n    \"\"\"Test deployment ID validation when taken\"\"\"\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        create_deployment_mock(\n            name=\"test\",\n            deployment_id=\"test\",\n        )\n    )\n\n    result = await k8s_client.validate_deployment_id(\"test-deploy\")\n    assert result is False\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_first_try(mock_validate: MagicMock) -> None:\n    \"\"\"Test deployment ID generation when first attempt works\"\"\"\n    mock_validate.return_value = True\n\n    result = await k8s_client.find_deployment_id(\"My Service\")\n    assert result == \"my-service\"\n    mock_validate.assert_called_once_with(\"my-service\")\n\n\n@pytest.mark.asyncio\nasync def test_find_deployment_id_with_collision(mock_validate: MagicMock) -> None:\n    \"\"\"Test deployment ID generation with name collision\"\"\"\n    # First call returns False (taken), second returns True (available)\n    mock_validate.side_effect = [False, True]\n\n    result = await k8s_client.find_deployment_id(\"test\")\n    # Should get a random suffix since \"test\" was taken\n    assert result.startswith(\"test-\")\n    assert len(result) == 10  # \"test-\" + 5 random chars\n    assert mock_validate.call_count == 2\n\n\n@pytest.mark.asyncio\nasync def test_validate_deployment_token_success(mock_k8s: MagicMock) -> None:\n    \"\"\"Test successful token validation\"\"\"\n    mock_deployment = create_deployment_mock(\n        name=\"test-deployment\",\n        auth_token=\"valid-token-123\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        mock_deployment\n    )\n\n    result = await k8s_client.validate_deployment_token(\n        \"test-deployment\", \"valid-token-123\"\n    )\n    assert result is not None\n    assert result.status.authToken == \"valid-token-123\"\n    assert result.metadata.name == \"test-deployment\"\n\n\n@pytest.mark.asyncio\nasync def test_validate_deployment_token_wrong_token(mock_k8s: MagicMock) -> None:\n    \"\"\"Test token validation with wrong token\"\"\"\n    mock_deployment = create_deployment_mock(\n        name=\"test-deployment\",\n        auth_token=\"different-token\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        mock_deployment\n    )\n\n    result = await k8s_client.validate_deployment_token(\n        \"test-deployment\", \"wrong-token\"\n    )\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_validate_deployment_token_no_token(mock_k8s: MagicMock) -> None:\n    \"\"\"Test token validation when deployment has no token\"\"\"\n    mock_deployment = create_deployment_mock(\n        name=\"test-deployment\",\n        auth_token=None,\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        mock_deployment\n    )\n\n    result = await k8s_client.validate_deployment_token(\"test-deployment\", \"any-token\")\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_validate_deployment_token_deployment_not_found(\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Test token validation when deployment doesn't exist\"\"\"\n    from kubernetes.client.exceptions import ApiException\n\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.side_effect = ApiException(\n        status=404\n    )\n\n    result = await k8s_client.validate_deployment_token(\n        \"nonexistent-deployment\", \"any-token\"\n    )\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_get_secret_names_success(mock_k8s: MagicMock) -> None:\n    \"\"\"Test successful retrieval of secret names\"\"\"\n    mock_secret = Mock()\n    mock_secret.data = {\n        \"API_KEY\": \"value1\",\n        \"DATABASE_URL\": \"value2\",\n        \"GITHUB_PAT\": \"token\",\n    }\n    mock_k8s.k8s_core_v1.read_namespaced_secret.return_value = mock_secret\n\n    result = await k8s_client.get_secret_names(\"test-secret\")\n    assert set(result) == {\"API_KEY\", \"DATABASE_URL\", \"GITHUB_PAT\"}\n\n\n@pytest.mark.asyncio\nasync def test_get_secret_names_not_found(mock_k8s: MagicMock) -> None:\n    \"\"\"Test handling of secret not found\"\"\"\n    mock_k8s.k8s_core_v1.read_namespaced_secret.side_effect = ApiException(404)\n\n    result = await k8s_client.get_secret_names(\"nonexistent-secret\")\n    assert result == []\n\n\n@pytest.mark.asyncio\nasync def test_get_secret_names_empty_secret(mock_k8s: MagicMock) -> None:\n    \"\"\"Test handling of secret with no data\"\"\"\n    mock_secret = Mock()\n    mock_secret.data = None\n    mock_k8s.k8s_core_v1.read_namespaced_secret.return_value = mock_secret\n\n    result = await k8s_client.get_secret_names(\"empty-secret\")\n    assert result == []\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_secret_names\")\n@pytest.mark.asyncio\nasync def test_get_secret_names_batch(mock_get_secret_names: MagicMock) -> None:\n    \"\"\"Test batch retrieval of secret names\"\"\"\n    # Mock individual calls\n    mock_get_secret_names.side_effect = [\n        [\"API_KEY\", \"DATABASE_URL\"],\n        [\"GITHUB_PAT\"],\n        None,\n    ]\n\n    result = await k8s_client.get_secret_names_batch([\"secret1\", \"secret2\", \"secret3\"])\n\n    expected = {\n        \"secret1\": [\"API_KEY\", \"DATABASE_URL\"],\n        \"secret2\": [\"GITHUB_PAT\"],\n        \"secret3\": None,\n    }\n    assert result == expected\n\n\ndef test_llamadeployment_crd_missing_status(mock_k8s: MagicMock) -> None:\n    \"\"\"CRDs without a status field (not yet reconciled) should default to Pending phase\"\"\"\n    raw = create_deployment_mock(name=\"new-deploy\", deployment_id=\"new-deploy\")\n    del raw[\"status\"]\n\n    crd = LlamaDeploymentCRD.model_validate(raw)\n    assert crd.status.phase is None\n\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n    response = k8s_client._llamadeployment_to_response(crd)\n    assert response.status == \"Pending\"\n\n\ndef test_llamadeployment_to_response_basic(mock_k8s: MagicMock) -> None:\n    \"\"\"Test basic conversion from LlamaDeployment to DeploymentResponse\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"test-deployment\",\n        deployment_id=\"test-deployment\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/test/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        secret_name=\"test-secret\",\n    )\n\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment)\n\n    assert isinstance(result, DeploymentResponse)\n    assert result.id == \"test-deployment\"\n    assert result.name == \"test-deployment\"\n    assert result.repo_url == \"https://github.com/test/repo.git\"\n    assert result.git_ref == \"abc123\"\n    assert result.has_personal_access_token is False\n    assert result.secret_names is None\n\n\ndef test_llamadeployment_to_response_with_secrets(mock_k8s: MagicMock) -> None:\n    \"\"\"Test conversion with secret names including GITHUB_PAT filtering\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"test-deployment\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/test/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        secret_name=\"test-secret\",\n    )\n\n    secret_names = [\"API_KEY\", \"DATABASE_URL\", \"GITHUB_PAT\"]\n\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment, secret_names)\n\n    assert result.has_personal_access_token is True  # GITHUB_PAT was present\n    assert result.secret_names == [\n        \"API_KEY\",\n        \"DATABASE_URL\",\n    ]\n\n\ndef test_llamadeployment_to_response_only_github_pat(mock_k8s: MagicMock) -> None:\n    \"\"\"Test conversion when only GITHUB_PAT is in secrets\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"test-deployment\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/test/repo.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"deploy.yml\",\n        secret_name=\"test-secret\",\n    )\n\n    secret_names = [\"GITHUB_PAT\"]\n\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment, secret_names)\n\n    assert result.has_personal_access_token is True\n    assert result.secret_names is None  # No secrets left after filtering GITHUB_PAT\n\n\ndef test_llamadeployment_to_response_with_ingress(mock_k8s: MagicMock) -> None:\n    \"\"\"Test conversion with ingress enabled\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"test-deployment\",\n        git_ref=\"abc123\",\n    )\n\n    mock_k8s.enable_ingress = True\n    mock_k8s.domain = \"127.0.0.1.nip.io\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment)\n\n    assert result.apiserver_url is not None\n    assert \"test-deployment.127.0.0.1.nip.io:8090\" in str(result.apiserver_url)\n\n\ndef test_llamadeployment_to_response_building_mapped_to_pending(\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Building phase is mapped to Pending for backward-compatible clients.\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"build-deploy\",\n        status=\"Building\",\n    )\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment)\n\n    assert result.status == \"Pending\"\n    assert result.warning is not None\n    assert \"Building\" in result.warning\n\n\ndef test_llamadeployment_to_response_buildfailed_mapped_to_failed(\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"BuildFailed phase is mapped to Failed for backward-compatible clients.\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"fail-deploy\",\n        status=\"BuildFailed\",\n    )\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment)\n\n    assert result.status == \"Failed\"\n    assert result.warning is not None\n    assert \"BuildFailed\" in result.warning\n\n\ndef test_llamadeployment_to_response_running_not_mapped(\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Non-build phases pass through unchanged with no warning.\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"run-deploy\",\n        status=\"Running\",\n    )\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment)\n\n    assert result.status == \"Running\"\n    assert result.warning is None\n\n\ndef test_llamadeployment_to_response_awaitingcode_mapped_to_pending(\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"AwaitingCode phase is mapped to Pending for backward-compatible clients.\"\"\"\n    llamadeployment = create_deployment_mock_crd(\n        name=\"awaiting-deploy\",\n        status=\"AwaitingCode\",\n    )\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"default\"\n\n    result = k8s_client._llamadeployment_to_response(llamadeployment)\n\n    assert result.status == \"Pending\"\n    assert result.warning is not None\n    assert \"Waiting for code push\" in result.warning\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client._create_k8s_secret\")\n@pytest.mark.asyncio\nasync def test_update_deployment_basic_fields(\n    mock_create_secret: MagicMock,\n    mock_get_deployment: MagicMock,\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Test updating basic deployment fields without secrets\"\"\"\n\n    # Mock existing deployment\n    existing_deployment = create_deployment_mock(\n        name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/old-repo.git\",\n        deployment_file_path=\"old_deploy.yml\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        existing_deployment\n    )\n\n    # Mock final result\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/new-repo.git\",  # updated\n        git_ref=\"\",\n        deployment_file_path=\"new_deploy.yml\",  # updated\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n        status=\"Running\",\n    )\n\n    update = DeploymentUpdate(\n        repo_url=\"https://github.com/user/new-repo.git\",\n        deployment_file_path=\"new_deploy.yml\",\n    )\n\n    result = await k8s_client.update_deployment(\"test-deploy\", update)\n\n    # Verify the deployment was fetched\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.assert_called_once_with(\n        group=\"deploy.llamaindex.ai\",\n        version=\"v1\",\n        namespace=mock_k8s.namespace,\n        plural=\"llamadeployments\",\n        name=\"test-deploy\",\n    )\n\n    # Verify the deployment was updated with new spec\n    mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.assert_called_once()\n    call_args = mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.call_args\n    updated_body = call_args[1][\"body\"]\n\n    # Verify Kubernetes metadata is present\n    assert updated_body[\"apiVersion\"] == \"deploy.llamaindex.ai/v1\"\n    assert updated_body[\"kind\"] == \"LlamaDeployment\"\n\n    # Verify spec fields\n    assert updated_body[\"spec\"][\"repoUrl\"] == \"https://github.com/user/new-repo.git\"\n    assert updated_body[\"spec\"][\"deploymentFilePath\"] == \"new_deploy.yml\"\n    assert updated_body[\"spec\"][\"projectId\"] == \"test-project\"  # unchanged\n\n    # No secret operations should have been called\n    mock_create_secret.assert_not_called()\n\n    # Final deployment should be returned\n    assert result is not None\n    assert result.repo_url == \"https://github.com/user/new-repo.git\"\n\n\n@patch(\n    \"llama_agents.control_plane.k8s_client.get_deployment\",\n    new_callable=AsyncMock,\n)\n@patch(\n    \"llama_agents.control_plane.k8s_client._create_k8s_secret\",\n    new_callable=AsyncMock,\n)\n@pytest.mark.asyncio\nasync def test_update_deployment_with_secrets(\n    mock_create_secret: MagicMock,\n    mock_get_deployment: MagicMock,\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Test updating deployment with secret changes\"\"\"\n\n    # Mock existing deployment\n    existing_deployment = create_deployment_mock(\n        name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        deployment_file_path=\"deploy.yml\",\n        secret_name=\"test-deploy-secrets\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        existing_deployment\n    )\n\n    # Mock existing secret\n    mock_secret = Mock()\n    mock_secret.data = {\n        \"EXISTING_SECRET\": base64.b64encode(b\"old_value\").decode(),\n        \"REMOVE_ME\": base64.b64encode(b\"will_be_removed\").decode(),\n    }\n    mock_k8s.k8s_core_v1.read_namespaced_secret.return_value = mock_secret\n\n    # Mock final result\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"\",\n        deployment_file_path=\"deploy.yml\",\n        has_personal_access_token=True,  # PAT was added\n        secret_names=[\"EXISTING_SECRET\", \"NEW_SECRET\"],  # updated secrets\n        apiserver_url=None,\n        status=\"Running\",\n    )\n\n    update = DeploymentUpdate(\n        personal_access_token=\"ghp_newtoken\",\n        secrets={\n            \"NEW_SECRET\": \"new_value\",\n            \"REMOVE_ME\": None,  # remove this\n        },\n    )\n\n    result = await k8s_client.update_deployment(\"test-deploy\", update)\n\n    # Verify secret was read\n    mock_k8s.k8s_core_v1.read_namespaced_secret.assert_called_once_with(\n        name=\"test-deploy-secrets\",\n        namespace=mock_k8s.namespace,\n    )\n\n    # Verify secret was updated with correct changes\n    mock_create_secret.assert_called_once()\n    secret_call_args = mock_create_secret.call_args\n    updated_secrets = secret_call_args[0][1]  # Second argument\n\n    assert updated_secrets[\"GITHUB_PAT\"] == \"ghp_newtoken\"  # PAT added\n    assert updated_secrets[\"NEW_SECRET\"] == \"new_value\"  # new secret added\n    assert updated_secrets[\"EXISTING_SECRET\"] == \"old_value\"  # existing preserved\n    assert \"REMOVE_ME\" not in updated_secrets  # removed\n\n    # Verify deployment was updated\n    mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.assert_called_once()\n\n    assert result is not None\n    assert result.has_personal_access_token is True\n\n\n@pytest.mark.asyncio\nasync def test_update_deployment_not_found(mock_k8s: MagicMock) -> None:\n    \"\"\"Test updating non-existent deployment\"\"\"\n\n    # Mock deployment not found\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.side_effect = ApiException(\n        status=404\n    )\n\n    update = DeploymentUpdate(repo_url=\"https://github.com/user/new-repo.git\")\n\n    result = await k8s_client.update_deployment(\"nonexistent\", update)\n    assert result is None\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client._create_k8s_secret\")\n@pytest.mark.asyncio\nasync def test_update_deployment_secret_required_but_missing(\n    mock_create_secret: MagicMock,\n    mock_get_deployment: MagicMock,\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Test updating with secret changes but no existing secret - should create lazily\"\"\"\n\n    # Mock existing deployment without secret\n    existing_deployment = create_deployment_mock(\n        name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        deployment_file_path=\"deploy.yml\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        existing_deployment\n    )\n\n    # Mock final result\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"\",\n        deployment_file_path=\"deploy.yml\",\n        has_personal_access_token=True,\n        secret_names=None,\n        apiserver_url=None,\n        status=\"Running\",\n    )\n\n    update = DeploymentUpdate(personal_access_token=\"ghp_token\")\n\n    result = await k8s_client.update_deployment(\"test-deploy\", update)\n\n    # Verify secret was created with just the PAT\n    mock_create_secret.assert_called_once()\n    secret_call_args = mock_create_secret.call_args\n    secret_name = secret_call_args[0][0]\n    updated_secrets = secret_call_args[0][1]\n\n    assert secret_name == \"test-deploy-secrets\"\n    assert updated_secrets[\"GITHUB_PAT\"] == \"ghp_token\"\n\n    # Verify deployment was updated with secret name\n    mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.assert_called_once()\n    call_args = mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.call_args\n    updated_body = call_args[1][\"body\"]\n\n    # Verify Kubernetes metadata is present\n    assert updated_body[\"apiVersion\"] == \"deploy.llamaindex.ai/v1\"\n    assert updated_body[\"kind\"] == \"LlamaDeployment\"\n\n    # Verify spec fields\n    assert updated_body[\"spec\"][\"secretName\"] == \"test-deploy-secrets\"\n\n    assert result is not None\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@patch(\"llama_agents.control_plane.k8s_client._create_k8s_secret\")\n@pytest.mark.asyncio\nasync def test_update_deployment_secret_not_found(\n    mock_create_secret: MagicMock,\n    mock_get_deployment: MagicMock,\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Test updating when secret is referenced but doesn't exist - should create it\"\"\"\n\n    # Mock existing deployment with secret reference\n    existing_deployment = create_deployment_mock(\n        name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        deployment_file_path=\"deploy.yml\",\n        secret_name=\"missing-secret\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        existing_deployment\n    )\n\n    # Mock secret not found\n    mock_k8s.k8s_core_v1.read_namespaced_secret.side_effect = ApiException(status=404)\n\n    # Mock final result\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"\",\n        deployment_file_path=\"deploy.yml\",\n        has_personal_access_token=True,\n        secret_names=None,\n        apiserver_url=None,\n        status=\"Running\",\n    )\n\n    update = DeploymentUpdate(personal_access_token=\"ghp_token\")\n\n    result = await k8s_client.update_deployment(\"test-deploy\", update)\n\n    # Verify secret was created with just the PAT (starting from empty)\n    mock_create_secret.assert_called_once()\n    secret_call_args = mock_create_secret.call_args\n    secret_name = secret_call_args[0][0]\n    updated_secrets = secret_call_args[0][1]\n\n    assert secret_name == \"missing-secret\"\n    assert updated_secrets[\"GITHUB_PAT\"] == \"ghp_token\"\n\n    # Verify deployment was updated\n    mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.assert_called_once()\n\n    assert result is not None\n\n\n@pytest.mark.asyncio\nasync def test_update_deployment_api_error(mock_k8s: MagicMock) -> None:\n    \"\"\"Test API error during deployment update\"\"\"\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.side_effect = ApiException(\n        status=500, reason=\"Internal Server Error\"\n    )\n\n    update = DeploymentUpdate(repo_url=\"https://github.com/user/new-repo.git\")\n\n    with pytest.raises(ApiException):\n        await k8s_client.update_deployment(\"test-deploy\", update)\n\n\n# Tests for create_deployment with git_ref\n@patch(\"llama_agents.control_plane.k8s_client.find_deployment_id\")\n@pytest.mark.asyncio\nasync def test_create_deployment_with_git_ref(\n    mock_find_deployment_id: MagicMock,\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Test create_deployment with git_ref parameter\"\"\"\n    # Mock the dependencies\n    mock_find_deployment_id.return_value = \"test-deploy\"\n\n    # Mock the k8s client calls\n    mock_k8s.k8s_custom_objects.create_namespaced_custom_object.return_value = {}\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"test-namespace\"\n\n    result = await k8s_client.create_deployment(\n        project_id=\"test-project\",\n        display_name=\"Test Deploy\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n    )\n\n    # Verify the deployment was created\n    assert result.git_ref == \"main\"\n    assert result.display_name == \"Test Deploy\"\n\n\n@patch(\"llama_agents.control_plane.k8s_client.find_deployment_id\")\n@patch(\"llama_agents.control_plane.k8s_client._create_k8s_secret\")\n@pytest.mark.asyncio\nasync def test_create_deployment_with_git_ref_and_pat(\n    mock_create_secret: MagicMock,\n    mock_find_deployment_id: MagicMock,\n    mock_k8s: MagicMock,\n) -> None:\n    \"\"\"Test create_deployment with git_ref and PAT\"\"\"\n    mock_find_deployment_id.return_value = \"test-deploy\"\n    mock_create_secret.return_value = None\n\n    # Mock the k8s client calls\n    mock_k8s.k8s_custom_objects.create_namespaced_custom_object.return_value = {}\n    mock_k8s.enable_ingress = False\n    mock_k8s.namespace = \"test-namespace\"\n\n    result = await k8s_client.create_deployment(\n        project_id=\"test-project\",\n        display_name=\"Test Deploy\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"feature-branch\",\n        pat=\"ghp_token123\",\n    )\n\n    assert result.git_ref == \"feature-branch\"\n    assert result.has_personal_access_token is True\n\n\n# Tests for update_deployment with git_ref validation\n@pytest.mark.asyncio\nasync def test_update_deployment_git_ref_validation_error(mock_k8s: MagicMock) -> None:\n    \"\"\"Test update_deployment with invalid git_ref continues with warning\"\"\"\n    # Mock existing deployment\n    existing_deployment = create_deployment_mock(\n        name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        existing_deployment\n    )\n\n    # Mock updated deployment for get_deployment call\n    mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.return_value = (\n        existing_deployment\n    )\n    mock_k8s.domain = \"127.0.0.1.nip.io\"\n    mock_k8s.enable_ingress = True\n\n    update = DeploymentUpdate(git_ref=\"invalid\")\n\n    # Should succeed with warning, not raise exception\n    result = await k8s_client.update_deployment(\"test-deploy\", update)\n\n    # Verify the update was applied\n    assert result is not None\n\n\n@patch(\"llama_agents.control_plane.k8s_client.get_deployment\")\n@pytest.mark.asyncio\nasync def test_update_deployment_with_git_ref_success(\n    mock_get_deployment: MagicMock, mock_k8s: MagicMock\n) -> None:\n    \"\"\"Test successful update_deployment with git_ref\"\"\"\n    # Mock existing deployment\n    existing_deployment = create_deployment_mock(\n        name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n    )\n    mock_k8s.k8s_custom_objects.get_namespaced_custom_object.return_value = (\n        existing_deployment\n    )\n\n    # Mock final result\n    mock_get_deployment.return_value = DeploymentResponse(\n        id=\"test-deploy\",\n        display_name=\"test-deploy\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"feature-branch\",  # updated\n        deployment_file_path=\"deploy.yml\",\n        status=\"Running\",\n        has_personal_access_token=False,\n        secret_names=None,\n        apiserver_url=None,\n    )\n\n    update = DeploymentUpdate(git_ref=\"feature-branch\")\n    result = await k8s_client.update_deployment(\"test-deploy\", update)\n\n    # Verify the deployment was updated\n    mock_k8s.k8s_custom_objects.replace_namespaced_custom_object.assert_called_once()\n\n    assert result is not None\n    assert result.git_ref == \"feature-branch\"\n\n\n@pytest.mark.asyncio\nasync def test_get_latest_replicaset_for_deployment_not_found(\n    mock_k8s: MagicMock,\n) -> None:\n    mock_k8s.k8s_apps_v1.read_namespaced_deployment.side_effect = ApiException(\n        status=404\n    )\n    result = await k8s_client.get_latest_replicaset_for_deployment(\"nope\")\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_get_latest_replicaset_for_deployment_picks_highest_revision(\n    mock_k8s: MagicMock,\n) -> None:\n    # Mock deployment\n    dep = Mock()\n    dep.metadata = Mock(uid=\"dep-uid\")\n    mock_k8s.k8s_apps_v1.read_namespaced_deployment.return_value = dep\n\n    # Two ReplicaSets with revisions 1 and 2, both owned by dep-uid\n    rs1 = V1ReplicaSet(\n        metadata=V1ObjectMeta(\n            name=\"rs-1\",\n            uid=\"rs-uid-1\",\n            annotations={\"deployment.kubernetes.io/revision\": \"1\"},\n            owner_references=[\n                V1OwnerReference(\n                    api_version=\"apps/v1\", kind=\"Deployment\", uid=\"dep-uid\", name=\"d\"\n                )\n            ],\n        )\n    )\n    rs2 = V1ReplicaSet(\n        metadata=V1ObjectMeta(\n            name=\"rs-2\",\n            uid=\"rs-uid-2\",\n            annotations={\"deployment.kubernetes.io/revision\": \"2\"},\n            owner_references=[\n                V1OwnerReference(\n                    api_version=\"apps/v1\", kind=\"Deployment\", uid=\"dep-uid\", name=\"d\"\n                )\n            ],\n        )\n    )\n    rs_list = Mock(items=[rs1, rs2])\n    mock_k8s.k8s_apps_v1.list_namespaced_replica_set.return_value = rs_list\n\n    result = await k8s_client.get_latest_replicaset_for_deployment(\"dep\")\n    assert result is not None\n    assert result.metadata is not None\n    assert result.metadata.name == \"rs-2\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_replicaset_logs_follow(mock_k8s: MagicMock) -> None:\n    # Patch latest replicaset helper to provide a fixed RS uid\n    with patch(\n        \"llama_agents.control_plane.k8s_client.get_latest_replicaset_for_deployment\",\n        return_value=V1ReplicaSet(metadata=V1ObjectMeta(uid=\"rs-uid\")),\n    ):\n        # One pod owned by this RS with one container\n        pod = V1Pod(\n            metadata=V1ObjectMeta(\n                name=\"pod-1\",\n                owner_references=[\n                    V1OwnerReference(\n                        api_version=\"apps/v1\",\n                        kind=\"ReplicaSet\",\n                        uid=\"rs-uid\",\n                        name=\"rs\",\n                    )\n                ],\n            ),\n            spec=V1PodSpec(containers=[V1Container(name=\"app\")]),\n        )\n        mock_k8s.k8s_core_v1.list_namespaced_pod.return_value = Mock(items=[pod])\n\n        class FakeStreamResponse:\n            def stream(\n                self, amt: int | None = None, decode_content: bool = False\n            ) -> Iterator[bytes]:\n                yield b\"hello\\n\"\n                yield b\"world\\n\"\n\n        # Streamed content for follow=True path\n        mock_k8s.k8s_core_v1.read_namespaced_pod_log.return_value = FakeStreamResponse()\n\n        gen = k8s_client.stream_replicaset_logs(\"dep\")\n        # Pull a couple lines then stop\n        first = await anext(gen)\n        second = await anext(gen)\n\n        assert first == LogLine(pod=\"pod-1\", container=\"app\", text=\"hello\")\n        assert second == LogLine(pod=\"pod-1\", container=\"app\", text=\"world\")\n\n\n@pytest.mark.asyncio\nasync def test_stream_replicaset_logs_non_follow_completes_with_stop_event(\n    mock_k8s: MagicMock,\n) -> None:\n    with patch(\n        \"llama_agents.control_plane.k8s_client.get_latest_replicaset_for_deployment\",\n        return_value=V1ReplicaSet(metadata=V1ObjectMeta(uid=\"rs-uid\")),\n    ):\n        pod = V1Pod(\n            metadata=V1ObjectMeta(\n                name=\"pod-1\",\n                owner_references=[\n                    V1OwnerReference(\n                        api_version=\"apps/v1\",\n                        kind=\"ReplicaSet\",\n                        uid=\"rs-uid\",\n                        name=\"rs\",\n                    )\n                ],\n            ),\n            spec=V1PodSpec(containers=[V1Container(name=\"app\")]),\n        )\n        mock_k8s.k8s_core_v1.list_namespaced_pod.return_value = Mock(items=[pod])\n        mock_k8s.k8s_core_v1.read_namespaced_pod_log.return_value = \"hello\\nworld\\n\"\n\n        async def collect_logs() -> list[LogLine]:\n            return [\n                line\n                async for line in k8s_client.stream_replicaset_logs(\n                    \"dep\",\n                    stop_event=asyncio.Event(),\n                    follow=False,\n                )\n            ]\n\n        lines = await asyncio.wait_for(collect_logs(), timeout=1)\n\n        assert lines == [\n            LogLine(pod=\"pod-1\", container=\"app\", text=\"hello\"),\n            LogLine(pod=\"pod-1\", container=\"app\", text=\"world\"),\n        ]\n\n\n@pytest.mark.asyncio\nasync def test_get_replicaset_pods_for_deployment_filters_by_owner(\n    mock_k8s: MagicMock,\n) -> None:\n    # Latest RS uid\n    with patch(\n        \"llama_agents.control_plane.k8s_client.get_latest_replicaset_for_deployment\",\n        return_value=V1ReplicaSet(metadata=V1ObjectMeta(uid=\"rs-uid\")),\n    ):\n        # Two pods: one owned, one not\n        owned = V1Pod(\n            metadata=V1ObjectMeta(\n                name=\"p1\",\n                owner_references=[\n                    V1OwnerReference(\n                        api_version=\"apps/v1\",\n                        kind=\"ReplicaSet\",\n                        uid=\"rs-uid\",\n                        name=\"rs\",\n                    )\n                ],\n            ),\n            spec=V1PodSpec(containers=[V1Container(name=\"c\")]),\n        )\n        other = V1Pod(\n            metadata=V1ObjectMeta(name=\"p2\"),\n            spec=V1PodSpec(containers=[V1Container(name=\"c\")]),\n        )\n        mock_k8s.k8s_core_v1.list_namespaced_pod.return_value = Mock(\n            items=[owned, other]\n        )\n\n        pods = await get_replicaset_pods_for_deployment(\"dep\")\n        assert [p.metadata.name for p in pods if p.metadata is not None] == [\"p1\"]\n\n\n@pytest.mark.asyncio\nasync def test_stream_container_logs_single_pod_multi_lines(\n    mock_k8s: MagicMock,\n) -> None:\n    class FakeStreamResponse:\n        def stream(\n            self, amt: int | None = None, decode_content: bool = False\n        ) -> Iterator[bytes]:\n            yield b\"a\\n\"\n            yield b\"b\\n\"\n\n    mock_k8s.k8s_core_v1.read_namespaced_pod_log.return_value = FakeStreamResponse()\n\n    cancel, gen = await stream_container_logs(\"pod-1\", \"app\")\n    assert await anext(gen) == \"a\"\n    assert await anext(gen) == \"b\"\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_storage.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Unit tests for the S3ObjectStorage client-construction path.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom botocore import UNSIGNED\nfrom botocore.client import Config\nfrom llama_agents.control_plane.storage import S3ObjectStorage\n\n\ndef _make_async_cm() -> AsyncMock:\n    cm = AsyncMock()\n    cm.__aenter__.return_value = MagicMock()\n    cm.__aexit__.return_value = None\n    return cm\n\n\n@pytest.mark.asyncio\nasync def test_client_uses_unsigned_config_when_unsigned_is_true() -> None:\n    storage = S3ObjectStorage(\n        bucket=\"b\",\n        endpoint_url=\"https://s3proxy.example\",\n        region=\"us-east-1\",\n        access_key=\"leaked\",\n        secret_key=\"leaked\",\n        unsigned=True,\n    )\n    with patch.object(\n        storage._session, \"client\", return_value=_make_async_cm()\n    ) as mock_client:\n        async with storage._client():\n            pass\n\n    _, kwargs = mock_client.call_args\n    cfg = kwargs.get(\"config\")\n    assert isinstance(cfg, Config)\n    assert getattr(cfg, \"signature_version\") is UNSIGNED\n    assert \"aws_access_key_id\" not in kwargs\n    assert \"aws_secret_access_key\" not in kwargs\n\n\n@pytest.mark.asyncio\nasync def test_client_omits_config_and_passes_creds_when_unsigned_is_false() -> None:\n    storage = S3ObjectStorage(\n        bucket=\"b\",\n        endpoint_url=\"https://s3.example\",\n        region=\"us-east-1\",\n        access_key=\"ak\",\n        secret_key=\"sk\",\n        unsigned=False,\n    )\n    with patch.object(\n        storage._session, \"client\", return_value=_make_async_cm()\n    ) as mock_client:\n        async with storage._client():\n            pass\n\n    _, kwargs = mock_client.call_args\n    assert \"config\" not in kwargs\n    assert kwargs.get(\"aws_access_key_id\") == \"ak\"\n    assert kwargs.get(\"aws_secret_access_key\") == \"sk\"\n"
  },
  {
    "path": "packages/llama-agents-control-plane/tests/test_ui_build_path.py",
    "content": "import json\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\nfrom llama_agents.control_plane.git import git_service\nfrom llama_agents.control_plane.git._git_service import GitRepository\nfrom llama_agents.core.git.git_util import GitCloneResult\n\n\n@pytest.mark.asyncio\nasync def test_validate_git_application_ui_path_for_pyproject_at_examples_basic_ui(\n    tmp_path: Path,\n) -> None:\n    # Arrange: mock clone_repo to populate a temp repo layout with pyproject at examples/basic_ui and UI under examples/basic_ui/ui\n    async def _mock_clone_repo(\n        repository_url: str,\n        git_ref: str | None = None,\n        basic_auth: str | None = None,\n        dest_dir: Path | str | None = None,\n    ) -> GitCloneResult:\n        # Simulate repo_root\n        if dest_dir is None:\n            raise AssertionError(\"dest_dir must be provided for _mock_clone_repo\")\n        repo_root = Path(dest_dir)\n        # examples/basic_ui/pyproject.toml with tool.llamadeploy\n        config_dir = repo_root / \"examples\" / \"basic_ui\"\n        ui_dir = config_dir / \"ui\"\n        config_dir.mkdir(parents=True, exist_ok=True)\n        ui_dir.mkdir(parents=True, exist_ok=True)\n\n        # minimal pyproject.toml with name so config validates after adding a workflow\n        (config_dir / \"pyproject.toml\").write_text(\n            \"\"\"\n[project]\nname = \"basic-ui\"\n\n[tool.llamadeploy]\nname = \"basic-ui\"\nworkflows = { default = \"quick_start.workflow:app\" }\n[tool.llamadeploy.ui]\ndirectory = \"ui\"\n\"\"\".strip()\n        )\n\n        # minimal package.json with a build script so ui_build_output_path is detected\n        (ui_dir / \"package.json\").write_text(\n            json.dumps(\n                {\"name\": \"ui\", \"version\": \"0.0.0\", \"scripts\": {\"build\": \"vite build\"}}\n            )\n        )\n\n        return GitCloneResult(git_sha=\"deadbeef\", git_ref=git_ref or \"main\")\n\n    with (\n        patch(\n            \"llama_agents.control_plane.git._git_service.clone_repo\",\n            new=_mock_clone_repo,\n        ),\n        # Pretend repo access is fine to reach the config parsing logic\n        patch(\n            \"llama_agents.control_plane.git._git_service.GitService.obtain_basic_auth_token\",\n            return_value=(\n                None,\n                GitRepository(url=\"https://example.com/repo.git\", access_token=None),\n            ),\n        ),\n    ):\n        # Act\n        result = await git_service.validate_git_application(\n            repository_url=\"https://example.com/repo.git\",\n            git_ref=None,\n            deployment_file_path=\"examples/basic_ui\",\n            deployment_id=None,\n            pat=None,\n        )\n\n    # Assert\n    assert result.is_valid is True\n    # Expect static assets path relative to repo root\n    assert result.ui_build_output_path == Path(\"examples/basic_ui/ui/dist\")\n"
  },
  {
    "path": "packages/llama-agents-core/CHANGELOG.md",
    "content": "# llama-agents-core\n\n## 0.10.2\n\n### Patch Changes\n\n- c3fac21: Validate `appserver_version` as a public PEP 440 version\n\n## 0.10.1\n\n### Patch Changes\n\n- 463c79d: Add `follow=false` query param on `GET /deployments/{id}/logs` so clients can fetch currently-available logs and exit. The default stays `follow=true`; existing streaming consumers are unchanged.\n\n## 0.10.0\n\n### Minor Changes\n\n- 2280e04: Rename deployment field `llama_deploy_version` to `appserver_version`. The old name remains as a deprecated input/output alias so existing clients and servers keep working.\n\n## 0.9.0\n\n### Minor Changes\n\n- e8b8f47: feat: add support for organizations\n\n## 0.8.5\n\n### Patch Changes\n\n- 7ad3049: Reduce full clones from github for config, repo validation, and sha discovery. Reduce dependencies on system git, preferring dulwich\n\n## 0.8.4\n\n### Patch Changes\n\n- f27d98f: Fix bootstrap bug from SSRF protection applied at wrong boundary\n\n## 0.8.3\n\n### Patch Changes\n\n- 3f12660: Add SSRF protection to git URL validation, blocking private/internal IP addresses\n\n## 0.8.2\n\n### Patch Changes\n\n- 46f2675: security patches\n\n## 0.8.1\n\n### Patch Changes\n\n- 58e7942: Rename Docker image repos to per-component names (llama-agents-<component>) with plain version tags\n\n## 0.8.0\n\n### Minor Changes\n\n- e2f3abd: Rename deployment name to display_name, add optional explicit id on create\n\n## 0.7.0\n\n### Minor Changes\n\n- 7bb9a90: Add dulwich-based git serving for internal repos. Users can push code via llamactl push and build pods clone via the build API. Bare repos are stored as tarballs in S3\n\n## 0.6.2\n\n### Patch Changes\n\n- 508b5da: Fix deployment update, fix github user auth\n\n## 0.6.1\n\n### Patch Changes\n\n- 1b86f90: Fix python -m llama_deploy.\\* broken by missing get_code\n\n## 0.6.0\n\n### Minor Changes\n\n- 4ab011f: Rename packages from llama-deploy to llama-agents.\n\n## 0.5.0\n\n### Minor Changes\n\n- ac74af4: Run build separately as a 1x time process per deployment update. Build stored in s3. Allows for fast unsuspend, and better future support for replication\n"
  },
  {
    "path": "packages/llama-agents-core/README.md",
    "content": "# llama-agents-core\n\nCore models and schemas for LlamaAgents.\n\nFor an end-to-end introduction, see [Getting started with LlamaAgents](https://developers.llamaindex.ai/python/cloud/llamaagents/getting-started).\n"
  },
  {
    "path": "packages/llama-agents-core/package.json",
    "content": "{\n  \"name\": \"llama-agents-core\",\n  \"version\": \"0.10.2\",\n  \"private\": false\n}\n"
  },
  {
    "path": "packages/llama-agents-core/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.7.20,<0.8.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"pytest>=8.4.1\",\n  \"pytest-asyncio>=0.25.3\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"respx>=0.22.0\",\n  \"ruff>=0.12.9\",\n  \"ty>=0.0.15\"\n]\n\n[project]\nname = \"llama-agents-core\"\nversion = \"0.10.2\"\ndescription = \"Core models and schemas for LlamaDeploy\"\nreadme = \"README.md\"\nlicense = {text = \"MIT\"}\nrequires-python = \">=3.10, <4\"\ndependencies = [\n  \"dulwich[https]>=0.22.0\",\n  \"fastapi>=0.115.0\",\n  \"overrides>=7.7.0\",\n  \"packaging>=24.1\",\n  \"pydantic>=2.12.0\",\n  \"pyyaml>=6.0.2\",\n  \"truststore>=0.10.4\",\n  \"types-pyyaml>=6.0.12.20250822\",\n  \"tomli>=2.0.1; python_version < \\\"3.11\\\"\"\n]\n\n[project.optional-dependencies]\nserver = [\n  \"fastapi>=0.115.0\"\n]\nclient = [\n  \"httpx>=0.24.0,<1.0.0\"\n]\n\n[tool.uv.build-backend]\nmodule-name = [\"llama_agents.core\", \"llama_deploy.core\"]\nnamespace = true\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/__init__.py",
    "content": "from . import schema\n\n__all__ = [\"schema\"]\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/_alias.py",
    "content": "# Alias: llama_deploy.* -> llama_agents.*\n#\n# This module makes the entire `llama_agents` namespace available under\n# `llama_deploy`, including all sub-modules. It uses a custom meta-path\n# finder to lazily redirect any import of `llama_deploy.<sub>` to\n# `llama_agents.<sub>`.\n\nfrom __future__ import annotations\n\nimport importlib\nimport importlib.util\nimport sys\nfrom importlib.abc import Loader, MetaPathFinder\nfrom importlib.machinery import ModuleSpec\nfrom types import CodeType, ModuleType\nfrom typing import Any, Sequence\n\n_ALIAS_PREFIX = \"llama_deploy\"\n_REAL_PREFIX = \"llama_agents\"\n\n\nclass _AliasLoader(Loader):\n    \"\"\"Loader that returns an already-imported module from sys.modules.\"\"\"\n\n    def __init__(self, real_name: str) -> None:\n        self.real_name = real_name\n\n    def create_module(self, spec: ModuleSpec) -> ModuleType | None:\n        return importlib.import_module(self.real_name)\n\n    def exec_module(self, module: ModuleType) -> None:\n        # Module is already fully initialized by the real import.\n        pass\n\n    def get_code(self, fullname: str) -> CodeType | None:\n        # runpy requires get_code() when using `python -m`. Delegate to the\n        # real module's loader so that `python -m llama_deploy.x` works.\n        real_suffix = fullname[len(_ALIAS_PREFIX) :]\n        real_name = _REAL_PREFIX + real_suffix\n        real_spec = importlib.util.find_spec(real_name)\n        if real_spec and real_spec.loader:\n            get_code: Any = getattr(real_spec.loader, \"get_code\", None)\n            if get_code is not None:\n                return get_code(real_name)\n        return None\n\n\nclass _AliasFinder(MetaPathFinder):\n    \"\"\"Meta-path finder that redirects llama_deploy.* to llama_agents.*\"\"\"\n\n    def find_spec(\n        self,\n        fullname: str,\n        path: Sequence[str] | None = None,\n        target: ModuleType | None = None,\n    ) -> ModuleSpec | None:\n        # Only handle llama_deploy.* (not the bare \"llama_deploy\" itself)\n        if not fullname.startswith(_ALIAS_PREFIX + \".\"):\n            return None\n        suffix = fullname[len(_ALIAS_PREFIX) :]\n        real_name = _REAL_PREFIX + suffix\n        real_spec = importlib.util.find_spec(real_name)\n        if real_spec is None:\n            return None\n        spec = ModuleSpec(\n            fullname,\n            _AliasLoader(real_name),\n            origin=real_spec.origin,\n            is_package=real_spec.submodule_search_locations is not None,\n        )\n        spec.submodule_search_locations = real_spec.submodule_search_locations\n        return spec\n\n\ndef install_alias_finder() -> None:\n    \"\"\"Install the llama_deploy -> llama_agents alias finder (idempotent).\"\"\"\n    if not any(isinstance(f, _AliasFinder) for f in sys.meta_path):\n        sys.meta_path.append(_AliasFinder())\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/_compat.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport sys\nfrom collections.abc import Mapping\nfrom typing import Any, BinaryIO\n\nif sys.version_info >= (3, 11):\n    # Stdlib TOML parser (Python 3.11+)\n    import tomllib as _toml_backend\nelse:\n    # Lightweight TOML backport for Python 3.10\n    import tomli as _toml_backend\n\n\ndef get_logging_level_mapping() -> Mapping[str, int]:\n    \"\"\"Return a mapping of log level names to their numeric values.\"\"\"\n    if sys.version_info >= (3, 11):\n        mapping = logging.getLevelNamesMapping()\n        return {k: int(v) for k, v in mapping.items() if isinstance(v, int)}\n\n    return {\n        name: level\n        for name, level in logging._nameToLevel.items()\n        if isinstance(level, int)\n    }\n\n\ndef load_toml_file(file_obj: BinaryIO) -> dict[str, Any]:\n    \"\"\"Load TOML data from a binary file object in a version-agnostic way.\"\"\"\n    return _toml_backend.load(file_obj)\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/client/manage_client.py",
    "content": "from __future__ import annotations\n\nfrom contextlib import asynccontextmanager\nfrom typing import AsyncGenerator, AsyncIterator, Callable, List\n\nimport httpx\nfrom httpx._types import PrimitiveData\nfrom llama_agents.core.client.ssl_util import get_httpx_verify_param\nfrom llama_agents.core.schema import LogEvent\nfrom llama_agents.core.schema.backups import (\n    BackupListResponse,\n    BackupResponse,\n    RestoreRequest,\n    RestoreResponse,\n)\nfrom llama_agents.core.schema.deployments import (\n    DeploymentCreate,\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    DeploymentsListResponse,\n    DeploymentUpdate,\n    RollbackRequest,\n)\nfrom llama_agents.core.schema.git_validation import (\n    RepositoryValidationRequest,\n    RepositoryValidationResponse,\n)\nfrom llama_agents.core.schema.projects import (\n    OrganizationsListResponse,\n    OrgSummary,\n    ProjectsListResponse,\n    ProjectSummary,\n)\nfrom llama_agents.core.schema.public import VersionResponse\n\n\nclass BaseClient:\n    def __init__(\n        self, base_url: str, api_key: str | None = None, auth: httpx.Auth | None = None\n    ) -> None:\n        self.base_url = base_url.rstrip(\"/\")\n        self.api_key = api_key\n\n        headers: dict[str, str] = {}\n        if api_key:\n            headers[\"Authorization\"] = f\"Bearer {api_key}\"\n\n        verify = get_httpx_verify_param()\n        self.client = httpx.AsyncClient(\n            base_url=self.base_url,\n            headers=headers,\n            auth=auth,\n            verify=verify,\n        )\n        self.hookless_client = httpx.AsyncClient(\n            base_url=self.base_url, headers=headers, auth=auth, verify=verify\n        )\n\n    async def aclose(self) -> None:\n        await self.client.aclose()\n        await self.hookless_client.aclose()\n\n\nclass ControlPlaneClient(BaseClient):\n    \"\"\"Unscoped client for non-project endpoints.\"\"\"\n\n    @classmethod\n    @asynccontextmanager\n    async def ctx(\n        cls, base_url: str, api_key: str | None = None, auth: httpx.Auth | None = None\n    ) -> AsyncIterator[ControlPlaneClient]:\n        client = cls(base_url, api_key, auth)\n        try:\n            yield client\n        finally:\n            try:\n                await client.aclose()\n            except Exception:\n                pass\n\n    def __init__(\n        self, base_url: str, api_key: str | None = None, auth: httpx.Auth | None = None\n    ) -> None:\n        super().__init__(base_url, api_key, auth)\n\n    async def server_version(self) -> VersionResponse:\n        response = await self.client.get(\"/api/v1beta1/deployments-public/version\")\n        _raise_for_status(response)\n        return VersionResponse.model_validate(response.json())\n\n    async def create_backup(self) -> BackupResponse:\n        response = await self.client.post(\"/api/v1beta1/backups\")\n        _raise_for_status(response)\n        return BackupResponse.model_validate(response.json())\n\n    async def list_backups(self) -> BackupListResponse:\n        response = await self.client.get(\"/api/v1beta1/backups\")\n        _raise_for_status(response)\n        return BackupListResponse.model_validate(response.json())\n\n    async def get_backup(self, backup_id: str) -> BackupResponse:\n        response = await self.client.get(f\"/api/v1beta1/backups/{backup_id}\")\n        _raise_for_status(response)\n        return BackupResponse.model_validate(response.json())\n\n    async def delete_backup(self, backup_id: str) -> BackupResponse:\n        response = await self.client.delete(f\"/api/v1beta1/backups/{backup_id}\")\n        _raise_for_status(response)\n        return BackupResponse.model_validate(response.json())\n\n    async def restore_backup(\n        self, backup_id: str, request: RestoreRequest | None = None\n    ) -> RestoreResponse:\n        if request is None:\n            request = RestoreRequest(backup_id=backup_id)\n        response = await self.client.post(\n            \"/api/v1beta1/backups/restore\",\n            json=request.model_dump(),\n        )\n        _raise_for_status(response)\n        return RestoreResponse.model_validate(response.json())\n\n    async def list_organizations(self) -> List[OrgSummary]:\n        response = await self.client.get(\"/api/v1beta1/deployments/organizations\")\n        _raise_for_status(response)\n        orgs_response = OrganizationsListResponse.model_validate(response.json())\n        return list(orgs_response.organizations)\n\n    async def list_projects(self, org_id: str | None = None) -> List[ProjectSummary]:\n        params = {}\n        if org_id is not None:\n            params[\"org_id\"] = org_id\n        response = await self.client.get(\n            \"/api/v1beta1/deployments/list-projects\", params=params\n        )\n        _raise_for_status(response)\n        projects_response = ProjectsListResponse.model_validate(response.json())\n        return list(projects_response.projects)\n\n\ndef _raise_for_status(response: httpx.Response) -> None:\n    \"\"\"\n    Custom raise for status that adds response body information to the error message, but still uses the httpx\n    error classes\n    \"\"\"\n    try:\n        response.raise_for_status()\n    except httpx.HTTPStatusError as e:\n        body = _response_body_snippet(response, limit=250)\n        request_id = response.headers.get(\"x-request-id\") or response.headers.get(\n            \"x-correlation-id\"\n        )\n        rid = f\" [request id: {request_id}]\" if request_id else \"\"\n        body_part = f\" - {body}\" if body else \"\"\n        raise httpx.HTTPStatusError(\n            f\"HTTP {response.status_code} for url {response.url}{body_part}{rid}\",\n            request=e.request or response.request,\n            response=e.response or response,\n        )\n\n\ndef _response_body_snippet(response: httpx.Response, limit: int = 500) -> str:\n    try:\n        text = response.text\n        if not text:\n            # fallback attempt if body not read\n            try:\n                data = response.json()\n            except Exception:\n                data = None\n            if data is not None:\n                text = str(data)\n        text = (text or \"\").strip()\n        if len(text) > limit:\n            return text[: limit - 3] + \"...\"\n        return text\n    except Exception:\n        return \"\"\n\n\nclass ProjectClient(BaseClient):\n    \"\"\"Project-scoped client for deployment operations.\"\"\"\n\n    @classmethod\n    @asynccontextmanager\n    async def ctx(\n        cls,\n        base_url: str,\n        project_id: str,\n        api_key: str | None = None,\n        auth: httpx.Auth | None = None,\n    ) -> AsyncIterator[ProjectClient]:\n        client = cls(base_url, project_id, api_key, auth)\n        try:\n            yield client\n        finally:\n            try:\n                await client.aclose()\n            except Exception:\n                pass\n\n    def __init__(\n        self,\n        base_url: str,\n        project_id: str,\n        api_key: str | None = None,\n        auth: httpx.Auth | None = None,\n    ) -> None:\n        super().__init__(base_url, api_key, auth)\n        self.project_id = project_id\n\n    async def list_deployments(self) -> List[DeploymentResponse]:\n        response = await self.client.get(\n            \"/api/v1beta1/deployments\",\n            params={\"project_id\": self.project_id},\n        )\n        _raise_for_status(response)\n        deployments_response = DeploymentsListResponse.model_validate(response.json())\n        return [deployment for deployment in deployments_response.deployments]\n\n    async def get_deployment(\n        self, deployment_id: str, include_events: bool = False\n    ) -> DeploymentResponse:\n        response = await self.client.get(\n            f\"/api/v1beta1/deployments/{deployment_id}\",\n            params={\"project_id\": self.project_id, \"include_events\": include_events},\n        )\n        _raise_for_status(response)\n        return DeploymentResponse.model_validate(response.json())\n\n    async def create_deployment(\n        self, deployment_data: DeploymentCreate\n    ) -> DeploymentResponse:\n        response = await self.client.post(\n            \"/api/v1beta1/deployments\",\n            params={\"project_id\": self.project_id},\n            json=deployment_data.model_dump(exclude_none=True),\n        )\n        _raise_for_status(response)\n        return DeploymentResponse.model_validate(response.json())\n\n    async def delete_deployment(self, deployment_id: str) -> None:\n        response = await self.client.delete(\n            f\"/api/v1beta1/deployments/{deployment_id}\",\n            params={\"project_id\": self.project_id},\n        )\n        _raise_for_status(response)\n\n    async def update_deployment(\n        self,\n        deployment_id: str,\n        update_data: DeploymentUpdate,\n    ) -> DeploymentResponse:\n        response = await self.client.patch(\n            f\"/api/v1beta1/deployments/{deployment_id}\",\n            params={\"project_id\": self.project_id},\n            json=update_data.model_dump(),\n        )\n        _raise_for_status(response)\n        return DeploymentResponse.model_validate(response.json())\n\n    async def get_deployment_history(\n        self, deployment_id: str\n    ) -> DeploymentHistoryResponse:\n        response = await self.client.get(\n            f\"/api/v1beta1/deployments/{deployment_id}/history\",\n            params={\"project_id\": self.project_id},\n        )\n        _raise_for_status(response)\n        return DeploymentHistoryResponse.model_validate(response.json())\n\n    async def rollback_deployment(\n        self, deployment_id: str, git_sha: str\n    ) -> DeploymentResponse:\n        response = await self.client.post(\n            f\"/api/v1beta1/deployments/{deployment_id}/rollback\",\n            params={\"project_id\": self.project_id},\n            json=RollbackRequest(git_sha=git_sha).model_dump(),\n        )\n        _raise_for_status(response)\n        return DeploymentResponse.model_validate(response.json())\n\n    async def validate_repository(\n        self,\n        repo_url: str,\n        deployment_id: str | None = None,\n        pat: str | None = None,\n    ) -> RepositoryValidationResponse:\n        response = await self.client.post(\n            \"/api/v1beta1/deployments/validate-repository\",\n            params={\"project_id\": self.project_id},\n            json=RepositoryValidationRequest(\n                repository_url=repo_url,\n                deployment_id=deployment_id,\n                pat=pat,\n            ).model_dump(),\n        )\n        _raise_for_status(response)\n        return RepositoryValidationResponse.model_validate(response.json())\n\n    async def stream_deployment_logs(\n        self,\n        deployment_id: str,\n        *,\n        include_init_containers: bool = False,\n        since_seconds: int | None = None,\n        tail_lines: int | None = None,\n        follow: bool = True,\n    ) -> AsyncGenerator[LogEvent, None]:\n        \"\"\"Stream logs as LogEvent items from the control plane using SSE.\n\n        Yields `LogEvent` models until the stream ends. With ``follow=False``\n        the server returns currently-available logs and ends the stream\n        naturally — useful for \"fetch and exit\" callers.\n        \"\"\"\n        params_dict: dict[str, PrimitiveData] = {\n            \"project_id\": self.project_id,\n            \"include_init_containers\": include_init_containers,\n            \"follow\": follow,\n        }\n        if since_seconds is not None:\n            params_dict[\"since_seconds\"] = since_seconds\n        if tail_lines is not None:\n            params_dict[\"tail_lines\"] = tail_lines\n\n        url = f\"/api/v1beta1/deployments/{deployment_id}/logs\"\n        headers = {\"Accept\": \"text/event-stream\"}\n\n        async with self.hookless_client.stream(\n            \"GET\",\n            url,\n            params=httpx.QueryParams(params_dict),\n            headers=headers,\n            timeout=None,\n        ) as response:\n            _raise_for_status(response)\n\n            event_name: str | None = None\n            data_lines: list[str] = []\n            async for line in response.aiter_lines():\n                if line is None:\n                    continue\n                line = line.decode() if isinstance(line, (bytes, bytearray)) else line\n                if line.startswith(\"event:\"):\n                    event_name = line[len(\"event:\") :].strip()\n                elif line.startswith(\"data:\"):\n                    data_lines.append(line[len(\"data:\") :].lstrip())\n                elif line.strip() == \"\":\n                    if event_name == \"log\" and data_lines:\n                        data_str = \"\\n\".join(data_lines)\n                        try:\n                            yield LogEvent.model_validate_json(data_str)\n                        except Exception:\n                            pass\n                    event_name = None\n                    data_lines = []\n\n\nCloser = Callable[[], None]\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/client/ssl_util.py",
    "content": "\"\"\"Utility functions for SSL/TLS configuration with optional truststore support.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport ssl\nfrom typing import Any\n\nimport truststore\n\n\ndef get_ssl_context() -> ssl.SSLContext | bool:\n    \"\"\"Get SSL context for httpx clients.\n\n    Returns an SSL context using truststore if LLAMA_DEPLOY_USE_TRUSTSTORE is set,\n    otherwise returns True (default SSL verification).\n\n    Truststore allows Python to use the system certificate store, which is useful\n    for corporate environments with MITM proxies.\n    \"\"\"\n    if os.getenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"\").lower() in (\"1\", \"true\", \"yes\"):\n        return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)\n    return True\n\n\ndef get_httpx_verify_param() -> Any:\n    \"\"\"Get the verify parameter for httpx clients.\n\n    Returns an SSL context using truststore if configured, otherwise returns True.\n    This can be passed directly to httpx.Client/AsyncClient's verify parameter.\n    \"\"\"\n    return get_ssl_context()\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/config.py",
    "content": "DEFAULT_DEPLOYMENT_FILE_PATH = \".\"\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/deployment_config.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom pathlib import Path\nfrom typing import Any, TypeVar\n\nimport yaml\nfrom llama_agents.core._compat import load_toml_file\nfrom llama_agents.core.git.git_util import get_git_root, is_git_repo\nfrom llama_agents.core.path_util import validate_path_traversal\nfrom pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator\n\nDEFAULT_DEPLOYMENT_NAME = \"default\"\n\n\ndef read_deployment_config_from_git_root_or_cwd(\n    cwd: Path, config_path: Path\n) -> \"DeploymentConfig\":\n    \"\"\"\n    Read the deployment config from the git root or cwd.\n    \"\"\"\n    if is_git_repo():\n        git_root = get_git_root()\n        relative_cwd_path = cwd.relative_to(git_root)\n        return read_deployment_config(git_root, relative_cwd_path / config_path)\n    return read_deployment_config(cwd, config_path)\n\n\ndef read_deployment_config(source_root: Path, config_path: Path) -> \"DeploymentConfig\":\n    \"\"\"\n    Read the deployment config from the config directory.\n\n    - first checks for a llama_agents.toml (or llama_deploy.toml) in the config_path\n    - then checks for a tool.llamaagents (or tool.llamadeploy) config in the pyproject.toml\n    - then check for a legacy yaml config (if config_path is a file, uses that, otherwise uses the config_path/llama_agents.yaml or llama_deploy.yaml)\n    - based on what was resolved here, discovers the package.json, if any ui, and resolves its values from the llamaagents (or llamadeploy) key in the package.json\n\n    In all cases, the llama_agents/llamaagents variant takes precedence over the llama_deploy/llamadeploy variant.\n\n    Args:\n        source_root: path to the root of the source code. References should not exit this directory.\n        config_path: path to a deployment config file, or directory containing a deployment config file.\n\n    Returns:\n        DeploymentConfig: the deployment config\n    \"\"\"\n    config_file: Path | None = None\n    if (source_root / config_path).is_file():\n        config_file = Path(config_path.name)\n        if str(config_file) in {\n            \"llama_agents.toml\",\n            \"llama_deploy.toml\",\n            \"pyproject.toml\",\n        }:\n            config_file = None\n        config_path = config_path.parent\n    local_agents_toml = source_root / config_path / \"llama_agents.toml\"\n    local_deploy_toml = source_root / config_path / \"llama_deploy.toml\"\n    local_toml_path = (\n        local_agents_toml if local_agents_toml.exists() else local_deploy_toml\n    )\n    pyproject_path = source_root / config_path / \"pyproject.toml\"\n    toml_config: DeploymentConfig = DeploymentConfig()\n    # local TOML format\n    if local_toml_path.exists():\n        with open(local_toml_path, \"rb\") as toml_file:\n            toml_data = load_toml_file(toml_file)\n            if isinstance(toml_data, dict):\n                toml_config = DeploymentConfig.model_validate(toml_data)\n    # pyproject.toml format\n    elif pyproject_path.exists():\n        with open(pyproject_path, \"rb\") as pyproject_file:\n            pyproject = load_toml_file(pyproject_file)\n            tool = pyproject.get(\"tool\", {})\n            project_name: str | None = None\n            project_metadata = pyproject.get(\"project\", {})\n            if isinstance(project_metadata, dict):\n                name = project_metadata.get(\"name\")\n                if isinstance(name, str):\n                    project_name = name\n            if isinstance(tool, dict):\n                llama_deploy = tool.get(\"llamaagents\") or tool.get(\"llamadeploy\", {})\n                if isinstance(llama_deploy, dict):\n                    if \"name\" not in llama_deploy:\n                        llama_deploy[\"name\"] = project_name\n                    toml_config = DeploymentConfig.model_validate(llama_deploy)\n    # legacy yaml format, (and why not support yaml in the new format too, since this is doing everything all the ways)\n    if toml_config.has_no_workflows():\n        agents_yaml = (\n            source_root / config_path / (config_file or Path(\"llama_agents.yaml\"))\n        )\n        deploy_yaml = (\n            source_root / config_path / (config_file or Path(\"llama_deploy.yaml\"))\n        )\n        yaml_path = agents_yaml if agents_yaml.exists() else deploy_yaml\n        if yaml_path.exists():\n            with open(yaml_path, \"r\", encoding=\"utf-8\") as yaml_file:\n                yaml_loaded = yaml.safe_load(yaml_file) or {}\n\n            old_config: DeploymentConfig | None = None\n            new_config: DeploymentConfig | None = None\n            try:\n                old_config = DeprecatedDeploymentConfig.model_validate(\n                    yaml_loaded\n                ).to_deployment_config()\n            except ValidationError:\n                pass\n            try:\n                new_config = DeploymentConfig.model_validate(yaml_loaded)\n            except ValidationError:\n                pass\n            loaded: DeploymentConfig | None = new_config\n            if (\n                old_config is not None\n                and old_config.is_valid()\n                and (new_config is None or not new_config.is_valid())\n            ):\n                loaded = old_config\n            if loaded is not None:\n                toml_config = toml_config.merge_config(loaded)\n\n    # package.json format\n    if toml_config.ui is not None:\n        package_json_path = (\n            source_root / config_path / toml_config.ui.directory / \"package.json\"\n        )\n        if package_json_path.exists():\n            with open(package_json_path, \"r\", encoding=\"utf-8\") as package_json_file:\n                package_json = json.load(package_json_file)\n            if isinstance(package_json, dict):\n                # Standard packageManager fallback, e.g. \"pnpm@9.0.0\" -> \"pnpm\"\n                pkg_manager_value = package_json.get(\"packageManager\")\n                pkg_manager_name: str | None = None\n                if isinstance(pkg_manager_value, str) and pkg_manager_value:\n                    pkg_manager_name = pkg_manager_value.split(\"@\", 1)[0] or None\n\n                llama_deploy = package_json.get(\"llamaagents\") or package_json.get(\n                    \"llamadeploy\", {}\n                )\n\n                if isinstance(llama_deploy, dict):\n                    # Prepare payload without leaking Path objects into Pydantic\n                    ui_dir = toml_config.ui.directory if toml_config.ui else None\n                    ui_payload: dict[str, object] = {**llama_deploy}\n                    if \"directory\" not in ui_payload and ui_dir is not None:\n                        ui_payload[\"directory\"] = ui_dir\n                    if (\n                        \"package_manager\" not in ui_payload\n                        and pkg_manager_name is not None\n                    ):\n                        ui_payload[\"package_manager\"] = pkg_manager_name\n\n                    ui_config = UIConfig.model_validate(ui_payload)\n                    if ui_config.build_output_dir is not None:\n                        ui_config.build_output_dir = str(\n                            Path(toml_config.ui.directory) / ui_config.build_output_dir\n                        )\n                    toml_config.ui = ui_config.merge_config(toml_config.ui)\n\n    if toml_config.ui is not None:\n        validate_path_traversal(\n            config_path / toml_config.ui.directory, source_root, \"ui_source\"\n        )\n        if toml_config.ui.build_output_dir:\n            validate_path_traversal(\n                config_path / toml_config.ui.build_output_dir,\n                source_root,\n                \"ui_build_output_dir\",\n            )\n\n    return toml_config\n\n\ndef resolve_config_parent(root: Path, deployment_path: Path) -> Path:\n    path = root / deployment_path\n    if path.is_file():\n        return path.parent\n    else:\n        return path\n\n\nDEFAULT_UI_PACKAGE_MANAGER = \"npm\"\nDEFAULT_UI_BUILD_COMMAND = \"build\"\nDEFAULT_UI_SERVE_COMMAND = \"dev\"\nDEFAULT_UI_PROXY_PORT = 4502\n\n\nclass DeploymentConfig(BaseModel):\n    name: str = Field(\n        default=DEFAULT_DEPLOYMENT_NAME,\n        description=\"The url safe path name of the deployment.\",\n    )\n    llama_cloud: bool = Field(\n        default=False,\n        description=\"If true, serving locally expects Llama Cloud access and will inject credentials when possible.\",\n    )\n    app: str | None = Field(\n        default=None,\n        description=\"A full bundle of all workflows as an 'app'. \\\"path.to_import:app_name\\\"\",\n    )\n    workflows: dict[str, str] = Field(\n        default_factory=dict,\n        description='Deprecated: A map of workflow names to their import paths. \"nice_name\": \"path.to_import:workflow_name\"',\n    )\n    env_files: list[str] = Field(\n        default_factory=list,\n        description=\"The environment files to load. Defaults to ['.env']\",\n    )\n    env: dict[str, str] = Field(\n        default_factory=dict,\n        description=\"Arbitrary environment variables to set. Defaults to {}\",\n    )\n    required_env_vars: list[str] = Field(\n        default_factory=list,\n        description=(\n            \"A list of environment variable names that must be defined at runtime. \"\n            \"If any are missing or empty, the app server will fail fast with an informative error.\"\n        ),\n    )\n    ui: UIConfig | None = Field(\n        default=None,\n        description=\"The UI configuration.\",\n    )\n\n    def merge_config(self, config: \"DeploymentConfig\") -> \"DeploymentConfig\":\n        \"\"\"Merge the config with another config.\"\"\"\n\n        return DeploymentConfig(\n            name=_pick_non_default(self.name, config.name, \"default\"),\n            llama_cloud=self.llama_cloud or config.llama_cloud,\n            app=self.app or config.app,\n            workflows={**self.workflows, **config.workflows},\n            env_files=list(set(self.env_files + config.env_files)),\n            env={**self.env, **config.env},\n            required_env_vars=list(\n                {\n                    *[v for v in self.required_env_vars],\n                    *[v for v in config.required_env_vars],\n                }\n            ),\n            ui=self.ui.merge_config(config.ui)\n            if config.ui is not None and self.ui is not None\n            else self.ui or config.ui,\n        )\n\n    def has_no_workflows(self) -> bool:\n        \"\"\"Check if the config has no workflows.\"\"\"\n        return len(self.workflows) == 0 and self.app is None\n\n    def has_both_app_and_workflows(self) -> bool:\n        \"\"\"Check if the config has both app and workflows.\"\"\"\n        return self.app is not None and len(self.workflows) > 0\n\n    def is_valid(self) -> bool:\n        \"\"\"Check if the config is valid.\"\"\"\n        try:\n            self.validate_config()\n            return True\n        except ValueError:\n            return False\n\n    def validate_config(self) -> None:\n        \"\"\"Validate the config.\"\"\"\n        if self.has_no_workflows():\n            raise ValueError(\"Config must have at least one workflow.\")\n        if self.has_both_app_and_workflows():\n            raise ValueError(\"Config cannot have both app and workflows configured.\")\n\n    def build_output_path(self) -> Path | None:\n        \"\"\"get the build output path, or default to the ui directory/dist\"\"\"\n        if self.ui is None:\n            return None\n        return (\n            Path(self.ui.build_output_dir)\n            if self.ui.build_output_dir\n            else Path(self.ui.directory) / \"dist\"\n        )\n\n\nT = TypeVar(\"T\")\n\n\ndef _pick_non_default(a: T, b: T, default: T) -> T:\n    if a != default:\n        return a\n    return b or default\n\n\nclass UIConfig(BaseModel):\n    directory: str = Field(\n        ...,\n        description=\"The directory containing the UI, relative to the pyproject.toml directory\",\n    )\n    build_output_dir: str | None = Field(\n        default=None,\n        description=\"The directory containing the built UI, relative to the pyproject.toml directory. Defaults to 'dist' relative to the ui_directory, if defined\",\n    )\n    package_manager: str = Field(\n        default=DEFAULT_UI_PACKAGE_MANAGER,\n        description=f\"The package manager to use to build the UI. Defaults to '{DEFAULT_UI_PACKAGE_MANAGER}'\",\n    )\n    build_command: str = Field(\n        default=DEFAULT_UI_BUILD_COMMAND,\n        description=f\"The npm script command to build the UI. Defaults to '{DEFAULT_UI_BUILD_COMMAND}' if not specified\",\n    )\n    serve_command: str = Field(\n        default=DEFAULT_UI_SERVE_COMMAND,\n        description=f\"The command to serve the UI. Defaults to '{DEFAULT_UI_SERVE_COMMAND}' if not specified\",\n    )\n    proxy_port: int = Field(\n        default=DEFAULT_UI_PROXY_PORT,\n        description=f\"The port to proxy the UI to. Defaults to '{DEFAULT_UI_PROXY_PORT}' if not specified\",\n    )\n\n    def merge_config(self, config: \"UIConfig\") -> \"UIConfig\":\n        \"\"\"Merge the config with the default config.\"\"\"\n\n        return UIConfig(\n            directory=self.directory,\n            build_output_dir=self.build_output_dir or config.build_output_dir,\n            package_manager=_pick_non_default(\n                self.package_manager, config.package_manager, DEFAULT_UI_PACKAGE_MANAGER\n            ),\n            build_command=_pick_non_default(\n                self.build_command, config.build_command, DEFAULT_UI_BUILD_COMMAND\n            ),\n            serve_command=_pick_non_default(\n                self.serve_command, config.serve_command, DEFAULT_UI_SERVE_COMMAND\n            ),\n            proxy_port=_pick_non_default(\n                self.proxy_port, config.proxy_port, DEFAULT_UI_PROXY_PORT\n            ),\n        )\n\n\nclass ServiceSourceV0(BaseModel):\n    \"\"\"Configuration for where to load the workflow or other source. Path is relative to the config file its declared within.\"\"\"\n\n    location: str\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def validate_fields(cls, data: Any) -> Any:\n        if isinstance(data, dict):\n            if \"name\" in data:\n                data[\"location\"] = data.pop(\"name\")\n        return data\n\n\nclass DerecatedService(BaseModel):\n    \"\"\"Configuration for a single service.\"\"\"\n\n    source: ServiceSourceV0 | None = Field(default=None)\n    import_path: str | None = Field(default=None)\n    env: dict[str, str] | None = Field(default=None)\n    env_files: list[str] | None = Field(default=None)\n    python_dependencies: list[str] | None = Field(default=None)\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def validate_fields(cls, data: Any) -> Any:\n        if isinstance(data, dict):\n            # Handle YAML aliases\n            if \"path\" in data:\n                data[\"import_path\"] = data.pop(\"path\")\n            if \"import-path\" in data:\n                data[\"import_path\"] = data.pop(\"import-path\")\n            if \"env-files\" in data:\n                data[\"env_files\"] = data.pop(\"env-files\")\n\n        return data\n\n    def module_location(self) -> tuple[str, str]:\n        \"\"\"\n        Parses the import path, and target, discarding legacy file path portion, if any\n\n        \"src/module.workflow:my_workflow\" -> (\"module.workflow\", \"my_workflow\")\n        \"\"\"\n        if self.import_path is None:\n            raise ValueError(\"import_path is required to compute module_location\")\n        module_name, workflow_name = self.import_path.split(\":\")\n        return Path(module_name).name, workflow_name\n\n\nclass DeprecatedDeploymentConfig(BaseModel):\n    \"\"\"Model definition mapping a deployment config file.\"\"\"\n\n    model_config = ConfigDict(populate_by_name=True, extra=\"ignore\")\n\n    name: str\n    default_service: str | None = Field(default=None)\n    services: dict[str, DerecatedService]\n    ui: DerecatedService | None = None\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def validate_fields(cls, data: Any) -> Any:\n        # Handle YAML aliases\n        if isinstance(data, dict):\n            if \"default-service\" in data:\n                data[\"default_service\"] = data.pop(\"default-service\")\n\n        return data\n\n    @classmethod\n    def from_yaml(\n        cls,\n        path: Path,\n    ) -> \"DeprecatedDeploymentConfig\":\n        \"\"\"Read config data from a yaml file.\"\"\"\n        with open(path, \"r\", encoding=\"utf-8\") as yaml_file:\n            config = yaml.safe_load(yaml_file) or {}\n\n        instance = cls.model_validate(config)\n        return instance\n\n    def to_deployment_config(self) -> DeploymentConfig:\n        \"\"\"Convert the deployment config to a DeploymentConfig.\"\"\"\n        workflows = {}\n        env_files = []\n        env = {}\n        ui_directory: str | None = None\n        for service_name, service in self.services.items():\n            if service.import_path:\n                path, name = service.module_location()\n                workflows[service_name] = f\"{path}:{name}\"\n            if service.env_files:\n                env_files.extend(service.env_files)\n            if service.env:\n                env.update(service.env)\n        if self.default_service:\n            workflows[\"default\"] = workflows[self.default_service]\n        env_files = list(set(env_files))\n\n        if self.ui:\n            ui_directory = self.ui.source.location if self.ui.source else None\n\n        return DeploymentConfig(\n            name=self.name,\n            workflows=workflows,\n            env_files=env_files,\n            env=env,\n            ui=UIConfig(directory=ui_directory) if ui_directory else None,\n        )\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/git/git_util.py",
    "content": "\"\"\"\nGit utilities for exploring, cloning, and parsing llama-deploy repositories.\n\nBacked by the pure-Python ``dulwich`` library so the host does not need a\n``git`` binary on PATH.\n\"\"\"\n\nimport asyncio\nimport contextlib\nimport io\nimport ipaddress\nimport re\nimport shutil\nimport socket\nimport tempfile\nimport urllib.parse\nfrom collections.abc import Iterator\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nimport yaml\nfrom dulwich import porcelain\nfrom dulwich.client import get_transport_and_path\nfrom dulwich.errors import NotGitRepository\nfrom dulwich.objects import ObjectID\nfrom dulwich.refs import Ref\nfrom dulwich.repo import Repo\nfrom dulwich.walk import Walker\n\n_HEAD_REF = Ref(b\"HEAD\")\n_REFS_HEADS = b\"refs/heads/\"\n_REFS_TAGS = b\"refs/tags/\"\n_REFS_REMOTES = b\"refs/remotes/\"\n_DETACHED_BRANCH_REF = Ref(b\"refs/heads/_llama_checkout\")\n\n\n@contextlib.contextmanager\ndef _discover_repo() -> Iterator[Repo]:\n    \"\"\"Discover the repo at cwd, yield it, and guarantee close.\"\"\"\n    try:\n        repo = Repo.discover(start=str(Path.cwd()))\n    except NotGitRepository as e:\n        raise GitAccessError(\"Not a git repository\") from e\n    try:\n        yield repo\n    finally:\n        repo.close()\n\n\ndef parse_github_repo_url(repo_url: str) -> tuple[str, str]:\n    \"\"\"\n    Parse GitHub repository URL to extract owner and repo name.\n\n    Args:\n        repo_url: GitHub repository URL (various formats supported)\n\n    Returns:\n        Tuple of (owner, repo_name)\n\n    Raises:\n        ValueError: If URL format is not recognized\n    \"\"\"\n    url = repo_url.rstrip(\"/\").removesuffix(\".git\")\n\n    patterns = [\n        r\"https://github\\.com/([^/]+)/([^/]+)\",\n        r\"git@github\\.com:([^/]+)/([^/]+)\",\n        r\"github\\.com/([^/]+)/([^/]+)\",\n    ]\n\n    for pattern in patterns:\n        match = re.match(pattern, url)\n        if match:\n            return match.group(1), match.group(2)\n\n    raise ValueError(f\"Could not parse GitHub repository URL: {repo_url}\")\n\n\nclass GitAccessError(Exception):\n    \"\"\"Error raised when a user reportable git error occurs, e.g connection fails, cannot access repository, timeout, ref not found, etc.\"\"\"\n\n    def __init__(self, message: str):\n        self.message = message\n        super().__init__(message)\n\n\n_ALLOWED_SCHEMES = {\"https\", \"http\"}\n\nFULL_SHA_RE = re.compile(r\"^[0-9a-f]{40}$\")\nSHA_LIKE_RE = re.compile(r\"^[0-9a-f]{7,40}$\")\n\n\ndef validate_git_url(url: str) -> None:\n    \"\"\"Validate that a git URL uses an allowed scheme.\n\n    Raises GitAccessError for dangerous or unrecognised URL schemes such as\n    ``ext::`` (which lets git invoke arbitrary commands) or URLs that start\n    with ``-`` (which could be interpreted as flags).\n\n    Note: this does NOT check whether the hostname resolves to a private\n    network address.  Callers that accept user-supplied URLs should also\n    call :func:`validate_git_url_no_ssrf` to guard against SSRF.\n    \"\"\"\n    if url.startswith(\"-\"):\n        raise GitAccessError(f\"Invalid git URL (starts with '-'): {url}\")\n\n    if url.startswith(\"ext::\"):\n        raise GitAccessError(f\"Disallowed git URL scheme 'ext': {url}\")\n\n    parsed = urllib.parse.urlparse(url)\n    scheme = parsed.scheme.lower()\n\n    if scheme not in _ALLOWED_SCHEMES:\n        raise GitAccessError(\n            f\"Disallowed git URL scheme '{scheme}': {url}. \"\n            f\"Allowed schemes: {', '.join(sorted(_ALLOWED_SCHEMES))}\"\n        )\n\n\ndef validate_git_url_no_ssrf(url: str) -> None:\n    \"\"\"Validate git URL scheme AND reject hostnames that resolve to private IPs.\n\n    Use this at API boundaries where the URL comes from user input.\n    \"\"\"\n    validate_git_url(url)\n\n    parsed = urllib.parse.urlparse(url)\n    hostname = parsed.hostname\n    if not hostname:\n        raise GitAccessError(f\"Invalid git URL (no hostname): {url}\")\n\n    _check_hostname_not_private(hostname)\n\n\ndef _check_hostname_not_private(hostname: str) -> None:\n    \"\"\"Resolve hostname and raise GitAccessError if it points to a private/internal IP.\"\"\"\n    try:\n        addr_infos = socket.getaddrinfo(hostname, None)\n    except socket.gaierror as e:\n        raise GitAccessError(f\"DNS resolution failed for {hostname}\") from e\n\n    for addr_info in addr_infos:\n        ip = ipaddress.ip_address(addr_info[4][0])\n        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:\n            raise GitAccessError(\n                f\"Git URL resolves to a private network address: {hostname}\"\n            )\n\n\n@dataclass\nclass GitCloneResult:\n    git_sha: str\n    git_ref: str | None = None\n\n\ndef _split_basic_auth(basic_auth: str | None) -> tuple[str | None, str | None]:\n    \"\"\"Parse a ``user:password`` style string into separate components.\"\"\"\n    if not basic_auth:\n        return None, None\n    if \":\" not in basic_auth:\n        # Treat as a token-only credential (passed in the user slot)\n        return basic_auth, None\n    user, _, password = basic_auth.partition(\":\")\n    return user, password\n\n\ndef _resolved_git_ref_for_head(repo: Repo) -> str | None:\n    \"\"\"Read the symbolic HEAD ref and return a friendly branch/tag name (or None).\"\"\"\n    head_ref = repo.refs.read_ref(_HEAD_REF)\n    if head_ref is None:\n        return None\n    if head_ref.startswith(b\"ref: \"):\n        head_ref = head_ref[5:]\n    # If the read produced an actual SHA (40 hex), HEAD is detached.\n    if FULL_SHA_RE.match(head_ref.decode(errors=\"replace\")):\n        # Try to find a tag pointing at HEAD\n        head_sha = repo.refs[_HEAD_REF]\n        for ref_name in repo.refs.subkeys(Ref(_REFS_TAGS)):\n            tag_ref = Ref(_REFS_TAGS + ref_name)\n            try:\n                if repo.refs[tag_ref] == head_sha:\n                    return ref_name.decode()\n            except KeyError:\n                continue\n        return None\n    if head_ref.startswith(_REFS_HEADS):\n        return head_ref[len(_REFS_HEADS) :].decode()\n    if head_ref.startswith(_REFS_TAGS):\n        return head_ref[len(_REFS_TAGS) :].decode()\n    return head_ref.decode()\n\n\ndef resolve_ref_in_repo(repo: Repo, git_ref: str) -> bytes | None:\n    \"\"\"Resolve a git ref string to a commit SHA in a dulwich Repo.\n\n    Tries named refs (bare path, heads, tags), full SHA, then short SHA\n    prefix.  For named refs, annotated tags are peeled to the underlying\n    commit.\n\n    Returns the SHA as bytes, or ``None`` if the ref cannot be found.\n    Raises :class:`GitAccessError` when a short SHA prefix is ambiguous.\n    \"\"\"\n    ref_bytes = git_ref.encode()\n\n    for candidate in (\n        Ref(ref_bytes),\n        Ref(_REFS_HEADS + ref_bytes),\n        Ref(_REFS_TAGS + ref_bytes),\n    ):\n        try:\n            return repo.get_peeled(candidate)\n        except KeyError:\n            continue\n\n    if FULL_SHA_RE.match(git_ref):\n        try:\n            return repo[ref_bytes].id\n        except (AssertionError, KeyError):\n            pass\n\n    if SHA_LIKE_RE.match(git_ref):\n        try:\n            matching_shas = repo.object_store.iter_prefix(ref_bytes)\n            first = next(matching_shas)\n            if next(matching_shas, None) is not None:\n                raise GitAccessError(f\"Git ref is ambiguous: {git_ref}\")\n            return first\n        except StopIteration:\n            pass\n        except GitAccessError:\n            raise\n        except (KeyError, OSError):\n            pass\n\n    return None\n\n\ndef _checkout_ref(repo: Repo, git_ref: str) -> None:\n    \"\"\"Update HEAD to point at the requested ref.\n\n    Raises GitAccessError if the ref cannot be resolved or if a short SHA-like\n    ref is ambiguous.\n    \"\"\"\n    target_sha = resolve_ref_in_repo(repo, git_ref)\n\n    # Also try remote tracking refs (only present in cloned repos)\n    if target_sha is None:\n        ref_bytes = git_ref.encode()\n        try:\n            target_sha = repo.get_peeled(Ref(_REFS_REMOTES + b\"origin/\" + ref_bytes))\n        except KeyError:\n            pass\n\n    if target_sha is None:\n        raise GitAccessError(f\"Git ref not found: {git_ref}\")\n\n    repo.refs.set_symbolic_ref(_HEAD_REF, _DETACHED_BRANCH_REF)\n    repo.refs[_DETACHED_BRANCH_REF] = ObjectID(target_sha)\n    porcelain.reset(repo, \"hard\", target_sha.decode())\n\n\ndef clone_repo_sync(\n    repository_url: str,\n    git_ref: str | None = None,\n    basic_auth: str | None = None,\n    dest_dir: Path | str | None = None,\n    depth: int | None = None,\n    git_sha: str | None = None,\n) -> GitCloneResult:\n    \"\"\"\n    Clone a repository and checkout a specific ref, if provided. If user reportable access errors occur, raises a GitAccessError.\n\n    Args:\n        repository_url: The URL of the repository to clone\n        git_ref: The git reference to checkout, if provided. May be a branch\n            name, tag, full 40-character commit SHA, or a short SHA-like\n            prefix. SHA-like refs are resolved after clone so they do not get\n            misclassified as branch names.\n        git_sha: Optional pinned commit SHA to check out explicitly. When set,\n            this takes precedence over ``git_ref`` for checkout behavior and\n            depth selection.\n        basic_auth: The basic auth to use to clone the repository, in\n            ``user:password`` form. Token-only credentials are also accepted\n            (passed via the URL user component).\n        dest_dir: The directory to clone the repository to, if provided. The\n            directory must not already contain a checkout — partial state is\n            not handled. When omitted, a temporary directory is used and\n            cleaned up after the function returns.\n        depth: Optional shallow clone depth. ``depth=1`` requests only the\n            most recent commit on the cloned ref. Defaults to a full clone\n            for backwards compatibility.\n\n    Returns:\n        GitCloneResult: A dataclass containing the git SHA and resolved git ref (e.g. main if None was provided)\n    \"\"\"\n    validate_git_url(repository_url)\n    user, password = _split_basic_auth(basic_auth)\n\n    cleanup_temp: Path | None = None\n    if dest_dir is None:\n        cleanup_temp = Path(tempfile.mkdtemp(prefix=\"llama_clone_\"))\n        target_path = cleanup_temp\n    else:\n        target_path = Path(dest_dir)\n        target_path.mkdir(parents=True, exist_ok=True)\n\n    explicit_git_sha = bool(git_sha)\n    is_sha_ref = bool(git_ref and SHA_LIKE_RE.match(git_ref))\n    branch_arg: bytes | None = None\n    if not explicit_git_sha and git_ref and not is_sha_ref:\n        branch_arg = git_ref.encode()\n\n    # When the caller pinned a specific SHA, depth=1 of the default branch\n    # may not contain the requested commit. dulwich does not have a clean\n    # \"shallow fetch this SHA\" path, so fall back to a full clone in that\n    # case to guarantee the SHA is reachable.\n    effective_depth = depth\n    if explicit_git_sha or is_sha_ref:\n        effective_depth = None\n\n    transport_kwargs: dict[str, Any] = {}\n    if user is not None:\n        transport_kwargs[\"username\"] = user\n    if password is not None:\n        transport_kwargs[\"password\"] = password\n\n    try:\n        try:\n            repo = porcelain.clone(\n                source=repository_url,\n                target=str(target_path),\n                depth=effective_depth,\n                branch=branch_arg,\n                checkout=True,\n                errstream=io.BytesIO(),\n                **transport_kwargs,\n            )\n        except Exception as e:\n            # Dulwich surfaces network/protocol failures as a mix of\n            # HangupException, NotGitRepository, OSError, and assorted\n            # GitProtocolError subclasses — normalize them all here.\n            raise GitAccessError(\n                f\"Failed to clone repository {repository_url}: {e}\"\n            ) from e\n\n        try:\n            resolved_ref: str | None = git_ref\n\n            if git_sha is not None:\n                # Dulwich cannot fetch a specific SHA at clone time, so check\n                # out the requested commit after the clone.\n                _checkout_ref(repo, git_sha)\n                head_sha_bytes = repo.head()\n            else:\n                try:\n                    head_sha_bytes = repo.head()\n                except KeyError as e:\n                    raise GitAccessError(\n                        f\"Cloned repository {repository_url} has no HEAD\"\n                    ) from e\n\n            if git_sha is None and git_ref and is_sha_ref:\n                # Preserve the historical compatibility path for SHA-shaped\n                # git_ref values that callers still pass through the generic API.\n                _checkout_ref(repo, git_ref)\n                head_sha_bytes = repo.head()\n                resolved_ref = git_ref\n            elif git_ref is None:\n                resolved_ref = _resolved_git_ref_for_head(repo)\n\n            return GitCloneResult(git_sha=head_sha_bytes.decode(), git_ref=resolved_ref)\n        finally:\n            repo.close()\n    finally:\n        if cleanup_temp is not None:\n            shutil.rmtree(cleanup_temp, ignore_errors=True)\n\n\nasync def clone_repo(\n    repository_url: str,\n    git_ref: str | None = None,\n    basic_auth: str | None = None,\n    dest_dir: Path | str | None = None,\n    depth: int | None = None,\n    git_sha: str | None = None,\n) -> GitCloneResult:\n    \"\"\"Clone a repository without blocking the event loop.\"\"\"\n    return await asyncio.to_thread(\n        clone_repo_sync,\n        repository_url,\n        git_ref,\n        basic_auth,\n        dest_dir,\n        depth,\n        git_sha,\n    )\n\n\ndef validate_deployment_file(repo_dir: Path, deployment_file_path: str) -> bool:\n    \"\"\"\n    Validate that the deployment file exists in the repository.\n\n    Args:\n        repo_dir: The directory of the repository\n        deployment_file_path: The path to the deployment file, relative to the repository root\n\n    Returns:\n        True if the deployment file exists and appears to be valid, False otherwise\n    \"\"\"\n    deployment_file = repo_dir / deployment_file_path\n    if not deployment_file.exists():\n        return False\n    with open(deployment_file, \"r\") as f:\n        try:\n            loaded = yaml.safe_load(f)\n            if not isinstance(loaded, dict):\n                return False\n            if \"name\" not in loaded:\n                return False\n            if not isinstance(loaded[\"name\"], str):\n                return False\n            if \"services\" not in loaded:\n                return False\n            if not isinstance(loaded[\"services\"], dict):\n                return False\n            return True  # good nuff for now. Eventually this should parse it into a model validated format\n        except yaml.YAMLError:\n            return False\n\n\ndef _probe_remote(\n    repository_url: str, user: str | None = None, password: str | None = None\n) -> bool:\n    \"\"\"Run an ls-remote-equivalent against ``repository_url``.\"\"\"\n    transport_kwargs: dict[str, Any] = {}\n    if user is not None:\n        transport_kwargs[\"username\"] = user\n    if password is not None:\n        transport_kwargs[\"password\"] = password\n    try:\n        client, path = get_transport_and_path(repository_url, **transport_kwargs)\n    except Exception:\n        return False\n    try:\n        client.get_refs(path.encode() if isinstance(path, str) else path)\n        return True\n    except Exception:\n        return False\n\n\ndef validate_git_public_access_sync(repository_url: str) -> bool:\n    \"\"\"Check if a git repository is publicly accessible without authentication.\"\"\"\n    validate_git_url_no_ssrf(repository_url)\n    return _probe_remote(repository_url)\n\n\nasync def validate_git_public_access(repository_url: str) -> bool:\n    \"\"\"Check if a git repository is publicly accessible without blocking.\"\"\"\n    return await asyncio.to_thread(validate_git_public_access_sync, repository_url)\n\n\ndef validate_git_credential_access_sync(repository_url: str, basic_auth: str) -> bool:\n    \"\"\"Check if a credential provides access to a git repository.\"\"\"\n    validate_git_url_no_ssrf(repository_url)\n    user, password = _split_basic_auth(basic_auth)\n    return _probe_remote(repository_url, user=user, password=password)\n\n\nasync def validate_git_credential_access(repository_url: str, basic_auth: str) -> bool:\n    \"\"\"Check credentialed git access without blocking the event loop.\"\"\"\n    return await asyncio.to_thread(\n        validate_git_credential_access_sync, repository_url, basic_auth\n    )\n\n\ndef is_git_repo() -> bool:\n    try:\n        with _discover_repo():\n            return True\n    except GitAccessError:\n        return False\n\n\ndef list_remotes() -> list[str]:\n    with _discover_repo() as repo:\n        config = repo.get_config()\n        urls: list[str] = []\n        for section in config.sections():\n            if len(section) == 2 and section[0] == b\"remote\":\n                try:\n                    url = config.get(section, b\"url\")\n                except KeyError:\n                    continue\n                urls.append(url.decode())\n        return urls\n\n\ndef get_current_branch() -> str | None:\n    with _discover_repo() as repo:\n        head_ref = repo.refs.read_ref(_HEAD_REF)\n        if head_ref is None:\n            return None\n        if head_ref.startswith(b\"ref: \"):\n            head_ref = head_ref[5:]\n        if not head_ref.startswith(_REFS_HEADS):\n            return None\n        branch = head_ref[len(_REFS_HEADS) :].decode()\n        return branch or None\n\n\ndef get_commit_sha_for_ref(ref: str) -> str | None:\n    with _discover_repo() as repo:\n        ref_bytes = ref.encode()\n        for candidate in (\n            Ref(ref_bytes),\n            Ref(_REFS_HEADS + ref_bytes),\n            Ref(_REFS_TAGS + ref_bytes),\n            Ref(_REFS_REMOTES + ref_bytes),\n        ):\n            try:\n                return repo.refs[candidate].decode()\n            except KeyError:\n                continue\n\n        if FULL_SHA_RE.match(ref):\n            try:\n                obj = repo[ref_bytes]\n                return obj.id.decode()\n            except KeyError:\n                return None\n        return None\n\n\ndef get_git_root() -> Path:\n    with _discover_repo() as repo:\n        return Path(repo.path)\n\n\ndef working_tree_has_changes() -> bool:\n    \"\"\"\n    Returns True if the working tree has uncommitted or untracked changes.\n    Safe to call; returns False if unable to determine.\n    \"\"\"\n    try:\n        status = porcelain.status(str(Path.cwd()))\n    except Exception:\n        return False\n    staged = status.staged or {}\n    has_staged = any(staged.get(key) for key in (\"add\", \"delete\", \"modify\"))\n    return bool(has_staged or status.unstaged or status.untracked)\n\n\ndef get_unpushed_commits_count() -> int | None:\n    \"\"\"\n    Returns the number of local commits ahead of the upstream.\n\n    - Returns an integer >= 0 when an upstream is configured\n    - Returns None when no upstream is configured\n    - Returns 0 if the status cannot be determined\n    \"\"\"\n    try:\n        with _discover_repo() as repo:\n            try:\n                head_ref = repo.refs.read_ref(_HEAD_REF)\n            except Exception:\n                return 0\n            if not head_ref or not head_ref.startswith(b\"ref: \"):\n                # Detached HEAD — no upstream concept\n                return None\n            branch_full = head_ref[5:]\n            if not branch_full.startswith(_REFS_HEADS):\n                return None\n            branch_name = branch_full[len(_REFS_HEADS) :]\n\n            config = repo.get_config()\n            try:\n                merge_ref = config.get((b\"branch\", branch_name), b\"merge\")\n                remote = config.get((b\"branch\", branch_name), b\"remote\")\n            except KeyError:\n                return None\n\n            if not merge_ref.startswith(_REFS_HEADS):\n                return None\n            upstream_branch = merge_ref[len(_REFS_HEADS) :]\n            tracking_ref = Ref(_REFS_REMOTES + remote + b\"/\" + upstream_branch)\n\n            try:\n                local_sha = repo.refs[_HEAD_REF]\n                upstream_sha = repo.refs[tracking_ref]\n            except KeyError:\n                return 0\n\n            try:\n                walker = Walker(\n                    repo.object_store,\n                    [local_sha],\n                    exclude=[upstream_sha],\n                    max_entries=1001,\n                )\n                count = sum(1 for _ in walker)\n            except Exception:\n                return 0\n            return count\n    except GitAccessError:\n        return 0\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/iter_utils.py",
    "content": "\"\"\"Iterator utilities for buffering, sorting, and debouncing streams.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport time\nfrom typing import Any, AsyncGenerator, Callable, TypeVar, cast\n\nfrom typing_extensions import Literal\n\nT = TypeVar(\"T\")\n\n\nasync def debounced_sorted_prefix(\n    inner: AsyncGenerator[T, None],\n    *,\n    key: Callable[[T], Any],\n    debounce_seconds: float = 0.1,\n    max_window_seconds: float = 0.1,\n) -> AsyncGenerator[T, None]:\n    \"\"\"Yield a stream where the initial burst is sorted, then passthrough.\n\n    Behavior:\n    - Buffer early items and sort them by the provided key.\n    - Flush the buffer when either:\n      - No new item arrives for `debounce_seconds`, or\n      - `max_window_seconds` elapses from the first buffered item, or\n    - After the first flush, subsequent items are yielded passthrough, in arrival order.\n\n    This async variant uses an asyncio.Queue and a background task to pump `inner`.\n    \"\"\"\n\n    buffer: list[T] = []\n    debouncer = Debouncer(debounce_seconds, max_window_seconds)\n    merged = merge_generators(inner, debouncer.aiter())\n\n    async for item in merged:\n        if item == \"__COMPLETE__\":\n            buffer.sort(key=key)\n            for buffered_item in buffer:\n                yield buffered_item\n            buffer = []\n        else:\n            # item is T after checking != \"__COMPLETE__\"\n            actual_item = cast(T, item)\n            if debouncer.is_complete:\n                yield actual_item\n            else:\n                debouncer.extend_window()\n                buffer.append(actual_item)\n\n\nCOMPLETE = Literal[\"__COMPLETE__\"]\n\n\nasync def merge_generators(\n    *generators: AsyncGenerator[T, None],\n    stop_on_first_completion: bool = False,\n) -> AsyncGenerator[T, None]:\n    \"\"\"\n    Merge multiple async iterators into a single async iterator, yielding items as\n    soon as any source produces them.\n\n    - If stop_on_first_completion is False (default), continues until all inputs are exhausted.\n    - If stop_on_first_completion is True, stops as soon as any input completes.\n    - Propagates exceptions from any input immediately.\n    \"\"\"\n    if not generators:\n        return\n\n    active_generators: dict[int, AsyncGenerator[T, None]] = {\n        index: generator for index, generator in enumerate(generators)\n    }\n\n    next_item_tasks: dict[int, asyncio.Task[T]] = {}\n    exception_to_raise: BaseException | None = None\n    stopped_on_first_completion = False\n\n    # Prime one pending task per generator to maintain fairness\n    for index, generator in active_generators.items():\n        next_item_tasks[index] = asyncio.create_task(anext(generator))\n\n    try:\n        while next_item_tasks and exception_to_raise is None:\n            done, _ = await asyncio.wait(\n                set(next_item_tasks.values()),\n                return_when=asyncio.FIRST_COMPLETED,\n            )\n\n            completed_results: list[tuple[int, T]] = []\n            for finished in done:\n                # Locate which generator this task belonged to\n                task_index: int | None = None\n                for index, task in next_item_tasks.items():\n                    if task is finished:\n                        task_index = index\n                        break\n\n                if task_index is None:\n                    # Should not happen, but continue defensively\n                    continue\n\n                try:\n                    value = finished.result()\n                except StopAsyncIteration:\n                    # Generator exhausted\n                    if stop_on_first_completion:\n                        stopped_on_first_completion = True\n                        # Break out of the inner loop; the outer loop will\n                        # observe the stop flag and exit to the finally block\n                        # where pending tasks are cancelled and generators closed.\n                        break\n                    else:\n                        next_item_tasks.pop(task_index, None)\n                        active_generators.pop(task_index, None)\n                        continue\n                except Exception as exc:  # noqa: BLE001 - propagate specific generator error\n                    exception_to_raise = exc\n                    break\n                else:\n                    completed_results.append((task_index, value))\n            if stopped_on_first_completion:\n                break\n            for task_index, value in completed_results:\n                # Remove the finished task before yielding\n                next_item_tasks.pop(task_index, None)\n                yield value\n                # Schedule the next item fetch for this generator\n                active_gen: AsyncGenerator[T, None] | None = active_generators.get(\n                    task_index\n                )\n                if active_gen is not None:\n                    next_item_tasks[task_index] = asyncio.create_task(anext(active_gen))\n    finally:\n        # Ensure we do not leak tasks or open generators\n        for task in next_item_tasks.values():\n            task.cancel()\n        if next_item_tasks:\n            try:\n                await asyncio.gather(*next_item_tasks.values(), return_exceptions=True)\n            except Exception:\n                pass\n        for gen in active_generators.values():\n            try:\n                await gen.aclose()\n            except Exception:\n                pass\n\n    if exception_to_raise is not None:\n        raise exception_to_raise\n    if stopped_on_first_completion:\n        return\n\n\nclass Debouncer:\n    \"\"\"\n    Continually extends a complete time while extend is called, up to a max window.\n    Exposes methods that notify on completion\n    \"\"\"\n\n    def __init__(\n        self,\n        debounce_seconds: float = 0.1,\n        max_window_seconds: float = 1,\n        get_time: Callable[[], float] = time.monotonic,\n    ):\n        self.debounce_seconds = debounce_seconds\n        self.max_window_seconds = max_window_seconds\n        self.complete_signal = asyncio.Event()\n        self.get_time = get_time\n        self.start_time = self.get_time()\n        self.complete_time = self.start_time + self.debounce_seconds\n        self.max_complete_time = self.start_time + self.max_window_seconds\n        asyncio.create_task(self._loop())\n\n    async def _loop(self) -> None:\n        while not self.complete_signal.is_set():\n            now = self.get_time()\n            remaining = min(self.complete_time, self.max_complete_time) - now\n            if remaining <= 0:\n                self.complete_signal.set()\n            else:\n                await asyncio.sleep(remaining)\n\n    @property\n    def is_complete(self) -> bool:\n        return self.complete_signal.is_set()\n\n    def extend_window(self) -> None:\n        \"\"\"Mark a new item has arrived, extending the debounce window.\"\"\"\n        now = self.get_time()\n        self.complete_time = now + self.debounce_seconds\n\n    async def wait(self) -> None:\n        \"\"\"Wait for the debounce window to expire, or the max window to elapse.\"\"\"\n        await self.complete_signal.wait()\n\n    async def aiter(self) -> AsyncGenerator[COMPLETE, None]:\n        \"\"\"Yield a stream that emits an element when the wait event occurs.\"\"\"\n        await self.wait()\n        yield \"__COMPLETE__\"\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/path_util.py",
    "content": "from pathlib import Path\n\n\ndef validate_path_traversal(\n    path: Path, source_root: Path, path_type: str = \"path\"\n) -> None:\n    \"\"\"Validates that a path is within the source root to prevent path traversal attacks.\n\n    Args:\n        path: The path to validate\n        source_root: The root directory that paths should be relative to\n        path_type: Description of the path type for error messages\n\n    Raises:\n        DeploymentError: If the path is outside the source root\n    \"\"\"\n    resolved_path = (source_root / path).resolve()\n    resolved_source_root = source_root.resolve()\n\n    if not resolved_path.is_relative_to(resolved_source_root):\n        msg = (\n            f\"{path_type} {path} is not a subdirectory of the source root {source_root}\"\n        )\n        raise RuntimeError(msg)\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/py.typed",
    "content": ""
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/schema/__init__.py",
    "content": "from .backups import (\n    BackupListResponse,\n    BackupResponse,\n    RestoreDeploymentResult,\n    RestoreRequest,\n    RestoreResponse,\n)\nfrom .base import Base\nfrom .deployments import (\n    DeploymentCreate,\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    DeploymentsListResponse,\n    DeploymentUpdate,\n    LlamaDeploymentPhase,\n    LlamaDeploymentSpec,\n    LogEvent,\n    RollbackRequest,\n    apply_deployment_update,\n)\nfrom .git_validation import RepositoryValidationRequest, RepositoryValidationResponse\nfrom .projects import (\n    OrganizationsListResponse,\n    OrgSummary,\n    ProjectsListResponse,\n    ProjectSummary,\n)\nfrom .public import Capabilities, Capability, VersionResponse\n\n__all__ = [\n    \"BackupListResponse\",\n    \"BackupResponse\",\n    \"RestoreDeploymentResult\",\n    \"RestoreRequest\",\n    \"RestoreResponse\",\n    \"Base\",\n    \"LogEvent\",\n    \"DeploymentCreate\",\n    \"DeploymentResponse\",\n    \"DeploymentUpdate\",\n    \"DeploymentsListResponse\",\n    \"DeploymentHistoryResponse\",\n    \"RollbackRequest\",\n    \"LlamaDeploymentSpec\",\n    \"apply_deployment_update\",\n    \"LlamaDeploymentPhase\",\n    \"RepositoryValidationResponse\",\n    \"RepositoryValidationRequest\",\n    \"OrgSummary\",\n    \"OrganizationsListResponse\",\n    \"ProjectSummary\",\n    \"ProjectsListResponse\",\n    \"VersionResponse\",\n    \"Capabilities\",\n    \"Capability\",\n]\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/schema/backups.py",
    "content": "\"\"\"Schema models for backup/restore API.\"\"\"\n\nfrom datetime import datetime\nfrom typing import Literal\n\nfrom .base import Base\n\n\nclass BackupResponse(Base):\n    backup_id: str\n    status: Literal[\"completed\", \"failed\", \"deleted\"]\n    timestamp: datetime | None = None\n    deployment_count: int | None = None\n    size_bytes: int | None = None\n    error: str | None = None\n\n\nclass BackupListResponse(Base):\n    backups: list[BackupResponse]\n\n\nclass RestoreRequest(Base):\n    backup_id: str\n    conflict_mode: Literal[\"skip\", \"overwrite-if-newer\", \"overwrite-always\"] = \"skip\"\n    include_deletions: bool = False\n\n\nclass RestoreDeploymentResult(Base):\n    name: str\n    action: Literal[\"created\", \"updated\", \"skipped\", \"failed\", \"deleted\"]\n    error: str | None = None\n\n\nclass RestoreResponse(Base):\n    backup_id: str\n    status: Literal[\"completed\", \"failed\"]\n    results: list[RestoreDeploymentResult] | None = None\n    error: str | None = None\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/schema/base.py",
    "content": "from pydantic import BaseModel, ConfigDict\n\nbase_config = ConfigDict(\n    from_attributes=True,\n    arbitrary_types_allowed=True,\n    use_enum_values=False,\n    # ===== timedelta serialization =====\n    # Serialize timedelta as float\n    # This was the default behavior in pydantic v1, but was changed in v2 to be \"iso8601\"\n    # We want to keep the same behavior as v1 for now\n    # ================================\n    ser_json_timedelta=\"float\",\n    # NOTE: we often use data model with fields that have \"model_\" prefix,\n    # so we need to set protected_namespaces=() to avoid conflict\n    protected_namespaces=(),\n)\n\n\nclass Base(BaseModel):\n    model_config = base_config\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/schema/deployments.py",
    "content": "import re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Annotated, Literal\n\nfrom packaging.version import InvalidVersion, Version\nfrom pydantic import BeforeValidator, Field, HttpUrl, computed_field, model_validator\n\nfrom .base import Base\n\nAPPSERVER_TAG_PREFIX = \"appserver-\"\n\n# DNS-1035 label: starts with lowercase letter, lowercase alphanumeric + hyphens,\n# max 63 chars, no trailing hyphen.\n_DNS_1035_RE = re.compile(r\"^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$\")\n\n\ndef validate_dns_1035_label(value: str) -> str:\n    \"\"\"Validate that a string is a valid DNS-1035 label.\n\n    Rules: starts with lowercase letter, only lowercase alphanumeric and hyphens,\n    max 63 chars, no trailing hyphen.\n\n    Raises ValueError if invalid.\n    \"\"\"\n    if not _DNS_1035_RE.match(value):\n        raise ValueError(\n            f\"Invalid DNS-1035 label: {value!r}. Must start with a lowercase letter, \"\n            \"contain only lowercase alphanumeric characters and hyphens, \"\n            \"max 63 characters, and not end with a hyphen.\"\n        )\n    return value\n\n\ndef validate_appserver_version(value: str) -> str:\n    \"\"\"Validate the public package version used as the appserver image tag.\"\"\"\n    try:\n        version = Version(value)\n    except InvalidVersion:\n        raise ValueError(\n            f\"invalid appserver_version {value!r}, expected something like '0.11.3'\"\n        ) from None\n\n    if version.epoch != 0:\n        raise ValueError(f\"invalid appserver_version {value!r} (epoch not supported)\")\n\n    if version.local is not None:\n        raise ValueError(\n            f\"invalid appserver_version {value!r} (local segment not supported)\"\n        )\n    return value\n\n\ndef _validate_optional_appserver_version(value: str | None) -> str | None:\n    if value is None:\n        return value\n    return validate_appserver_version(value)\n\n\nAppserverVersionField = Annotated[\n    str | None, BeforeValidator(_validate_optional_appserver_version)\n]\n\n\n# Sentinel URL scheme used in the CRD's repoUrl to indicate\n# the deployment's code is stored in the internal S3-backed repo.\nINTERNAL_CODE_REPO_SCHEME = \"internal://\"\n\n\ndef version_to_image_tag(version: str) -> str:\n    \"\"\"Convert an appserver_version like '0.4.2' to an image tag like '0.4.2'.\"\"\"\n    return version\n\n\ndef image_tag_to_version(tag: str) -> str | None:\n    \"\"\"Extract version from image tag.\n\n    Handles both new plain tags ('0.4.2') and legacy prefixed tags ('appserver-0.4.2').\n    Returns the version string, or None if the tag is not a recognized version format.\n    \"\"\"\n    if tag.startswith(APPSERVER_TAG_PREFIX):\n        return tag.removeprefix(APPSERVER_TAG_PREFIX)\n    # New-style plain version tags — check if it looks like a version (starts with digit)\n    if tag and tag[0].isdigit():\n        return tag\n    return None\n\n\n# K8s CRD phase values\nLlamaDeploymentPhase = Literal[\n    \"Pending\",  # Waiting for deployment resources to be ready (pods starting up)\n    \"Running\",  # Deployment is healthy and serving traffic\n    \"Failed\",  # Complete deployment failure - no pods available\n    \"RollingOut\",  # Rolling update in progress - new pods being created while old ones still serve traffic\n    \"RolloutFailed\",  # New deployment failed but old pods are still available and serving traffic\n    \"Suspended\",  # Deployment is suspended (scaled to 0 replicas)\n    \"Building\",  # Build Job is in progress\n    \"BuildFailed\",  # Build Job failed\n]\n\n\nclass DeploymentEvent(Base):\n    message: str | None = Field(\n        default=None, description=\"Human-readable event message\"\n    )\n    reason: str | None = Field(\n        default=None, description=\"Machine-readable reason string\"\n    )\n    type: str | None = Field(default=None, description=\"Event type (Normal or Warning)\")\n    first_timestamp: datetime | None = Field(\n        default=None, description=\"When this event was first observed\"\n    )\n    last_timestamp: datetime | None = Field(\n        default=None, description=\"When this event was last observed\"\n    )\n    count: int | None = Field(\n        default=None, description=\"Number of times this event has occurred\"\n    )\n\n\nclass DeploymentResponse(Base):\n    id: str = Field(description=\"Stable DNS-safe identifier for the deployment\")\n    display_name: str = Field(description=\"User-facing display label\")\n    repo_url: str = Field(description=\"Git repository URL for the deployment source\")\n    deployment_file_path: str = Field(\n        description=\"Path to the deployment config file within the repository\"\n    )\n    git_ref: str | None = Field(\n        default=None, description=\"Git reference (branch, tag, or commit) to deploy\"\n    )\n    git_sha: str | None = Field(\n        default=None, description=\"Resolved git commit SHA of the current deployment\"\n    )\n    has_personal_access_token: bool = Field(\n        default=False,\n        description=\"Whether a personal access token is configured for repo access\",\n    )\n    project_id: str = Field(description=\"ID of the project this deployment belongs to\")\n    secret_names: list[str] | None = Field(\n        default=None,\n        description=\"Names of configured secrets (excluding GITHUB_PAT)\",\n    )\n    apiserver_url: HttpUrl | None = Field(\n        default=None, description=\"URL of the deployment's API server\"\n    )\n    # `LlamaDeploymentPhase | str`: tolerate unknown phase values from a newer\n    # server. Known phases still narrow to the Literal arm; unknown ones fall\n    # through to `str` instead of failing validation on older clients.\n    status: LlamaDeploymentPhase | str = Field(description=\"Current deployment phase\")\n    warning: str | None = Field(\n        default=None,\n        description=\"Warning message about the deployment state\",\n    )\n    events: list[DeploymentEvent] | None = Field(\n        default=None, description=\"Recent Kubernetes events for this deployment\"\n    )\n    appserver_version: str | None = Field(\n        default=None, description=\"Appserver version (e.g. '0.4.2')\"\n    )\n    suspended: bool = Field(\n        default=False, description=\"Whether the deployment is scaled to 0 replicas\"\n    )\n\n    @computed_field(description=\"Deprecated: use display_name\")\n    @property\n    def name(self) -> str:\n        return self.display_name\n\n    @computed_field(description=\"Deprecated: use appserver_version\")\n    @property\n    def llama_deploy_version(self) -> str | None:\n        return self.appserver_version\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _compat_aliases(cls, data: dict) -> dict:  # type: ignore[type-arg]\n        \"\"\"Accept deprecated 'name' and 'llama_deploy_version' input aliases.\"\"\"\n        if isinstance(data, dict):\n            if not data.get(\"display_name\") and data.get(\"name\"):\n                data[\"display_name\"] = data[\"name\"]\n            if not data.get(\"appserver_version\") and data.get(\"llama_deploy_version\"):\n                data[\"appserver_version\"] = data[\"llama_deploy_version\"]\n        return data\n\n\nclass DeploymentsListResponse(Base):\n    deployments: list[DeploymentResponse] = Field(\n        description=\"List of deployments in the project\"\n    )\n\n\nclass DeploymentCreate(Base):\n    display_name: str = Field(description=\"User-facing display label\")\n    id: str | None = Field(\n        default=None,\n        description=\"Optional explicit DNS-safe identifier. \"\n        \"If omitted, generated from display_name.\",\n    )\n    repo_url: str = Field(default=\"\", description=\"Git repository URL to deploy from\")\n    deployment_file_path: str | None = Field(\n        default=None,\n        description=\"Path to the deployment config file within the repository\",\n    )\n    git_ref: str | None = Field(\n        default=None,\n        description=\"Git reference (branch, tag, or commit) to deploy\",\n    )\n    personal_access_token: str | None = Field(\n        default=None, description=\"Personal access token for private repo access\"\n    )\n    secrets: dict[str, str] | None = Field(\n        default=None,\n        description=\"Key-value pairs to store as deployment secrets\",\n    )\n    appserver_version: AppserverVersionField = Field(\n        default=None,\n        description=\"Appserver version to use (e.g. '0.4.2'). \"\n        \"If omitted, server may set based on client version.\",\n    )\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _compat_aliases(cls, data: dict) -> dict:  # type: ignore[type-arg]\n        \"\"\"Accept deprecated 'name' and 'llama_deploy_version' input aliases.\"\"\"\n        if isinstance(data, dict):\n            if data.get(\"name\") and not data.get(\"display_name\"):\n                data[\"display_name\"] = data.pop(\"name\")\n            if data.get(\"llama_deploy_version\") and not data.get(\"appserver_version\"):\n                data[\"appserver_version\"] = data.pop(\"llama_deploy_version\")\n        return data\n\n    @computed_field(description=\"Deprecated: use display_name\")\n    @property\n    def name(self) -> str:\n        return self.display_name\n\n    @computed_field(description=\"Deprecated: use appserver_version\")\n    @property\n    def llama_deploy_version(self) -> str | None:\n        return self.appserver_version\n\n    @model_validator(mode=\"after\")\n    def _require_id_format(self) -> \"DeploymentCreate\":\n        if self.id is not None:\n            validate_dns_1035_label(self.id)\n        return self\n\n\nclass LlamaDeploymentMetadata(Base):\n    name: str\n    namespace: str\n    uid: str | None = None\n    resourceVersion: str | None = None\n    creationTimestamp: datetime | None = None\n    annotations: dict[str, str] | None = None\n    labels: dict[str, str] | None = None\n\n\nclass LlamaDeploymentSpec(Base):\n    \"\"\"\n    LlamaDeployment spec fields as defined in the Kubernetes CRD.\n\n    Maps to the spec section of the LlamaDeployment custom resource.\n    Field names match exactly with the K8s CRD for direct conversion.\n    \"\"\"\n\n    projectId: str\n    repoUrl: str\n    deploymentFilePath: str = \".\"\n    gitRef: str | None = None\n    gitSha: str | None = None\n    displayName: str | None = None\n    secretName: str | None = None\n    # when true, the deployment will prebuild the UI assets and serve them from a static file server\n    staticAssetsPath: str | None = None\n    # explicit imageTag (operator will use this if provided)\n    imageTag: str | None = None\n    suspended: bool = False\n    # monotonically increasing counter to force a new build (retry failed builds)\n    buildGeneration: int = 0\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _migrate_name(cls, data: dict) -> dict:  # type: ignore[type-arg]\n        \"\"\"Migrate deprecated spec.name → spec.displayName from old CRDs.\"\"\"\n        if isinstance(data, dict):\n            if data.get(\"name\") and not data.get(\"displayName\"):\n                data[\"displayName\"] = data.pop(\"name\")\n        return data\n\n    def get_display_name(self) -> str:\n        \"\"\"Return the display name.\"\"\"\n        return self.displayName or \"\"\n\n\nclass LlamaDeploymentStatus(Base):\n    \"\"\"\n    LlamaDeployment status fields as defined in the Kubernetes CRD.\n\n    Maps to the status section of the LlamaDeployment custom resource.\n    \"\"\"\n\n    phase: str | None = None\n    message: str | None = None\n    lastUpdated: datetime | None = None\n    authToken: str | None = None\n    # Historical list of released versions from the CRD (camelCase fields)\n    releaseHistory: list[\"ReleaseHistoryEntry\"] | None = None\n\n\nclass LlamaDeploymentCRD(Base):\n    metadata: LlamaDeploymentMetadata\n    spec: LlamaDeploymentSpec\n    status: LlamaDeploymentStatus = Field(default_factory=LlamaDeploymentStatus)\n\n\nclass DeploymentUpdate(Base):\n    \"\"\"\n    Patch-style update model for deployments.\n\n    Fields set to None remain unchanged.\n\n    For secrets: provide a dict where string values add/update secrets\n    and null values remove secrets.\n    \"\"\"\n\n    display_name: str | None = Field(\n        default=None, description=\"Updated user-facing display label\"\n    )\n    repo_url: str | None = Field(default=None, description=\"Updated git repository URL\")\n    deployment_file_path: str | None = Field(\n        default=None,\n        description=\"Updated path to the deployment config file\",\n    )\n    git_ref: str | None = Field(\n        default=None, description=\"Updated git reference to deploy\"\n    )\n    git_sha: str | None = Field(\n        default=None, description=\"Resolved git commit SHA (set by service layer)\"\n    )\n    personal_access_token: str | None = Field(\n        default=None,\n        description=\"Updated personal access token. Empty string removes it.\",\n    )\n    secrets: dict[str, str | None] | None = Field(\n        default=None,\n        description=\"Secret updates: string values add/update, null values remove\",\n    )\n    static_assets_path: Path | None = Field(\n        default=None, description=\"Path to prebuilt UI assets (set by service layer)\"\n    )\n    appserver_version: AppserverVersionField = Field(\n        default=None, description=\"Updated appserver version selector\"\n    )\n    image_tag: str | None = Field(\n        default=None,\n        description=\"Explicit image tag (takes precedence over appserver_version)\",\n    )\n    bump_to_latest_appserver: bool = Field(\n        default=False,\n        description=\"Bump the appserver image to the cluster's current default version\",\n    )\n    suspended: bool | None = Field(\n        default=None, description=\"Set to true to suspend (scale to 0), false to resume\"\n    )\n    rebuild: bool = Field(\n        default=False,\n        description=\"Force a rebuild (e.g. retry after transient network failure)\",\n    )\n\n    @computed_field(description=\"Deprecated: use display_name\")\n    @property\n    def name(self) -> str | None:\n        return self.display_name\n\n    @computed_field(description=\"Deprecated: use appserver_version\")\n    @property\n    def llama_deploy_version(self) -> str | None:\n        return self.appserver_version\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _compat_aliases(cls, data: dict) -> dict:  # type: ignore[type-arg]\n        \"\"\"Accept deprecated 'name' and 'llama_deploy_version' input aliases.\"\"\"\n        if isinstance(data, dict):\n            if data.get(\"name\") and not data.get(\"display_name\"):\n                data[\"display_name\"] = data.pop(\"name\")\n            if data.get(\"llama_deploy_version\") and not data.get(\"appserver_version\"):\n                data[\"appserver_version\"] = data.pop(\"llama_deploy_version\")\n        return data\n\n    def has_git_fields(self) -> bool:\n        \"\"\"Return True if any git-affecting fields are set.\"\"\"\n        return any(\n            [\n                self.repo_url is not None,\n                self.deployment_file_path is not None,\n                self.git_ref is not None,\n                self.personal_access_token is not None,\n            ]\n        )\n\n    def _has_substantive_fields(self) -> bool:\n        \"\"\"Return True if update contains fields that affect the running deployment.\"\"\"\n        return any(\n            [\n                self.repo_url is not None,\n                self.deployment_file_path is not None,\n                self.git_ref is not None,\n                self.personal_access_token is not None,\n                self.secrets is not None,\n                self.image_tag is not None,\n                self.appserver_version is not None,\n                self.bump_to_latest_appserver,\n            ]\n        )\n\n\nclass DeploymentUpdateResult(Base):\n    \"\"\"\n    Result of applying a DeploymentUpdate to a LlamaDeploymentSpec.\n\n    Contains the updated spec and lists of secret changes to apply.\n    \"\"\"\n\n    updated_spec: LlamaDeploymentSpec\n    secret_adds: dict[str, str]\n    secret_removes: list[str]\n\n\ndef apply_deployment_update(\n    update: DeploymentUpdate,\n    existing_spec: LlamaDeploymentSpec,\n) -> DeploymentUpdateResult:\n    \"\"\"\n    Apply a DeploymentUpdate to an existing LlamaDeploymentSpec.\n\n    Returns the updated spec and lists of secret changes.\n\n    Args:\n        update: The update to apply (snake_case fields from API)\n        existing_spec: The current LlamaDeploymentSpec (camelCase fields)\n        git_sha: The resolved git SHA to set\n\n    Returns:\n        DeploymentUpdateResult with updated spec and secret changes\n    \"\"\"\n    # Start with a copy of the existing spec\n    updated_spec = existing_spec.model_copy()\n\n    # Apply direct field updates (only if not None)\n    # Convert from snake_case API fields to camelCase spec fields\n    if update.display_name is not None:\n        updated_spec.displayName = update.display_name\n\n    if update.repo_url is not None:\n        updated_spec.repoUrl = update.repo_url\n\n    if update.deployment_file_path is not None:\n        updated_spec.deploymentFilePath = update.deployment_file_path\n\n    if update.git_ref is not None:\n        updated_spec.gitRef = update.git_ref\n\n    # Update gitSha if provided\n    if update.git_sha is not None:\n        updated_spec.gitSha = None if update.git_sha == \"\" else update.git_sha\n\n    if update.static_assets_path is not None:\n        updated_spec.staticAssetsPath = str(update.static_assets_path)\n\n    # Track secret changes\n    secret_adds: dict[str, str] = {}\n    secret_removes: list[str] = []\n\n    # Handle personal access token (stored as GITHUB_PAT secret)\n    if update.personal_access_token is not None:\n        if update.personal_access_token == \"\":\n            # Empty string means remove the PAT\n            secret_removes.append(\"GITHUB_PAT\")\n        else:\n            # Non-empty string means add/update the PAT\n            secret_adds[\"GITHUB_PAT\"] = update.personal_access_token\n\n    # Handle explicit secret updates\n    secrets = update.secrets\n    if secrets is not None:\n        for key, value in secrets.items():\n            if value is None:\n                # None means remove this secret\n                secret_removes.append(key)\n            else:\n                # String value means add/update this secret\n                secret_adds[key] = value\n\n    if update.suspended is not None:\n        updated_spec.suspended = update.suspended\n\n    # Auto-resume: if the deployment was suspended and this update contains\n    # substantive fields (but doesn't explicitly set suspended), resume it.\n    if (\n        update.suspended is None\n        and existing_spec.suspended\n        and update._has_substantive_fields()\n    ):\n        updated_spec.suspended = False\n\n    # Handle image tag / version selector (image_tag takes precedence)\n    if update.image_tag is not None:\n        updated_spec.imageTag = update.image_tag\n    elif update.appserver_version is not None:\n        updated_spec.imageTag = version_to_image_tag(update.appserver_version)\n\n    # Bump buildGeneration to force a rebuild (e.g. retry after transient failure)\n    if update.rebuild:\n        updated_spec.buildGeneration = existing_spec.buildGeneration + 1\n\n    return DeploymentUpdateResult(\n        updated_spec=updated_spec,\n        secret_adds=secret_adds,\n        secret_removes=secret_removes,\n    )\n\n\nclass LogEvent(Base):\n    pod: str = Field(description=\"Name of the Kubernetes pod\")\n    container: str = Field(description=\"Name of the container within the pod\")\n    text: str = Field(description=\"Log line content\")\n    timestamp: datetime = Field(description=\"When the log line was emitted\")\n\n\n# ===== Release history models =====\n\n\nclass ReleaseHistoryEntry(Base):\n    \"\"\"\n    Mirrors the CRD status.releaseHistory entry with camelCase keys.\n    \"\"\"\n\n    gitSha: str\n    imageTag: str | None = None\n    releasedAt: datetime\n\n\nclass ReleaseHistoryItem(Base):\n    \"\"\"\n    API-exposed release history item with snake_case keys for clients.\n    \"\"\"\n\n    git_sha: str = Field(description=\"Git commit SHA for this release\")\n    image_tag: str | None = Field(\n        default=None, description=\"Appserver image tag used for this release\"\n    )\n    released_at: datetime = Field(description=\"When this version was released\")\n\n\nclass DeploymentHistoryResponse(Base):\n    deployment_id: str = Field(description=\"ID of the deployment\")\n    history: list[ReleaseHistoryItem] = Field(\n        description=\"List of released versions, newest first\"\n    )\n\n\nclass RollbackRequest(Base):\n    git_sha: str = Field(description=\"Git commit SHA to roll back to\")\n    image_tag: str | None = Field(\n        default=None, description=\"Image tag to use for the rollback\"\n    )\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/schema/git_validation.py",
    "content": "from pathlib import Path\n\nfrom pydantic import BaseModel, Field\n\n\nclass RepositoryValidationResponse(BaseModel):\n    \"\"\"\n    Unified response for repository validation that works for any git repository.\n    This is the primary schema that should be used for the /validate-repository endpoint.\n    \"\"\"\n\n    accessible: bool = Field(\n        ...,\n        description=\"Whether the repository can be accessed by any means available to the server\",\n    )\n    message: str = Field(..., description=\"Human-readable string explaining the status\")\n    pat_is_obsolete: bool = Field(\n        default=False,\n        description=\"True if validation succeeded via GitHub App for a deployment that previously used a PAT\",\n    )\n    github_app_name: str | None = Field(\n        default=None,\n        description=\"Name of the GitHub App if repository is a private GitHub repo and server has GitHub App configured\",\n    )\n    github_app_installation_url: str | None = Field(\n        default=None,\n        description=\"GitHub App installation/authorization URL for connecting the app to a repository owner\",\n    )\n    github_app_settings_url: str | None = Field(\n        default=None,\n        description=\"GitHub App installation settings URL for managing repository access on an existing installation\",\n    )\n    github_app_authorization_url: str | None = Field(\n        default=None,\n        description=\"Browser-openable URL that triggers GitHub OAuth for CLI clients needing to connect their GitHub account\",\n    )\n\n\nclass RepositoryValidationRequest(BaseModel):\n    repository_url: str\n    deployment_id: str | None = None\n    pat: str | None = None\n\n\nclass GitApplicationValidationResponse(BaseModel):\n    \"\"\"\n    After general repository validation, a model that describes further validation of configuration, such as the\n    git reference, it's resolved SHA (if resolveable), and whether the deployment file is valid.\n    \"\"\"\n\n    is_valid: bool\n    error_message: str | None = None\n    git_ref: str | None = None\n    git_sha: str | None = None\n    valid_deployment_file_path: str | None = None\n    ui_build_output_path: Path | None = Field(\n        default=None,\n        description=\"Path to the UI build output, if the deployment's UI has a package.json with a build script; None if no UI is configured\",\n    )\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/schema/projects.py",
    "content": "from typing import Any\n\nfrom pydantic import model_validator\n\nfrom .base import Base\n\n\nclass OrgSummary(Base):\n    \"\"\"Summary of an organization.\"\"\"\n\n    org_id: str\n    org_name: str\n    is_default: bool = False\n\n\nclass OrganizationsListResponse(Base):\n    \"\"\"Response model for listing organizations.\"\"\"\n\n    organizations: list[OrgSummary]\n\n\nclass ProjectSummary(Base):\n    \"\"\"Summary of a project with deployment count\"\"\"\n\n    project_id: str\n    project_name: str\n    deployment_count: int\n    org_id: str = \"default\"\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def set_default_project_name(cls, data: Any) -> Any:\n        if isinstance(data, dict):\n            if \"project_name\" not in data or data.get(\"project_name\") is None:\n                if \"project_id\" in data:\n                    data[\"project_name\"] = data[\"project_id\"]\n        return data\n\n\nclass ProjectsListResponse(Base):\n    \"\"\"Response model for listing projects with deployment counts\"\"\"\n\n    projects: list[ProjectSummary]\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/schema/public.py",
    "content": "from typing import Literal\n\nfrom .base import Base\n\nCapability = Literal[\"code_push\"] | str\n\n\nclass Capabilities:\n    \"\"\"Known capability identifiers advertised by the server.\"\"\"\n\n    CODE_PUSH: Capability = \"code_push\"\n    ORGANIZATIONS: Capability = \"organizations\"\n\n\nclass VersionResponse(Base):\n    version: str\n    requires_auth: bool = False\n    min_llamactl_version: str | None = None\n    capabilities: list[Capability] = []\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/server/manage_api/__init__.py",
    "content": "from ._abstract_deployments_service import (\n    AbstractDeploymentsService,\n    AbstractPublicDeploymentsService,\n)\nfrom ._create_deployments_router import create_v1beta1_deployments_router\nfrom ._exceptions import DeploymentNotFoundError, ReplicaSetNotFoundError\n\n__all__ = [\n    \"AbstractDeploymentsService\",\n    \"AbstractPublicDeploymentsService\",\n    \"create_v1beta1_deployments_router\",\n    \"DeploymentNotFoundError\",\n    \"ReplicaSetNotFoundError\",\n]\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/server/manage_api/_abstract_deployments_service.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import AsyncGenerator, cast\n\nfrom fastapi import Request, Response\nfrom llama_agents.core import schema\nfrom llama_agents.core.schema import LogEvent\nfrom llama_agents.core.schema.deployments import (\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    RollbackRequest,\n)\n\n\nclass AbstractPublicDeploymentsService(ABC):\n    @abstractmethod\n    async def get_version(self) -> schema.VersionResponse:\n        \"\"\"\n        Get the version of the server\n        \"\"\"\n        ...\n\n\nclass AbstractDeploymentsService(ABC):\n    @abstractmethod\n    async def get_organizations(self) -> schema.OrganizationsListResponse:\n        \"\"\"\n        Get a list of organizations.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def get_projects(\n        self, org_id: str | None = None\n    ) -> schema.ProjectsListResponse:\n        \"\"\"\n        Get a list of projects, optionally filtered by org_id.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def validate_repository(\n        self,\n        project_id: str,\n        request: schema.RepositoryValidationRequest,\n    ) -> schema.RepositoryValidationResponse:\n        \"\"\"\n        Validate repository access and return unified response.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def create_deployment(\n        self,\n        project_id: str,\n        deployment_data: schema.DeploymentCreate,\n    ) -> DeploymentResponse:\n        \"\"\"\n        Create a new deployment\n\n        Args:\n            project_id: The ID of the project to create the deployment in\n            deployment_data: The data for the deployment\n\n        Returns:\n            The created deployment\n        Raises:\n            DeploymentNotFoundError: If the deployment ID is not found\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def get_deployments(\n        self,\n        project_id: str,\n    ) -> schema.DeploymentsListResponse:\n        \"\"\"\n        Get a list of deployments for a project\n\n        Args:\n            project_id: The ID of the project to get the deployments for\n\n        Returns:\n            A list of deployments\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def get_deployment(\n        self,\n        project_id: str,\n        deployment_id: str,\n        include_events: bool = False,\n    ) -> schema.DeploymentResponse:\n        \"\"\"\n        Get a deployment by ID\n\n        Args:\n            project_id: The ID of the project to get the deployment for\n            deployment_id: The ID of the deployment to get\n            include_events: Whether to include events in the response\n\n        Returns:\n            The deployment\n        Raises:\n            DeploymentNotFoundError: If the deployment ID is not found\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def delete_deployment(\n        self,\n        project_id: str,\n        deployment_id: str,\n    ) -> None:\n        \"\"\"\n        Delete a deployment\n\n        Args:\n            project_id: The ID of the project to delete the deployment from\n            deployment_id: The ID of the deployment to delete\n\n        Returns:\n            None\n        Raises:\n            DeploymentNotFoundError: If the deployment ID is not found\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def update_deployment(\n        self,\n        project_id: str,\n        deployment_id: str,\n        update_data: schema.DeploymentUpdate,\n    ) -> DeploymentResponse:\n        \"\"\"\n        Update a deployment\n\n        Args:\n            project_id: The ID of the project to update the deployment in\n            deployment_id: The ID of the deployment to update\n            update_data: The data to update the deployment with\n\n        Returns:\n            The updated deployment\n        Raises:\n            DeploymentNotFoundError: If the deployment ID is not found\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def get_deployment_history(\n        self, project_id: str, deployment_id: str\n    ) -> DeploymentHistoryResponse:\n        \"\"\"\n        Get the release history for a deployment.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def rollback_deployment(\n        self, project_id: str, deployment_id: str, request: RollbackRequest\n    ) -> DeploymentResponse:\n        \"\"\"\n        Roll back a deployment to a previous git sha.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def stream_deployment_logs(\n        self,\n        project_id: str,\n        deployment_id: str,\n        include_init_containers: bool = False,\n        since_seconds: int | None = None,\n        tail_lines: int | None = None,\n        follow: bool = True,\n    ) -> AsyncGenerator[LogEvent, None]:\n        \"\"\"\n        Stream the logs for a deployment.\n\n        Build job logs (if any) are automatically merged into the stream\n        before application logs.\n\n        Args:\n            project_id: The ID of the project to stream the logs for\n            deployment_id: The ID of the deployment to stream the logs for\n            include_init_containers: Whether to include init containers in the logs\n            since_seconds: The number of seconds to stream the logs for\n            tail_lines: The number of lines to stream the logs for\n            follow: If False, return only currently-available logs and end the\n                stream; if True (default), keep streaming and reconnect across\n                ReplicaSet changes.\n\n        Returns:\n            A generator of log events\n        Raises:\n            DeploymentNotFoundError: If the deployment ID is not found\n        \"\"\"\n        # This method is abstract. The following unreachable code ensures type\n        # checkers treat it as an async generator, so call sites can `async for`.\n        raise NotImplementedError\n        if False:\n            yield cast(LogEvent, None)\n\n    @abstractmethod\n    async def handle_git_request(\n        self,\n        request: Request,\n        project_id: str,\n        deployment_id: str,\n        git_path: str,\n    ) -> Response:\n        \"\"\"Handle a git HTTP request (info/refs, upload-pack, receive-pack).\n\n        Args:\n            request: The incoming HTTP request\n            project_id: The project the deployment belongs to\n            deployment_id: The deployment to serve git for\n            git_path: The git sub-path (e.g. info/refs, git-upload-pack)\n\n        Returns:\n            The HTTP response from the git backend\n        \"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/server/manage_api/_create_deployments_router.py",
    "content": "import logging\nfrom collections.abc import AsyncGenerator\nfrom typing import Awaitable, Callable\n\nfrom fastapi import APIRouter, Depends, HTTPException, Request, Response, params\nfrom fastapi.params import Header, Query\nfrom fastapi.responses import StreamingResponse\nfrom llama_agents.core import schema\nfrom typing_extensions import Annotated\n\nfrom ._abstract_deployments_service import (\n    AbstractDeploymentsService,\n    AbstractPublicDeploymentsService,\n)\nfrom ._exceptions import DeploymentNotFoundError, ReplicaSetNotFoundError\n\nlogger = logging.getLogger(__name__)\n\n\nasync def get_project_id(\n    project_id: Annotated[str | None, Query()] = None,\n    project_id_header: Annotated[str | None, Header(alias=\"project-id\")] = None,\n) -> str:\n    resolved = project_id or project_id_header\n    if not resolved:\n        raise HTTPException(\n            status_code=422,\n            detail=\"project_id is required (query param or project-id header)\",\n        )\n    return resolved\n\n\ndef create_v1beta1_deployments_router(\n    deployments_service: AbstractDeploymentsService,\n    public_service: AbstractPublicDeploymentsService,\n    get_project_id: Callable[..., Awaitable[str]] = get_project_id,\n    dependencies: list[params.Depends] | None = None,\n    public_dependencies: list[params.Depends] | None = None,\n    include_in_schema: bool = True,\n) -> APIRouter:\n    base_router = APIRouter(prefix=\"/api/v1beta1\", include_in_schema=include_in_schema)\n    public_router = APIRouter(\n        tags=[\"v1beta1-deployments-public\"],\n        dependencies=public_dependencies,\n        include_in_schema=include_in_schema,\n    )\n    router = APIRouter(\n        tags=[\"v1beta1-deployments\"],\n        dependencies=dependencies,\n        include_in_schema=include_in_schema,\n    )\n\n    @public_router.get(\"/version\")\n    async def get_version() -> schema.VersionResponse:\n        return await public_service.get_version()\n\n    @router.get(\"/organizations\")\n    async def get_organizations() -> schema.OrganizationsListResponse:\n        \"\"\"Get all organizations\"\"\"\n        return await deployments_service.get_organizations()\n\n    @router.get(\"/list-projects\")\n    async def get_projects(\n        org_id: Annotated[str | None, Query()] = None,\n    ) -> schema.ProjectsListResponse:\n        \"\"\"Get all unique projects with their deployment counts\"\"\"\n        return await deployments_service.get_projects(org_id=org_id)\n\n    @router.post(\"/validate-repository\")\n    async def validate_repository(\n        project_id: Annotated[str, Depends(get_project_id)],\n        request: schema.RepositoryValidationRequest,\n    ) -> schema.RepositoryValidationResponse:\n        \"\"\"Validate repository access and return unified response.\"\"\"\n        return await deployments_service.validate_repository(\n            project_id=project_id,\n            request=request,\n        )\n\n    @router.post(\"\", response_model=schema.DeploymentResponse)\n    async def create_deployment(\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_data: schema.DeploymentCreate,\n    ) -> Response:\n        deployment_response = await deployments_service.create_deployment(\n            project_id=project_id,\n            deployment_data=deployment_data,\n        )\n        # Return deployment response with warning header if there are git issues\n\n        response = Response(\n            content=deployment_response.model_dump_json(),\n            status_code=201,\n            media_type=\"application/json\",\n        )\n        return response\n\n    @router.get(\"\")\n    async def get_deployments(\n        project_id: Annotated[str, Depends(get_project_id)],\n    ) -> schema.DeploymentsListResponse:\n        return await deployments_service.get_deployments(project_id=project_id)\n\n    @router.get(\"/{deployment_id}\")\n    async def get_deployment(\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_id: str,\n        include_events: Annotated[bool, Query()] = False,\n    ) -> schema.DeploymentResponse:\n        try:\n            return await deployments_service.get_deployment(\n                project_id=project_id,\n                deployment_id=deployment_id,\n                include_events=include_events,\n            )\n        except DeploymentNotFoundError as e:\n            raise HTTPException(status_code=404, detail=str(e))\n\n    @router.get(\"/{deployment_id}/history\")\n    async def get_deployment_history(\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_id: str,\n    ) -> schema.DeploymentHistoryResponse:\n        try:\n            return await deployments_service.get_deployment_history(\n                project_id=project_id, deployment_id=deployment_id\n            )\n        except DeploymentNotFoundError as e:\n            raise HTTPException(status_code=404, detail=str(e))\n\n    @router.post(\"/{deployment_id}/rollback\")\n    async def rollback_deployment(\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_id: str,\n        request: schema.RollbackRequest,\n    ) -> schema.DeploymentResponse:\n        try:\n            return await deployments_service.rollback_deployment(\n                project_id=project_id, deployment_id=deployment_id, request=request\n            )\n        except DeploymentNotFoundError as e:\n            raise HTTPException(status_code=404, detail=str(e))\n\n    @router.delete(\"/{deployment_id}\")\n    async def delete_deployment(\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_id: str,\n    ) -> None:\n        try:\n            await deployments_service.delete_deployment(\n                project_id=project_id, deployment_id=deployment_id\n            )\n        except DeploymentNotFoundError as e:\n            raise HTTPException(status_code=404, detail=str(e))\n\n    @router.patch(\"/{deployment_id}\", response_model=schema.DeploymentResponse)\n    async def update_deployment(\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_id: str,\n        update_data: schema.DeploymentUpdate,\n    ) -> Response:\n        \"\"\"Update an existing deployment with patch-style changes\n\n        Args:\n            project_id: The project ID\n            deployment_id: The deployment ID to update\n            update_data: The patch-style update data\n        \"\"\"\n\n        try:\n            deployment_response = await deployments_service.update_deployment(\n                project_id=project_id,\n                deployment_id=deployment_id,\n                update_data=update_data,\n            )\n        except DeploymentNotFoundError as e:\n            raise HTTPException(status_code=404, detail=str(e))\n\n        response = Response(\n            content=deployment_response.model_dump_json(),\n            status_code=200,\n            media_type=\"application/json\",\n        )\n        return response\n\n    @router.get(\"/{deployment_id}/logs\")\n    async def stream_deployment_logs(\n        request: Request,\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_id: str,\n        include_init_containers: Annotated[bool, Query()] = False,\n        since_seconds: Annotated[int | None, Query()] = None,\n        tail_lines: Annotated[int | None, Query()] = None,\n        follow: Annotated[bool, Query()] = True,\n    ) -> StreamingResponse:\n        \"\"\"Stream logs for a deployment.\n\n        Build job logs (if any) are automatically merged before application logs.\n        With ``follow=true`` (default) the stream continues until the latest\n        ReplicaSet changes (e.g. a new rollout occurs). With ``follow=false``\n        the server returns whatever logs are currently available and ends the\n        SSE stream — useful for clients that want a bounded, \"fetch and exit\"\n        response.\n        \"\"\"\n\n        try:\n            inner = deployments_service.stream_deployment_logs(\n                project_id=project_id,\n                deployment_id=deployment_id,\n                include_init_containers=include_init_containers,\n                since_seconds=since_seconds,\n                tail_lines=tail_lines,\n                follow=follow,\n            )\n\n            async def sse_lines() -> AsyncGenerator[str, None]:\n                async for data in inner:\n                    yield \"event: log\\n\"\n                    yield f\"data: {data.model_dump_json()}\\n\\n\"\n\n            return StreamingResponse(\n                sse_lines(),\n                media_type=\"text/event-stream\",\n                headers={\n                    \"Cache-Control\": \"no-cache\",\n                    \"Connection\": \"keep-alive\",\n                    \"X-Accel-Buffering\": \"no\",\n                },\n            )\n\n        except DeploymentNotFoundError as e:\n            raise HTTPException(status_code=404, detail=str(e))\n        except ReplicaSetNotFoundError as e:\n            # Deployment exists but hasn't created a ReplicaSet yet\n            raise HTTPException(status_code=409, detail=str(e))\n\n    @router.get(\"/{deployment_id}/git/{git_path:path}\")\n    @router.post(\"/{deployment_id}/git/{git_path:path}\")\n    async def git_request(\n        request: Request,\n        project_id: Annotated[str, Depends(get_project_id)],\n        deployment_id: str,\n        git_path: str,\n    ) -> Response:\n        \"\"\"Handle git HTTP requests (info/refs, upload-pack, receive-pack).\"\"\"\n        try:\n            return await deployments_service.handle_git_request(\n                request=request,\n                project_id=project_id,\n                deployment_id=deployment_id,\n                git_path=git_path,\n            )\n        except DeploymentNotFoundError as e:\n            raise HTTPException(status_code=404, detail=str(e))\n\n    base_router.include_router(public_router, prefix=\"/deployments-public\")\n    base_router.include_router(router, prefix=\"/deployments\")\n    return base_router\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/server/manage_api/_exceptions.py",
    "content": "class DeploymentNotFoundError(Exception):\n    \"\"\"\n    Raised when a deployment is not found\n    \"\"\"\n\n    pass\n\n\nclass ReplicaSetNotFoundError(Exception):\n    \"\"\"\n    May be raised if a deployment does not set have a replica set\n    \"\"\"\n\n    pass\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_agents/core/ui_build.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom pathlib import Path\n\nfrom .deployment_config import DeploymentConfig\n\n\ndef resolve_ui_root(config_parent: Path, config: DeploymentConfig) -> Path | None:\n    \"\"\"Return the absolute path to the UI root if UI is configured; otherwise None.\"\"\"\n    if config.ui is None:\n        return None\n    return (config_parent / config.ui.directory).resolve()\n\n\ndef ui_build_output_path(config_parent: Path, config: DeploymentConfig) -> Path | None:\n    \"\"\"\n    Determine if the UI has a build script defined in package.json, and where the output will be.\n    Right now, assumes its just `/dist` in the UI root.\n\n    Returns:\n    - Path to the build output directory if a package.json exists and contains a \"build\" script\n    - None if there is no UI configured or no package.json exists\n    \"\"\"\n    ui_root = resolve_ui_root(config_parent, config)\n    if ui_root is None:\n        return None\n    if config.ui is None:\n        return None\n    package_json = ui_root / \"package.json\"\n    if not package_json.exists():\n        return None\n    try:\n        with open(package_json, \"r\", encoding=\"utf-8\") as f:\n            pkg = json.load(f)\n        if not isinstance(pkg, dict):\n            return None\n        scripts = pkg.get(\"scripts\", {}) or {}\n        if config.ui.build_command in scripts:\n            return config.build_output_path()\n        return None\n    except Exception:\n        # Do not raise for malformed package.json in validation contexts\n        return None\n"
  },
  {
    "path": "packages/llama-agents-core/src/llama_deploy/core/__init__.py",
    "content": "# Backwards-compatibility shim: llama_deploy.core -> llama_agents.core\nfrom llama_agents.core._alias import install_alias_finder\n\ninstall_alias_finder()\n\nfrom llama_agents.core import *  # noqa: E402, F403\nfrom llama_agents.core import __all__  # noqa: E402, F401\n"
  },
  {
    "path": "packages/llama-agents-core/tests/client/test_manage_client.py",
    "content": "\"\"\"Tests for client.py configuration and setup\"\"\"\n\nimport json\nfrom collections.abc import AsyncIterator\nfrom datetime import datetime\n\nimport httpx\nimport pytest\nimport pytest_asyncio\nimport respx\nfrom llama_agents.core.client.manage_client import (\n    ControlPlaneClient,\n)\nfrom llama_agents.core.client.manage_client import (\n    ProjectClient as LlamaDeployClient,\n)\nfrom llama_agents.core.schema import LogEvent\nfrom llama_agents.core.schema.deployments import DeploymentCreate\n\n\n@pytest_asyncio.fixture\nasync def client() -> AsyncIterator[LlamaDeployClient]:\n    \"\"\"Create a client with mocked config\"\"\"\n    c = LlamaDeployClient(base_url=\"http://localhost:8000\", project_id=\"test-project\")\n    try:\n        yield c\n    finally:\n        await c.aclose()\n\n\n@pytest.mark.asyncio\nasync def test_client_initialization() -> None:\n    \"\"\"Test client initialization with config\"\"\"\n    client = LlamaDeployClient(\n        base_url=\"http://localhost:8000\", project_id=\"test-project\"\n    )\n    assert client.base_url == \"http://localhost:8000\"\n    assert client.project_id == \"test-project\"\n    await client.aclose()\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_server_version_includes_new_fields() -> None:\n    async with ControlPlaneClient.ctx(base_url=\"http://localhost:8000\") as client:\n        respx.get(\"http://localhost:8000/api/v1beta1/deployments-public/version\").mock(\n            return_value=httpx.Response(\n                200,\n                json={\n                    \"version\": \"1.2.3\",\n                    \"requires_auth\": False,\n                    \"min_llamactl_version\": \"0.3.0a13\",\n                },\n            )\n        )\n\n        result = await client.server_version()\n        assert result.version == \"1.2.3\"\n        assert result.requires_auth is False\n        assert result.min_llamactl_version == \"0.3.0a13\"\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_server_version_defaults_when_min_missing() -> None:\n    async with ControlPlaneClient.ctx(base_url=\"http://localhost:8000\") as client:\n        respx.get(\"http://localhost:8000/api/v1beta1/deployments-public/version\").mock(\n            return_value=httpx.Response(\n                200,\n                json={\n                    \"version\": \"1.2.3\",\n                    \"requires_auth\": True,\n                },\n            )\n        )\n\n        result = await client.server_version()\n        assert result.version == \"1.2.3\"\n        assert result.requires_auth is True\n        assert result.min_llamactl_version is None\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_successful_request(client: LlamaDeployClient) -> None:\n    \"\"\"Test successful HTTP request\"\"\"\n    respx.get(\"http://localhost:8000/test\").mock(\n        return_value=httpx.Response(200, json={\"result\": \"success\"})\n    )\n\n    result = await client.client.get(\"/test\")\n\n    assert result.status_code == 200\n    assert result.json() == {\"result\": \"success\"}\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_request_with_json_data(client: LlamaDeployClient) -> None:\n    \"\"\"Test HTTP request with JSON data\"\"\"\n    respx.post(\"http://localhost:8000/test\").mock(\n        return_value=httpx.Response(200, json={\"result\": \"success\"})\n    )\n\n    data = {\"key\": \"value\"}\n    result = await client.client.post(\"/test\", json=data)\n\n    assert result.status_code == 200\n    assert result.json() == {\"result\": \"success\"}\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_get_deployments(client: LlamaDeployClient) -> None:\n    \"\"\"Test get_deployments method\"\"\"\n    respx.get(\n        \"http://localhost:8000/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n    ).mock(\n        return_value=httpx.Response(\n            200,\n            json={\n                \"deployments\": [\n                    {\n                        \"id\": \"test-deploy-1\",\n                        \"name\": \"Test Deploy 1\",\n                        \"project_id\": \"test-project\",\n                        \"repo_url\": \"https://github.com/test/repo1.git\",\n                        \"git_ref\": \"main\",\n                        \"status\": \"Running\",\n                        \"has_personal_access_token\": False,\n                        \"secret_names\": None,\n                        \"apiserver_url\": None,\n                        \"deployment_file_path\": \"deploy.yml\",\n                    }\n                ]\n            },\n        )\n    )\n\n    result = await client.list_deployments()\n\n    assert len(result) == 1\n    assert result[0].display_name == \"Test Deploy 1\"\n    assert result[0].git_ref == \"main\"\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_get_deployment(client: LlamaDeployClient) -> None:\n    \"\"\"Test get_deployment method\"\"\"\n    respx.get(\n        \"http://localhost:8000/api/v1beta1/deployments/test-deploy-1\",\n        params={\"project_id\": \"test-project\"},\n    ).mock(\n        return_value=httpx.Response(\n            200,\n            json={\n                \"id\": \"test-deploy-1\",\n                \"name\": \"Test Deploy 1\",\n                \"project_id\": \"test-project\",\n                \"repo_url\": \"https://github.com/test/repo1.git\",\n                \"git_ref\": \"main\",\n                \"status\": \"Running\",\n                \"has_personal_access_token\": False,\n                \"secret_names\": None,\n                \"apiserver_url\": None,\n                \"deployment_file_path\": \"deploy.yml\",\n            },\n        )\n    )\n\n    result = await client.get_deployment(\"test-deploy-1\")\n\n    assert result.display_name == \"Test Deploy 1\"\n    assert result.git_ref == \"main\"\n    assert result.repo_url == \"https://github.com/test/repo1.git\"\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_create_deployment_basic(client: LlamaDeployClient) -> None:\n    \"\"\"Test create_deployment without git_ref\"\"\"\n    route = respx.post(\n        \"http://localhost:8000/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n    ).mock(\n        return_value=httpx.Response(\n            201,\n            json={\n                \"id\": \"new-deploy-123\",\n                \"name\": \"New Deploy\",\n                \"project_id\": \"test-project\",\n                \"repo_url\": \"https://github.com/test/repo.git\",\n                \"git_ref\": \"\",\n                \"deployment_file_path\": \"deploy.yml\",\n                \"status\": \"Pending\",\n                \"has_personal_access_token\": False,\n                \"secret_names\": None,\n                \"apiserver_url\": None,\n            },\n        )\n    )\n\n    result = await client.create_deployment(\n        DeploymentCreate(\n            display_name=\"New Deploy\",\n            repo_url=\"https://github.com/test/repo.git\",\n            deployment_file_path=\"deploy.yml\",\n        )\n    )\n\n    assert result.display_name == \"New Deploy\"\n    assert result.git_ref == \"\"\n\n    # Verify request was made with correct data\n    assert route.called\n    request = route.calls.last.request\n    request_data = (request.content or b\"\").decode()\n    parsed_data = json.loads(request_data)\n    assert parsed_data[\"display_name\"] == \"New Deploy\"\n    assert parsed_data[\"name\"] == \"New Deploy\"  # backwards compat with old servers\n    assert parsed_data[\"repo_url\"] == \"https://github.com/test/repo.git\"\n    assert parsed_data[\"deployment_file_path\"] == \"deploy.yml\"\n    assert \"git_ref\" not in parsed_data  # Should be excluded when None\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_create_deployment_with_git_ref(client: LlamaDeployClient) -> None:\n    \"\"\"Test create_deployment with git_ref parameter\"\"\"\n    route = respx.post(\n        \"http://localhost:8000/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n    ).mock(\n        return_value=httpx.Response(\n            201,\n            json={\n                \"id\": \"new-deploy-456\",\n                \"name\": \"New Deploy with Ref\",\n                \"project_id\": \"test-project\",\n                \"repo_url\": \"https://github.com/test/repo.git\",\n                \"git_ref\": \"feature-branch\",\n                \"deployment_file_path\": \"deploy.yml\",\n                \"status\": \"Pending\",\n                \"has_personal_access_token\": False,\n                \"secret_names\": None,\n                \"apiserver_url\": None,\n            },\n        )\n    )\n\n    result = await client.create_deployment(\n        DeploymentCreate(\n            display_name=\"New Deploy with Ref\",\n            repo_url=\"https://github.com/test/repo.git\",\n            git_ref=\"feature-branch\",\n            deployment_file_path=\"deploy.yml\",\n        )\n    )\n\n    assert result.display_name == \"New Deploy with Ref\"\n    assert result.git_ref == \"feature-branch\"\n\n    # Verify request was made with correct data including git_ref\n    assert route.called\n    request = route.calls.last.request\n    request_data = (request.content or b\"\").decode()\n    parsed_data = json.loads(request_data)\n    assert parsed_data[\"display_name\"] == \"New Deploy with Ref\"\n    assert parsed_data[\"repo_url\"] == \"https://github.com/test/repo.git\"\n    assert parsed_data[\"git_ref\"] == \"feature-branch\"\n    assert parsed_data[\"deployment_file_path\"] == \"deploy.yml\"\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_create_deployment_with_all_params(client: LlamaDeployClient) -> None:\n    \"\"\"Test create_deployment with all parameters including git_ref\"\"\"\n    route = respx.post(\n        \"http://localhost:8000/api/v1beta1/deployments\",\n        params={\"project_id\": \"test-project\"},\n    ).mock(\n        return_value=httpx.Response(\n            201,\n            json={\n                \"id\": \"new-deploy-789\",\n                \"name\": \"Full Deploy\",\n                \"project_id\": \"test-project\",\n                \"repo_url\": \"https://github.com/test/repo.git\",\n                \"git_ref\": \"v1.0.0\",\n                \"deployment_file_path\": \"custom_deploy.yml\",\n                \"status\": \"Pending\",\n                \"has_personal_access_token\": True,\n                \"secret_names\": [\"API_KEY\", \"DATABASE_URL\"],\n                \"apiserver_url\": None,\n            },\n        )\n    )\n\n    result = await client.create_deployment(\n        DeploymentCreate(\n            display_name=\"Full Deploy\",\n            repo_url=\"https://github.com/test/repo.git\",\n            git_ref=\"v1.0.0\",\n            deployment_file_path=\"custom_deploy.yml\",\n            personal_access_token=\"ghp_token123\",\n            secrets={\"API_KEY\": \"secret1\", \"DATABASE_URL\": \"secret2\"},\n        )\n    )\n\n    assert result.display_name == \"Full Deploy\"\n    assert result.git_ref == \"v1.0.0\"\n    assert result.has_personal_access_token is True\n    assert result.secret_names == [\"API_KEY\", \"DATABASE_URL\"]\n\n    # Verify request was made with all data\n    assert route.called\n    request = route.calls.last.request\n    request_data = (request.content or b\"\").decode()\n    parsed_data = json.loads(request_data)\n    assert parsed_data[\"display_name\"] == \"Full Deploy\"\n    assert parsed_data[\"repo_url\"] == \"https://github.com/test/repo.git\"\n    assert parsed_data[\"git_ref\"] == \"v1.0.0\"\n    assert parsed_data[\"deployment_file_path\"] == \"custom_deploy.yml\"\n    assert parsed_data[\"personal_access_token\"] == \"ghp_token123\"\n    assert parsed_data[\"secrets\"] == {\"API_KEY\": \"secret1\", \"DATABASE_URL\": \"secret2\"}\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_delete_deployment(client: LlamaDeployClient) -> None:\n    \"\"\"Test delete_deployment method\"\"\"\n    route = respx.delete(\n        \"http://localhost:8000/api/v1beta1/deployments/test-deploy-1\",\n        params={\"project_id\": \"test-project\"},\n    ).mock(return_value=httpx.Response(200))\n\n    await client.delete_deployment(\"test-deploy-1\")\n\n    assert route.called\n\n\ndef _sse_bytes(*events: tuple[str, str]) -> bytes:\n    # Build SSE payload: sequence of (event, data_json)\n    parts = []\n    for event, data in events:\n        parts.append(f\"event: {event}\\n\".encode())\n        parts.append(f\"data: {data}\\n\".encode())\n        parts.append(b\"\\n\")\n    return b\"\".join(parts)\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_stream_deployment_logs_parses_log_events(\n    client: LlamaDeployClient,\n) -> None:\n    \"\"\"Stream SSE and parse LogEvent items.\"\"\"\n    payload = _sse_bytes(\n        (\n            \"log\",\n            LogEvent(\n                pod=\"p1\",\n                container=\"c1\",\n                text=\"hello\",\n                timestamp=datetime.now(),\n            ).model_dump_json(),\n        ),\n        (\n            \"log\",\n            LogEvent(\n                pod=\"p2\",\n                container=\"c2\",\n                text=\"world\",\n                timestamp=datetime.now(),\n            ).model_dump_json(),\n        ),\n    )\n\n    route = respx.get(\n        \"http://localhost:8000/api/v1beta1/deployments/dep-123/logs\",\n    ).mock(\n        return_value=httpx.Response(\n            200, content=payload, headers={\"Content-Type\": \"text/event-stream\"}\n        )\n    )\n\n    events = [e async for e in client.stream_deployment_logs(\"dep-123\")]\n    assert len(events) == 2\n    assert (events[0].pod, events[0].container, events[0].text) == (\"p1\", \"c1\", \"hello\")\n    assert (events[1].pod, events[1].container, events[1].text) == (\"p2\", \"c2\", \"world\")\n\n    assert route.called\n    req = route.calls.last.request\n    # Query params\n    assert req.url.params.get(\"project_id\") == \"test-project\"\n    assert req.url.params.get(\"include_init_containers\") in (\"False\", \"false\")\n    # Headers\n    assert req.headers.get(\"accept\") == \"text/event-stream\"\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_stream_deployment_logs_passes_optional_params(\n    client: LlamaDeployClient,\n) -> None:\n    payload = _sse_bytes(\n        (\n            \"log\",\n            LogEvent(\n                pod=\"p\", container=\"c\", text=\"t\", timestamp=datetime.now()\n            ).model_dump_json(),\n        )\n    )\n\n    route = respx.get(\n        \"http://localhost:8000/api/v1beta1/deployments/dep-1/logs\",\n    ).mock(\n        return_value=httpx.Response(\n            200, content=payload, headers={\"Content-Type\": \"text/event-stream\"}\n        )\n    )\n\n    events = [\n        e\n        async for e in client.stream_deployment_logs(\n            \"dep-1\", include_init_containers=True, since_seconds=120, tail_lines=250\n        )\n    ]\n    assert len(events) == 1\n    assert route.called\n    req = route.calls.last.request\n    assert req.url.params.get(\"project_id\") == \"test-project\"\n    assert req.url.params.get(\"include_init_containers\") in (\"True\", \"true\")\n    assert req.url.params.get(\"since_seconds\") == \"120\"\n    assert req.url.params.get(\"tail_lines\") == \"250\"\n\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_stream_deployment_logs_ignores_non_log_events(\n    client: LlamaDeployClient,\n) -> None:\n    payload = _sse_bytes(\n        (\"ping\", \"{}\"),\n        (\n            \"log\",\n            LogEvent(\n                pod=\"p\", container=\"c\", text=\"ok\", timestamp=datetime.now()\n            ).model_dump_json(),\n        ),\n    )\n\n    route = respx.get(\n        \"http://localhost:8000/api/v1beta1/deployments/dep-x/logs\",\n    ).mock(\n        return_value=httpx.Response(\n            200, content=payload, headers={\"Content-Type\": \"text/event-stream\"}\n        )\n    )\n\n    events = [e async for e in client.stream_deployment_logs(\"dep-x\")]\n    assert len(events) == 1\n    assert (events[0].pod, events[0].container, events[0].text) == (\"p\", \"c\", \"ok\")\n    assert route.called\n"
  },
  {
    "path": "packages/llama-agents-core/tests/test_deployment_config.py",
    "content": "import json\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.core.deployment_config import read_deployment_config\n\n\ndef write_file(path: Path, content: str) -> None:\n    path.parent.mkdir(parents=True, exist_ok=True)\n    path.write_text(content, encoding=\"utf-8\")\n\n\ndef test_pyproject_tool_llamadeploy_name_fallback(tmp_path: Path) -> None:\n    # Arrange: project.name should backfill tool.llamadeploy.name if missing\n    pyproject = \"\"\"\n    [project]\n    name = \"myproj\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"ui\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert\n    assert cfg.name == \"myproj\"\n    assert cfg.ui is not None and cfg.ui.directory == \"ui\"\n\n\ndef test_relative_ui_directory_within_root_is_ok(tmp_path: Path) -> None:\n    # Arrange\n    pyproject = \"\"\"\n    [project]\n    name = \"okrel\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"./frontend/ui\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert: path resolves within root\n    assert cfg.ui is not None\n    resolved = (tmp_path / cfg.ui.directory).resolve()\n    assert str(resolved).startswith(str(tmp_path.resolve()))\n\n\ndef test_pyproject_when_config_path_points_to_file(tmp_path: Path) -> None:\n    # Arrange: Put pyproject in a nested folder and call with the file path\n    nested = tmp_path / \"a\" / \"b\"\n    nested.mkdir(parents=True)\n    pyproject = \"\"\"\n    [project]\n    name = \"filepoint\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"ui\"\n    \"\"\"\n    write_file(nested / \"pyproject.toml\", pyproject)\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\"a/b/pyproject.toml\"))\n\n    # Assert\n    assert cfg.name == \"filepoint\"\n    assert cfg.ui is not None and cfg.ui.directory == \"ui\"\n\n\ndef test_nonexistent_config_dir_returns_defaults(tmp_path: Path) -> None:\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\"does-not-exist\"))\n\n    # Assert\n    assert cfg.name == \"default\"\n    assert cfg.ui is None\n\n\ndef test_llamadeploy_toml_parses_minimal_and_sets_ui(tmp_path: Path) -> None:\n    # Arrange: local TOML config\n    toml_content = \"\"\"\n    name = \"ldapp\"\n\n    [ui]\n    directory = \"web\"\n    build_output_dir = \"dist\"\n    package_manager = \"pnpm\"\n    build_command = \"build\"\n    serve_command = \"preview\"\n    proxy_port = 4503\n    \"\"\"\n    write_file(tmp_path / \"llama_deploy.toml\", toml_content)\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert\n    assert cfg.name == \"ldapp\"\n    assert cfg.ui is not None\n    assert cfg.ui.directory == \"web\"\n    assert cfg.ui.build_output_dir == \"dist\"\n    assert cfg.ui.package_manager == \"pnpm\"\n    assert cfg.ui.serve_command == \"preview\"\n    assert cfg.ui.proxy_port == 4503\n\n\ndef test_legacy_yaml_only_merging_into_new_config(tmp_path: Path) -> None:\n    # Arrange legacy YAML config\n    yaml_content = \"\"\"\n    name: legacy\n    services:\n      svc_one:\n        import_path: src/app.module:run\n        env:\n          A: \"1\"\n        env_files: [\".env\", \".env.local\"]\n      ui:\n        source:\n          location: ui_legacy\n    \"\"\"\n    write_file(tmp_path / \"llama_deploy.yaml\", yaml_content)\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert (intended): legacy values should be reflected\n    # Current implementation likely does not merge legacy due to a bug; add expectations anyway.\n    assert cfg.name in {\"legacy\", \"default\"}\n    # If legacy honored, ui directory should be from legacy\n    if cfg.ui is not None:\n        assert cfg.ui.directory == \"ui_legacy\"\n\n\ndef test_yaml_and_pyproject_merge_precedence(tmp_path: Path) -> None:\n    # Arrange legacy YAML + pyproject with conflicting name and UI\n    yaml_content = \"\"\"\n    name: legacy_name\n    services:\n      svc:\n        import_path: src/mod:func\n    ui:\n      source:\n        location: legacy_ui\n    \"\"\"\n    write_file(tmp_path / \"llama_deploy.yaml\", yaml_content)\n\n    pyproject = \"\"\"\n    [project]\n    name = \"new_name\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"new_ui\"\n    build_output_dir = \"new_dist\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert precedence: new config should take effect where overlapping\n    assert cfg.name in {\"new_name\", \"legacy_name\"}\n    if cfg.ui is not None:\n        assert cfg.ui.directory in {\"new_ui\", \"legacy_ui\"}\n        if cfg.ui.directory == \"new_ui\":\n            assert cfg.ui.build_output_dir == \"new_dist\"\n\n\ndef test_package_json_overrides_ui_fields(tmp_path: Path) -> None:\n    # Arrange: pyproject defines UI; package.json should override some fields\n    pyproject = \"\"\"\n    [project]\n    name = \"pkgmerge\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"ui\"\n    package_manager = \"npm\"\n    build_command = \"build\"\n    serve_command = \"serve\"\n    proxy_port = 4502\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    pkg_json = {\n        \"llamadeploy\": {\n            \"build_output_dir\": \"frontend-build\",\n            \"package_manager\": \"yarn\",\n            \"build_command\": \"custom-build\",\n            \"serve_command\": \"preview\",\n            \"proxy_port\": 4510,\n        }\n    }\n    (tmp_path / \"ui\").mkdir(parents=True, exist_ok=True)\n    (tmp_path / \"ui\" / \"package.json\").write_text(\n        json.dumps(pkg_json), encoding=\"utf-8\"\n    )\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert: package.json values override where provided\n    assert cfg is not None and cfg.ui is not None\n    assert cfg.ui.package_manager == \"yarn\"\n    assert cfg.ui.build_command == \"custom-build\"\n    assert cfg.ui.serve_command == \"preview\"\n    assert cfg.ui.proxy_port == 4510\n    # build_output_dir resolved relative to UI directory\n    assert cfg.ui.build_output_dir is not None\n    assert Path(cfg.ui.build_output_dir) == Path(\"ui/frontend-build\")\n\n\ndef test_package_json_package_manager_fallback_with_llamadeploy(tmp_path: Path) -> None:\n    # Arrange: pyproject has UI, package.json has packageManager and llamadeploy without package_manager\n    pyproject = \"\"\"\n    [project]\n    name = \"pm-fallback-ld\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"ui\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    pkg_json = {\n        \"packageManager\": \"pnpm@9.1.0\",\n        \"llamadeploy\": {\n            # no package_manager here on purpose\n        },\n    }\n    (tmp_path / \"ui\").mkdir(parents=True, exist_ok=True)\n    (tmp_path / \"ui\" / \"package.json\").write_text(\n        json.dumps(pkg_json), encoding=\"utf-8\"\n    )\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert: fallback applied from packageManager\n    assert cfg.ui is not None\n    assert cfg.ui.package_manager == \"pnpm\"\n\n\ndef test_package_json_package_manager_fallback_without_llamadeploy(\n    tmp_path: Path,\n) -> None:\n    # Arrange: pyproject has UI, package.json has only packageManager\n    pyproject = \"\"\"\n    [project]\n    name = \"pm-fallback-no-ld\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"ui\"\n    package_manager = \"npm\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    pkg_json = {\n        \"packageManager\": \"yarn@4.3.0\",\n        # no llamadeploy block\n    }\n    (tmp_path / \"ui\").mkdir(parents=True, exist_ok=True)\n    (tmp_path / \"ui\" / \"package.json\").write_text(\n        json.dumps(pkg_json), encoding=\"utf-8\"\n    )\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    # Assert: fallback applied from packageManager when no explicit package_manager in UI\n    assert cfg.ui is not None\n    assert cfg.ui.package_manager == \"yarn\"\n\n\ndef test_ui_directory_can_be_above_pyproject_dir_but_within_source_root(\n    tmp_path: Path,\n) -> None:\n    # Arrange: pyproject in nested dir, UI one level up but still inside source_root\n    project_dir = tmp_path / \"project\"\n    sub_dir = project_dir / \"sub\"\n    sub_dir.mkdir(parents=True)\n\n    pyproject = \"\"\"\n    [project]\n    name = \"nested\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"../ui\"\n    \"\"\"\n    write_file(sub_dir / \"pyproject.toml\", pyproject)\n\n    # Act\n    cfg = read_deployment_config(tmp_path, Path(\"project/sub\"))\n\n    # Assert: desired behavior is that ../ui resolves to project/ui (still under tmp_path)\n    # This asserts the intended constraint; if implementation only stores the raw string, this may need adjustment.\n    assert cfg.ui is not None\n    intended_resolved = (project_dir / \"ui\").resolve()\n    # The function returns a string path; normalize relative to sub_dir like the spec implies\n    observed = (sub_dir / cfg.ui.directory).resolve()\n    assert str(observed) == str(intended_resolved)\n\n\ndef test_ui_directory_cannot_escape_source_root(tmp_path: Path) -> None:\n    # Arrange: point UI outside the declared source_root\n    pyproject = \"\"\"\n    [project]\n    name = \"escape\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"../../outside\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    # Act & Assert: desired behavior is to raise when UI leaves source_root\n    with pytest.raises((ValueError, AssertionError, RuntimeError)):\n        _ = read_deployment_config(tmp_path, Path(\".\"))\n\n\ndef test_llama_agents_toml_is_preferred_over_llama_deploy_toml(\n    tmp_path: Path,\n) -> None:\n    \"\"\"When both llama_agents.toml and llama_deploy.toml exist, the new name wins.\"\"\"\n    write_file(\n        tmp_path / \"llama_agents.toml\",\n        'name = \"from-agents\"\\napp = \"mod:app\"\\n',\n    )\n    write_file(\n        tmp_path / \"llama_deploy.toml\",\n        'name = \"from-deploy\"\\napp = \"mod:app\"\\n',\n    )\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.name == \"from-agents\"\n\n\ndef test_llama_agents_toml_works_alone(tmp_path: Path) -> None:\n    \"\"\"llama_agents.toml is discovered when llama_deploy.toml does not exist.\"\"\"\n    write_file(\n        tmp_path / \"llama_agents.toml\",\n        'name = \"agents-only\"\\napp = \"mod:app\"\\n',\n    )\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.name == \"agents-only\"\n\n\ndef test_pyproject_tool_llamaagents_is_preferred_over_llamadeploy(\n    tmp_path: Path,\n) -> None:\n    \"\"\"When pyproject.toml has both [tool.llamaagents] and [tool.llamadeploy],\n    the new name wins.\"\"\"\n    pyproject = \"\"\"\n    [project]\n    name = \"dual\"\n\n    [tool.llamaagents]\n    app = \"mod:new_app\"\n\n    [tool.llamadeploy]\n    app = \"mod:old_app\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.app == \"mod:new_app\"\n\n\ndef test_pyproject_tool_llamaagents_works_alone(tmp_path: Path) -> None:\n    \"\"\"[tool.llamaagents] is recognized when [tool.llamadeploy] is absent.\"\"\"\n    pyproject = \"\"\"\n    [project]\n    name = \"agents-proj\"\n\n    [tool.llamaagents]\n    app = \"mod:app\"\n\n    [tool.llamaagents.ui]\n    directory = \"frontend\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.name == \"agents-proj\"\n    assert cfg.app == \"mod:app\"\n    assert cfg.ui is not None and cfg.ui.directory == \"frontend\"\n\n\ndef test_llama_agents_yaml_is_preferred_over_llama_deploy_yaml(\n    tmp_path: Path,\n) -> None:\n    \"\"\"When both yaml files exist, llama_agents.yaml wins.\"\"\"\n    write_file(\n        tmp_path / \"llama_agents.yaml\",\n        \"name: from-agents\\nservices:\\n  svc:\\n    import_path: mod:func\\n\",\n    )\n    write_file(\n        tmp_path / \"llama_deploy.yaml\",\n        \"name: from-deploy\\nservices:\\n  svc:\\n    import_path: mod:func\\n\",\n    )\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.name == \"from-agents\"\n\n\ndef test_llama_agents_yaml_works_alone(tmp_path: Path) -> None:\n    \"\"\"llama_agents.yaml is discovered when llama_deploy.yaml does not exist.\"\"\"\n    write_file(\n        tmp_path / \"llama_agents.yaml\",\n        \"name: agents-only\\nservices:\\n  svc:\\n    import_path: mod:func\\n\",\n    )\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.name == \"agents-only\"\n\n\ndef test_package_json_llamaagents_key_is_preferred_over_llamadeploy(\n    tmp_path: Path,\n) -> None:\n    \"\"\"When package.json has both llamaagents and llamadeploy keys,\n    the new name wins.\"\"\"\n    pyproject = \"\"\"\n    [project]\n    name = \"pkg-dual\"\n\n    [tool.llamaagents]\n    [tool.llamaagents.ui]\n    directory = \"ui\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    pkg_json = {\n        \"llamaagents\": {\"build_command\": \"new-build\"},\n        \"llamadeploy\": {\"build_command\": \"old-build\"},\n    }\n    (tmp_path / \"ui\").mkdir(parents=True, exist_ok=True)\n    (tmp_path / \"ui\" / \"package.json\").write_text(\n        json.dumps(pkg_json), encoding=\"utf-8\"\n    )\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.ui is not None\n    assert cfg.ui.build_command == \"new-build\"\n\n\ndef test_package_json_llamaagents_key_works_alone(tmp_path: Path) -> None:\n    \"\"\"The llamaagents key in package.json is recognized when llamadeploy is absent.\"\"\"\n    pyproject = \"\"\"\n    [project]\n    name = \"pkg-agents\"\n\n    [tool.llamaagents]\n    [tool.llamaagents.ui]\n    directory = \"ui\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    pkg_json = {\n        \"llamaagents\": {\n            \"package_manager\": \"pnpm\",\n            \"build_command\": \"agents-build\",\n        }\n    }\n    (tmp_path / \"ui\").mkdir(parents=True, exist_ok=True)\n    (tmp_path / \"ui\" / \"package.json\").write_text(\n        json.dumps(pkg_json), encoding=\"utf-8\"\n    )\n\n    cfg = read_deployment_config(tmp_path, Path(\".\"))\n\n    assert cfg.ui is not None\n    assert cfg.ui.package_manager == \"pnpm\"\n    assert cfg.ui.build_command == \"agents-build\"\n\n\ndef test_absolute_ui_directory_outside_source_root_is_rejected(tmp_path: Path) -> None:\n    # Arrange: absolute path outside root\n    outside = tmp_path.parent / \"outside-ui\"\n    pyproject = f\"\"\"\n    [project]\n    name = \"absescape\"\n\n    [tool.llamadeploy]\n    [tool.llamadeploy.ui]\n    directory = \"{outside}\"\n    \"\"\"\n    write_file(tmp_path / \"pyproject.toml\", pyproject)\n\n    # Act & Assert: expected to raise\n    with pytest.raises((ValueError, AssertionError, RuntimeError)):\n        _ = read_deployment_config(tmp_path, Path(\".\"))\n"
  },
  {
    "path": "packages/llama-agents-core/tests/test_git_util.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport socket\nimport tempfile\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nfrom dulwich import porcelain\nfrom dulwich.errors import HangupException\nfrom dulwich.refs import Ref\nfrom dulwich.repo import Repo\nfrom llama_agents.core.git.git_util import (\n    GitAccessError,\n    GitCloneResult,\n    _checkout_ref,\n    clone_repo,\n    clone_repo_sync,\n    get_commit_sha_for_ref,\n    get_unpushed_commits_count,\n    is_git_repo,\n    parse_github_repo_url,\n    validate_git_public_access,\n    validate_git_url,\n    validate_git_url_no_ssrf,\n    working_tree_has_changes,\n)\n\nGIT_UTIL = \"llama_agents.core.git.git_util\"\n\n\ndef _make_fake_repo(head_sha: str = \"abc123def456\" * 3 + \"0000\") -> MagicMock:\n    \"\"\"Build a fake dulwich Repo whose ``.head()`` returns ``head_sha``.\"\"\"\n    fake_repo = MagicMock()\n    fake_repo.head.return_value = head_sha.encode()\n    fake_repo.refs.read_ref.return_value = b\"ref: refs/heads/main\"\n    fake_repo.close = MagicMock()\n    return fake_repo\n\n\ndef _make_clone_source(path: Path) -> tuple[str, str, str]:\n    \"\"\"Create a git repo with two commits and a v1.0 tag on the first.\n\n    Returns (first_sha, second_sha, default_branch_name).\n    \"\"\"\n    porcelain.init(str(path))\n    (path / \"f.txt\").write_text(\"hello\")\n    porcelain.add(str(path), [str(path / \"f.txt\")])\n    sha1 = porcelain.commit(\n        str(path),\n        message=b\"first\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n    repo = Repo(str(path))\n    repo.refs[Ref(b\"refs/tags/v1.0\")] = sha1  # type: ignore[index]  # ty: ignore[invalid-assignment]\n    head_ref = repo.refs.read_ref(Ref(b\"HEAD\"))\n    assert head_ref is not None\n    branch = head_ref.removeprefix(b\"ref: refs/heads/\").decode()\n    repo.close()\n\n    (path / \"f.txt\").write_text(\"updated\")\n    porcelain.add(str(path), [str(path / \"f.txt\")])\n    sha2 = porcelain.commit(\n        str(path),\n        message=b\"second\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n    return sha1.decode(), sha2.decode(), branch\n\n\ndef test_parse_github_repo_url() -> None:\n    \"\"\"Test GitHub URL parsing with various formats.\"\"\"\n    # Standard HTTPS format\n    assert parse_github_repo_url(\"https://github.com/owner/repo\") == (\"owner\", \"repo\")\n\n    # HTTPS with .git suffix\n    assert parse_github_repo_url(\"https://github.com/owner/repo.git\") == (\n        \"owner\",\n        \"repo\",\n    )\n\n    # SSH format\n    assert parse_github_repo_url(\"git@github.com:owner/repo\") == (\"owner\", \"repo\")\n    assert parse_github_repo_url(\"git@github.com:owner/repo.git\") == (\"owner\", \"repo\")\n\n    # Without protocol prefix\n    assert parse_github_repo_url(\"github.com/owner/repo\") == (\"owner\", \"repo\")\n\n    # With trailing slashes\n    assert parse_github_repo_url(\"https://github.com/owner/repo/\") == (\"owner\", \"repo\")\n    assert parse_github_repo_url(\"https://github.com/owner/repo.git/\") == (\n        \"owner\",\n        \"repo\",\n    )\n\n    # Edge cases that should fail\n    with pytest.raises(ValueError, match=\"Could not parse GitHub repository URL\"):\n        parse_github_repo_url(\"not-a-github-url\")\n\n    with pytest.raises(ValueError):\n        parse_github_repo_url(\"https://gitlab.com/owner/repo\")\n\n    with pytest.raises(ValueError):\n        parse_github_repo_url(\"https://github.com/\")\n\n    with pytest.raises(ValueError):\n        parse_github_repo_url(\"https://github.com/owner\")\n\n\ndef test_clone_repo_branch_success(tmp_path: Path) -> None:\n    \"\"\"clone_repo with a branch ref returns the resolved SHA and ref.\"\"\"\n    src = tmp_path / \"source\"\n    sha1, sha2, branch = _make_clone_source(src)\n\n    with patch(f\"{GIT_UTIL}.validate_git_url\"):\n        result = clone_repo_sync(str(src), branch, dest_dir=tmp_path / \"dest\")\n\n    assert result == GitCloneResult(git_sha=sha2, git_ref=branch)\n\n\ndef test_clone_repo_no_ref(tmp_path: Path) -> None:\n    \"\"\"clone_repo with no ref resolves the remote default branch.\"\"\"\n    src = tmp_path / \"source\"\n    sha1, sha2, branch = _make_clone_source(src)\n\n    with patch(f\"{GIT_UTIL}.validate_git_url\"):\n        result = clone_repo_sync(str(src), dest_dir=tmp_path / \"dest\")\n\n    assert result == GitCloneResult(git_sha=sha2, git_ref=branch)\n\n\n@patch(f\"{GIT_UTIL}.porcelain.clone\")\n@pytest.mark.asyncio\nasync def test_clone_repo_no_ref_detached_resolves_tag(mock_clone: MagicMock) -> None:\n    \"\"\"A detached HEAD that matches a tag returns the tag name as the ref.\"\"\"\n    fake_repo = MagicMock()\n    head_sha = b\"c\" * 40\n    fake_repo.head.return_value = head_sha\n    # Symbolic HEAD reads as the SHA itself when detached\n    fake_repo.refs.read_ref.return_value = head_sha\n    fake_repo.refs.__getitem__.return_value = head_sha\n    fake_repo.refs.subkeys.return_value = [b\"v1.2.3\"]\n    mock_clone.return_value = fake_repo\n\n    with tempfile.TemporaryDirectory() as t:\n        result = await clone_repo(\n            \"https://github.com/user/repo.git\", dest_dir=Path(t) / \"sub\"\n        )\n\n    assert result == GitCloneResult(git_sha=head_sha.decode(), git_ref=\"v1.2.3\")\n\n\ndef test_clone_repo_full_sha(tmp_path: Path) -> None:\n    \"\"\"A 40-char SHA is fetched as the default ref then checked out.\"\"\"\n    src = tmp_path / \"source\"\n    sha1, sha2, branch = _make_clone_source(src)\n\n    with patch(f\"{GIT_UTIL}.validate_git_url\"):\n        result = clone_repo_sync(str(src), git_ref=sha1, dest_dir=tmp_path / \"dest\")\n\n    assert result.git_sha == sha1\n    assert result.git_ref == sha1\n\n\ndef test_clone_repo_explicit_git_sha_preserves_ref_metadata(tmp_path: Path) -> None:\n    \"\"\"An explicit SHA checkout keeps the symbolic ref as metadata.\"\"\"\n    src = tmp_path / \"source\"\n    sha1, sha2, branch = _make_clone_source(src)\n\n    with patch(f\"{GIT_UTIL}.validate_git_url\"):\n        result = clone_repo_sync(\n            str(src),\n            git_ref=\"my-feature\",\n            git_sha=sha1,\n            dest_dir=tmp_path / \"dest\",\n            depth=1,\n        )\n\n    assert result == GitCloneResult(git_sha=sha1, git_ref=\"my-feature\")\n\n\n@patch(f\"{GIT_UTIL}.porcelain.clone\")\n@pytest.mark.asyncio\nasync def test_clone_repo_explicit_git_sha_does_not_require_initial_head(\n    mock_clone: MagicMock,\n) -> None:\n    \"\"\"Explicit SHA checkout should work even if the cloned repo has no HEAD yet.\"\"\"\n    sha = \"deadbeef\" * 5  # 40 chars\n    fake_repo = MagicMock()\n    fake_repo.head.side_effect = KeyError(b\"HEAD\")\n    mock_clone.return_value = fake_repo\n\n    with patch(f\"{GIT_UTIL}._checkout_ref\") as mock_checkout:\n\n        def _set_head(\n            _repo: MagicMock,\n            _git_ref: str,\n        ) -> None:\n            fake_repo.head.side_effect = None\n            fake_repo.head.return_value = sha.encode()\n\n        mock_checkout.side_effect = _set_head\n        with tempfile.TemporaryDirectory() as t:\n            result = await clone_repo(\n                \"https://github.com/user/repo.git\",\n                git_ref=\"feature/branch\",\n                git_sha=sha,\n                dest_dir=Path(t) / \"sub\",\n            )\n\n    mock_checkout.assert_called_once_with(fake_repo, sha)\n    assert result == GitCloneResult(git_sha=sha, git_ref=\"feature/branch\")\n\n\ndef test_clone_repo_short_sha_like_ref(tmp_path: Path) -> None:\n    \"\"\"Short SHA-like refs are cloned without branch= and checked out after clone.\"\"\"\n    src = tmp_path / \"source\"\n    sha1, sha2, branch = _make_clone_source(src)\n    short = sha1[:8]\n\n    with patch(f\"{GIT_UTIL}.validate_git_url\"):\n        result = clone_repo_sync(\n            str(src), git_ref=short, dest_dir=tmp_path / \"dest\", depth=1\n        )\n\n    assert result.git_sha == sha1\n    assert result.git_ref == short\n\n\n@patch(f\"{GIT_UTIL}.porcelain.clone\")\n@pytest.mark.asyncio\nasync def test_clone_repo_network_error(mock_clone: MagicMock) -> None:\n    \"\"\"Network errors from dulwich are normalized to GitAccessError.\"\"\"\n    mock_clone.side_effect = HangupException()\n\n    with tempfile.TemporaryDirectory() as t:\n        with pytest.raises(GitAccessError, match=\"Failed to clone\"):\n            await clone_repo(\n                \"https://github.com/user/repo.git\", \"main\", dest_dir=Path(t) / \"sub\"\n            )\n\n\n@patch(f\"{GIT_UTIL}.porcelain.clone\")\n@pytest.mark.asyncio\nasync def test_clone_repo_with_basic_auth(mock_clone: MagicMock) -> None:\n    \"\"\"Basic auth gets parsed and forwarded to dulwich as username/password.\"\"\"\n    mock_clone.return_value = _make_fake_repo(\"e\" * 40)\n\n    with tempfile.TemporaryDirectory() as t:\n        await clone_repo(\n            \"https://github.com/user/repo.git\",\n            \"main\",\n            basic_auth=\"someuser:tokenvalue\",\n            dest_dir=Path(t) / \"sub\",\n        )\n\n    call = mock_clone.call_args\n    assert call.kwargs[\"username\"] == \"someuser\"\n    assert call.kwargs[\"password\"] == \"tokenvalue\"\n\n\n@patch(f\"{GIT_UTIL}.porcelain.clone\")\n@pytest.mark.asyncio\nasync def test_clone_repo_passes_depth(mock_clone: MagicMock) -> None:\n    \"\"\"Callers can request a shallow clone via the depth parameter.\"\"\"\n    mock_clone.return_value = _make_fake_repo(\"f\" * 40)\n\n    with tempfile.TemporaryDirectory() as t:\n        await clone_repo(\n            \"https://github.com/user/repo.git\",\n            \"main\",\n            dest_dir=Path(t) / \"sub\",\n            depth=1,\n        )\n\n    assert mock_clone.call_args.kwargs[\"depth\"] == 1\n\n\n@patch(f\"{GIT_UTIL}.porcelain.clone\")\n@pytest.mark.asyncio\nasync def test_clone_repo_sha_overrides_depth(mock_clone: MagicMock) -> None:\n    \"\"\"When given a SHA-shaped ref, depth is dropped to ensure reachability.\"\"\"\n    sha = \"deadbeef\" * 5  # 40 chars\n    fake_repo = MagicMock()\n    fake_repo.head.return_value = sha.encode()\n    fake_repo.refs.read_ref.return_value = b\"ref: refs/heads/main\"\n    fake_repo.refs.__getitem__.return_value = sha.encode()\n    mock_clone.return_value = fake_repo\n\n    with patch(f\"{GIT_UTIL}._checkout_ref\"):\n        with tempfile.TemporaryDirectory() as t:\n            await clone_repo(\n                \"https://github.com/user/repo.git\",\n                git_ref=sha,\n                dest_dir=Path(t) / \"sub\",\n                depth=1,\n            )\n\n    assert mock_clone.call_args.kwargs[\"depth\"] is None\n    # branch is None for a SHA ref since we can't pass a SHA as a branch\n    assert mock_clone.call_args.kwargs[\"branch\"] is None\n\n\ndef _make_real_repo_with_commit(tmp_path: Path) -> tuple[Repo, str]:\n    porcelain.init(str(tmp_path))\n    (tmp_path / \"f.txt\").write_text(\"hello\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    sha = porcelain.commit(\n        str(tmp_path),\n        message=b\"init\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n    return Repo(str(tmp_path)), sha.decode()\n\n\ndef test_checkout_ref_resolves_short_sha_prefix(tmp_path: Path) -> None:\n    \"\"\"Short SHA prefixes resolve to the matching commit object.\"\"\"\n    repo, sha = _make_real_repo_with_commit(tmp_path)\n    try:\n        _checkout_ref(repo, sha[:8])\n        assert repo.head().decode() == sha\n    finally:\n        repo.close()\n\n\ndef test_checkout_ref_rejects_ambiguous_short_sha_prefix(tmp_path: Path) -> None:\n    \"\"\"Ambiguous short SHA prefixes raise a user-facing access error.\"\"\"\n    repo, _ = _make_real_repo_with_commit(tmp_path)\n    try:\n        with patch.object(\n            repo.object_store,\n            \"iter_prefix\",\n            return_value=iter([b\"a\" * 40, b\"b\" * 40]),\n        ):\n            with pytest.raises(GitAccessError, match=\"ambiguous\"):\n                _checkout_ref(repo, \"deadbeef\")\n    finally:\n        repo.close()\n\n\ndef test_checkout_ref_rejects_missing_short_sha_prefix(tmp_path: Path) -> None:\n    \"\"\"Missing short SHA prefixes raise a user-facing access error.\"\"\"\n    repo, _ = _make_real_repo_with_commit(tmp_path)\n    try:\n        with pytest.raises(GitAccessError, match=\"Git ref not found: deadbeef\"):\n            _checkout_ref(repo, \"deadbeef\")\n    finally:\n        repo.close()\n\n\n@pytest.mark.asyncio\nasync def test_clone_repo_rejects_dangerous_url() -> None:\n    with pytest.raises(GitAccessError):\n        await clone_repo(\"ext::sh -c echo pwned\")\n\n\n# Lightweight tests for new git helpers\n\n\ndef test_working_tree_has_changes_true(tmp_path: Path) -> None:\n    porcelain.init(str(tmp_path))\n    (tmp_path / \"f.txt\").write_text(\"hello\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    porcelain.commit(\n        str(tmp_path),\n        message=b\"init\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n    (tmp_path / \"f.txt\").write_text(\"changed\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        assert working_tree_has_changes() is True\n\n\ndef test_working_tree_has_changes_false(tmp_path: Path) -> None:\n    porcelain.init(str(tmp_path))\n    (tmp_path / \"f.txt\").write_text(\"hello\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    porcelain.commit(\n        str(tmp_path),\n        message=b\"init\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        assert working_tree_has_changes() is False\n\n\n@patch(f\"{GIT_UTIL}.porcelain.status\", side_effect=Exception(\"boom\"))\ndef test_working_tree_has_changes_exception(_: MagicMock) -> None:\n    assert working_tree_has_changes() is False\n\n\ndef test_get_unpushed_commits_count_no_upstream(tmp_path: Path) -> None:\n    \"\"\"A fresh repo with no upstream returns None.\"\"\"\n    porcelain.init(str(tmp_path))\n    # Create a single commit so HEAD resolves\n    (tmp_path / \"f.txt\").write_text(\"hello\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    porcelain.commit(\n        str(tmp_path),\n        message=b\"init\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        assert get_unpushed_commits_count() is None\n\n\ndef test_get_unpushed_commits_count_ahead(tmp_path: Path) -> None:\n    \"\"\"When the local branch is ahead of its tracking ref, count the diff.\"\"\"\n    porcelain.init(str(tmp_path))\n    (tmp_path / \"f.txt\").write_text(\"first\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    first_sha = porcelain.commit(\n        str(tmp_path),\n        message=b\"first\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n\n    # Set up an \"upstream\" tracking ref pointing at the first commit\n    repo = Repo(str(tmp_path))\n    try:\n        repo.refs[Ref(b\"refs/remotes/origin/main\")] = first_sha  # type: ignore[index]  # ty: ignore[invalid-assignment]\n        # Configure branch.<current>.merge / .remote\n        config = repo.get_config()\n        head = repo.refs.read_ref(Ref(b\"HEAD\"))\n        assert head is not None and head.startswith(b\"ref: \")\n        branch_name = head[5:].removeprefix(b\"refs/heads/\")\n        config.set((b\"branch\", branch_name), b\"merge\", b\"refs/heads/main\")\n        config.set((b\"branch\", branch_name), b\"remote\", b\"origin\")\n        config.write_to_path()\n    finally:\n        repo.close()\n\n    # Add two more commits\n    (tmp_path / \"f.txt\").write_text(\"second\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    porcelain.commit(\n        str(tmp_path),\n        message=b\"second\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n    (tmp_path / \"f.txt\").write_text(\"third\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    porcelain.commit(\n        str(tmp_path),\n        message=b\"third\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        assert get_unpushed_commits_count() == 2\n\n\ndef test_get_unpushed_commits_count_no_repo(tmp_path: Path) -> None:\n    \"\"\"A non-git directory returns 0 (legacy behavior).\"\"\"\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        assert get_unpushed_commits_count() == 0\n\n\n# Tests for validate_git_public_access\n\n\n@patch(f\"{GIT_UTIL}._probe_remote\")\n@pytest.mark.asyncio\nasync def test_validate_git_public_access_true(mock_probe: MagicMock) -> None:\n    mock_probe.return_value = True\n    with patch(f\"{GIT_UTIL}.validate_git_url_no_ssrf\"):\n        assert (\n            await validate_git_public_access(\"https://github.com/public/repo.git\")\n            is True\n        )\n    mock_probe.assert_called_once_with(\"https://github.com/public/repo.git\")\n\n\n@patch(f\"{GIT_UTIL}._probe_remote\")\n@pytest.mark.asyncio\nasync def test_validate_git_public_access_false(mock_probe: MagicMock) -> None:\n    mock_probe.return_value = False\n    with patch(f\"{GIT_UTIL}.validate_git_url_no_ssrf\"):\n        assert (\n            await validate_git_public_access(\"https://github.com/private/repo.git\")\n            is False\n        )\n\n\ndef test_get_commit_sha_for_ref(tmp_path: Path) -> None:\n    \"\"\"Resolve a branch ref against a real (test) repo.\"\"\"\n    porcelain.init(str(tmp_path))\n    (tmp_path / \"f.txt\").write_text(\"hello\")\n    porcelain.add(str(tmp_path), [str(tmp_path / \"f.txt\")])\n    sha = porcelain.commit(\n        str(tmp_path),\n        message=b\"init\",\n        author=b\"t <t@example.com>\",\n        committer=b\"t <t@example.com>\",\n    )\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        # HEAD resolves\n        assert get_commit_sha_for_ref(\"HEAD\") == sha.decode()\n\n\ndef test_is_git_repo_returns_false_when_not_a_repo(tmp_path: Path) -> None:\n    \"\"\"In a directory that is not a git repo, return False.\"\"\"\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        assert is_git_repo() is False\n\n\ndef test_is_git_repo_returns_true_in_real_repo(tmp_path: Path) -> None:\n    porcelain.init(str(tmp_path))\n    with patch(f\"{GIT_UTIL}.Path.cwd\", return_value=tmp_path):\n        assert is_git_repo() is True\n\n\n# Tests for validate_git_url\n\n\n@pytest.mark.parametrize(\n    \"url\",\n    [\n        \"https://github.com/owner/repo.git\",\n        \"http://example.com/repo.git\",\n    ],\n)\ndef test_validate_git_url_allows_valid_schemes(url: str) -> None:\n    validate_git_url(url)\n\n\n@pytest.mark.parametrize(\n    \"url\",\n    [\n        pytest.param(\"ext::sh -c echo pwned\", id=\"ext-protocol\"),\n        pytest.param(\"-evil-url\", id=\"dash-prefix\"),\n        pytest.param(\"git@github.com:owner/repo.git\", id=\"ssh-shorthand\"),\n        pytest.param(\"ssh://git@github.com/owner/repo.git\", id=\"ssh-scheme\"),\n        pytest.param(\"ftp://example.com/repo\", id=\"unknown-scheme\"),\n    ],\n)\ndef test_validate_git_url_rejects_dangerous_urls(url: str) -> None:\n    with pytest.raises(GitAccessError):\n        validate_git_url(url)\n\n\n@pytest.mark.parametrize(\n    (\"family\", \"addr\"),\n    [\n        (socket.AF_INET, (\"127.0.0.1\", 0)),\n        (socket.AF_INET, (\"10.0.0.1\", 0)),\n        (socket.AF_INET, (\"172.16.0.1\", 0)),\n        (socket.AF_INET, (\"192.168.1.1\", 0)),\n        (socket.AF_INET, (\"169.254.169.254\", 0)),\n        (socket.AF_INET6, (\"::1\", 0, 0, 0)),\n    ],\n    ids=[\n        \"loopback\",\n        \"private-10\",\n        \"private-172\",\n        \"private-192\",\n        \"link-local\",\n        \"ipv6-loopback\",\n    ],\n)\ndef test_validate_git_url_no_ssrf_rejects_private_ips(\n    family: socket.AddressFamily, addr: tuple[str, ...]\n) -> None:\n    with patch(\n        \"llama_agents.core.git.git_util.socket.getaddrinfo\",\n        return_value=[(family, 0, 0, \"\", addr)],\n    ):\n        with pytest.raises(GitAccessError, match=\"private network\"):\n            validate_git_url_no_ssrf(\"https://example.com/repo.git\")\n\n\ndef test_validate_git_url_no_ssrf_allows_public_ip() -> None:\n    with patch(\n        \"llama_agents.core.git.git_util.socket.getaddrinfo\",\n        return_value=[(socket.AF_INET, 0, 0, \"\", (\"140.82.121.3\", 0))],\n    ):\n        validate_git_url_no_ssrf(\"https://github.com/owner/repo.git\")\n\n\ndef test_validate_git_url_no_ssrf_rejects_dns_failure() -> None:\n    with patch(\n        \"llama_agents.core.git.git_util.socket.getaddrinfo\",\n        side_effect=socket.gaierror(\"Name resolution failed\"),\n    ):\n        with pytest.raises(GitAccessError, match=\"DNS resolution failed\"):\n            validate_git_url_no_ssrf(\"https://nonexistent.example.com/repo.git\")\n"
  },
  {
    "path": "packages/llama-agents-core/tests/test_iter_utils.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import AsyncGenerator, Collection\n\nimport pytest\nfrom llama_agents.core.iter_utils import (\n    debounced_sorted_prefix,\n    merge_generators,\n)\n\n\nasync def _gen_with_delays(\n    values: list[str], delays: list[float]\n) -> AsyncGenerator[str, None]:\n    for value, delay in zip(values, delays):\n        if delay:\n            await asyncio.sleep(delay)\n        yield value\n\n\nasync def _gen_with_gates(\n    values: list[str], gates: list[asyncio.Event]\n) -> AsyncGenerator[str, None]:\n    for value, gate in zip(values, gates):\n        await gate.wait()\n        yield value\n\n\nasync def _gen_raises(after_items: int = 0) -> AsyncGenerator[str, None]:\n    produced = 0\n    while produced < after_items:\n        await asyncio.sleep(0)\n        produced += 1\n        yield f\"ok-{produced}\"\n    raise RuntimeError(\"boom\")\n\n\nasync def _gen_with_close_flag(\n    flag: asyncio.Event, gate: asyncio.Event\n) -> AsyncGenerator[str, None]:\n    try:\n        await gate.wait()\n        yield \"slow-1\"\n    finally:\n        flag.set()\n\n\nasync def _wait_for_count(values: Collection[object], expected_count: int) -> None:\n    for _ in range(100):\n        if len(values) == expected_count:\n            return\n        await asyncio.sleep(0)\n    raise AssertionError(f\"expected {expected_count} items, got {values}\")\n\n\n@pytest.mark.asyncio\nasync def test_merge_generators_interleaves_in_arrival_order() -> None:\n    gate_a = asyncio.Event()\n    gate_b = asyncio.Event()\n    gate_c = asyncio.Event()\n    gate_d = asyncio.Event()\n    g1 = _gen_with_gates([\"a\", \"c\"], [gate_a, gate_c])\n    g2 = _gen_with_gates([\"b\", \"d\"], [gate_b, gate_d])\n\n    merged: list[str] = []\n\n    async def collect() -> None:\n        async for item in merge_generators(g1, g2):\n            merged.append(item)\n\n    task = asyncio.create_task(collect())\n    gate_a.set()\n    await _wait_for_count(merged, 1)\n    gate_b.set()\n    await _wait_for_count(merged, 2)\n    gate_c.set()\n    await _wait_for_count(merged, 3)\n    gate_d.set()\n    await task\n\n    assert merged == [\"a\", \"b\", \"c\", \"d\"]\n\n\n@pytest.mark.asyncio\nasync def test_merge_generators_propagates_exception_immediately() -> None:\n    g_ok = _gen_with_delays([\"x\", \"y\"], [0.0, 0.1])\n    g_err = _gen_raises(after_items=1)\n\n    items: list[str] = []\n    with pytest.raises(RuntimeError, match=\"boom\"):\n        async for item in merge_generators(g_ok, g_err):\n            items.append(item)\n\n    # We should have received only items produced before the error\n    assert items[0] in {\"x\", \"ok-1\"}\n    assert len(items) <= 2\n\n\n@pytest.mark.asyncio\nasync def test_merge_generators_stop_on_first_completion_cancels_others() -> None:\n    closed_event = asyncio.Event()\n    slow_gate = asyncio.Event()\n    fast_gate = asyncio.Event()\n    g_slow = _gen_with_close_flag(closed_event, slow_gate)\n    g_fast = _gen_with_gates([\"done\"], [fast_gate])\n\n    collected: list[str] = []\n\n    async def collect() -> None:\n        async for item in merge_generators(\n            g_slow, g_fast, stop_on_first_completion=True\n        ):\n            collected.append(item)\n\n    task = asyncio.create_task(collect())\n    fast_gate.set()\n    await _wait_for_count(collected, 1)\n    await task\n\n    # Only the fast item should be seen deterministically\n    assert collected == [\"done\"]\n    # The slow generator should have been closed/cancelled\n    assert closed_event.is_set()\n\n\n@pytest.mark.asyncio\nasync def test_debounced_sorted_prefix_sorts_then_passthrough() -> None:\n    async def inner() -> AsyncGenerator[int, None]:\n        # Initial burst (unsorted order)\n        for value in [3, 1, 2]:\n            yield value\n        # Wait longer than debounce to ensure the first flush occurs\n        await asyncio.sleep(0.12)\n        # Subsequent items should pass through in arrival order\n        for value in [4, 5]:\n            await asyncio.sleep(0.01)\n            yield value\n\n    output: list[int] = []\n    async for item in debounced_sorted_prefix(\n        inner(), key=lambda x: x, debounce_seconds=0.05, max_window_seconds=0.1\n    ):\n        output.append(item)\n\n    # First three should be sorted, rest in order\n    assert output == [1, 2, 3, 4, 5]\n"
  },
  {
    "path": "packages/llama-agents-core/tests/test_schema.py",
    "content": "\"\"\"Unit tests for Pydantic schemas\"\"\"\n\nfrom datetime import datetime, timezone\n\nimport pytest\nfrom llama_agents.core.schema.deployments import (\n    APPSERVER_TAG_PREFIX,\n    DeploymentCreate,\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    DeploymentsListResponse,\n    DeploymentUpdate,\n    LlamaDeploymentPhase,\n    LlamaDeploymentSpec,\n    ReleaseHistoryEntry,\n    ReleaseHistoryItem,\n    apply_deployment_update,\n    image_tag_to_version,\n    version_to_image_tag,\n)\nfrom llama_agents.core.schema.projects import ProjectsListResponse, ProjectSummary\nfrom pydantic import HttpUrl, ValidationError\n\n\ndef test_deployment_create_valid() -> None:\n    \"\"\"Test valid DeploymentCreate data\"\"\"\n    deployment = DeploymentCreate(\n        display_name=\"Test Service\",\n        repo_url=\"https://github.com/user/repo.git\",\n        secrets={\"GITHUB_PAT\": \"ghp_token123\"},\n    )\n    assert deployment.display_name == \"Test Service\"\n    assert deployment.repo_url == \"https://github.com/user/repo.git\"\n    secrets = deployment.secrets\n    assert secrets is not None\n    assert secrets[\"GITHUB_PAT\"] == \"ghp_token123\"\n\n\n# -- Backwards compatibility: name <-> display_name --\n\n\ndef test_deployment_create_accepts_deprecated_name() -> None:\n    \"\"\"Old callers passing 'name' should still work.\"\"\"\n    deployment = DeploymentCreate(name=\"Legacy Name\", repo_url=\"https://example.com\")  # type: ignore[call-arg]  # ty: ignore[missing-argument, unknown-argument]\n    assert deployment.display_name == \"Legacy Name\"\n\n\ndef test_deployment_create_serializes_name_for_old_servers() -> None:\n    \"\"\"Serialized payload must include 'name' so old servers accept it.\"\"\"\n    deployment = DeploymentCreate(display_name=\"My App\", repo_url=\"https://example.com\")\n    data = deployment.model_dump(exclude_none=True)\n    assert data[\"name\"] == \"My App\"\n    assert data[\"display_name\"] == \"My App\"\n\n\ndef test_deployment_update_accepts_deprecated_name() -> None:\n    \"\"\"Old callers passing 'name' should still work.\"\"\"\n    update = DeploymentUpdate(name=\"Legacy Name\")  # type: ignore[call-arg]  # ty: ignore[unknown-argument]\n    assert update.display_name == \"Legacy Name\"\n\n\ndef test_deployment_update_serializes_name_for_old_servers() -> None:\n    \"\"\"Serialized payload must include 'name' so old servers accept it.\"\"\"\n    update = DeploymentUpdate(display_name=\"Renamed\")\n    data = update.model_dump()\n    assert data[\"name\"] == \"Renamed\"\n    assert data[\"display_name\"] == \"Renamed\"\n\n\ndef test_deployment_update_name_is_none_when_display_name_unset() -> None:\n    \"\"\"When display_name is not set, name should also be None.\"\"\"\n    update = DeploymentUpdate()\n    data = update.model_dump()\n    assert data[\"name\"] is None\n    assert data[\"display_name\"] is None\n\n\ndef test_deployment_response_deserializes_old_server_name() -> None:\n    \"\"\"Server responses with only 'name' (no display_name) should work.\"\"\"\n    resp = DeploymentResponse.model_validate(\n        {\n            \"id\": \"dep-1\",\n            \"name\": \"Old Server Deploy\",\n            \"project_id\": \"proj-1\",\n            \"repo_url\": \"https://example.com\",\n            \"git_ref\": \"main\",\n            \"deployment_file_path\": \"\",\n            \"status\": \"Running\",\n            \"has_personal_access_token\": False,\n        }\n    )\n    assert resp.display_name == \"Old Server Deploy\"\n\n\n# -- Backwards compatibility: llama_deploy_version <-> appserver_version --\n\n\ndef test_deployment_create_accepts_deprecated_llama_deploy_version() -> None:\n    \"\"\"Old callers passing 'llama_deploy_version' should still work.\"\"\"\n    deployment = DeploymentCreate(\n        display_name=\"App\",\n        repo_url=\"https://example.com\",\n        llama_deploy_version=\"0.4.2\",  # type: ignore[call-arg]  # ty: ignore[unknown-argument]\n    )\n    assert deployment.appserver_version == \"0.4.2\"\n\n\ndef test_deployment_create_canonical_wins_on_conflict() -> None:\n    \"\"\"When both fields are sent, canonical 'appserver_version' wins silently.\"\"\"\n    deployment = DeploymentCreate.model_validate(\n        {\n            \"display_name\": \"App\",\n            \"repo_url\": \"https://example.com\",\n            \"appserver_version\": \"0.4.2\",\n            \"llama_deploy_version\": \"0.3.0\",\n        }\n    )\n    assert deployment.appserver_version == \"0.4.2\"\n\n\ndef test_deployment_create_neither_version_field_set() -> None:\n    \"\"\"With neither field set, appserver_version is None.\"\"\"\n    deployment = DeploymentCreate(display_name=\"App\", repo_url=\"https://example.com\")\n    assert deployment.appserver_version is None\n\n\n@pytest.mark.parametrize(\n    \"version\",\n    [\n        \"0.11.3\",\n        \"1.0.0\",\n        \"0.12.0rc1\",\n        \"0.12.0.dev1\",\n        \"0.12.0.post1\",\n    ],\n)\ndef test_deployment_create_accepts_public_pep440_appserver_version(\n    version: str,\n) -> None:\n    deployment = DeploymentCreate(\n        display_name=\"App\",\n        repo_url=\"https://example.com\",\n        appserver_version=version,\n    )\n    assert deployment.appserver_version == version\n\n\n@pytest.mark.parametrize(\n    \"version\",\n    [\n        \"latest\",\n        \"tilt-dev\",\n        \"definitely-not-a-version\",\n        \"0.12.0+local\",\n        \"1!2.0\",\n    ],\n)\ndef test_deployment_create_rejects_non_public_pep440_appserver_version(\n    version: str,\n) -> None:\n    with pytest.raises(ValidationError, match=\"appserver_version\"):\n        DeploymentCreate(\n            display_name=\"App\",\n            repo_url=\"https://example.com\",\n            appserver_version=version,\n        )\n\n\ndef test_deployment_create_serializes_llama_deploy_version_for_old_servers() -> None:\n    \"\"\"Serialized payload must include 'llama_deploy_version' so old servers accept it.\"\"\"\n    deployment = DeploymentCreate(\n        display_name=\"App\",\n        repo_url=\"https://example.com\",\n        appserver_version=\"0.4.2\",\n    )\n    data = deployment.model_dump()\n    assert data[\"appserver_version\"] == \"0.4.2\"\n    assert data[\"llama_deploy_version\"] == \"0.4.2\"\n\n\ndef test_deployment_update_accepts_deprecated_llama_deploy_version() -> None:\n    \"\"\"Old callers patching 'llama_deploy_version' should still work.\"\"\"\n    update = DeploymentUpdate(llama_deploy_version=\"0.4.2\")  # type: ignore[call-arg]  # ty: ignore[unknown-argument]\n    assert update.appserver_version == \"0.4.2\"\n\n\ndef test_deployment_update_canonical_wins_on_conflict() -> None:\n    \"\"\"When both fields are sent, canonical 'appserver_version' wins silently.\"\"\"\n    update = DeploymentUpdate.model_validate(\n        {\"appserver_version\": \"0.4.2\", \"llama_deploy_version\": \"0.3.0\"}\n    )\n    assert update.appserver_version == \"0.4.2\"\n\n\ndef test_deployment_update_rejects_non_public_pep440_appserver_version() -> None:\n    with pytest.raises(ValidationError, match=\"appserver_version\"):\n        DeploymentUpdate(appserver_version=\"tilt-dev\")\n\n\ndef test_deployment_update_allows_internal_image_tag_escape_hatch() -> None:\n    update = DeploymentUpdate(image_tag=\"tilt-dev\")\n    assert update.image_tag == \"tilt-dev\"\n\n\ndef test_deployment_update_serializes_llama_deploy_version_for_old_servers() -> None:\n    \"\"\"Serialized PATCH payload must include 'llama_deploy_version' so old servers accept it.\"\"\"\n    update = DeploymentUpdate(appserver_version=\"0.4.2\")\n    data = update.model_dump()\n    assert data[\"appserver_version\"] == \"0.4.2\"\n    assert data[\"llama_deploy_version\"] == \"0.4.2\"\n\n\ndef test_deployment_update_llama_deploy_version_none_when_unset() -> None:\n    \"\"\"When appserver_version is not set, llama_deploy_version should also be None.\"\"\"\n    update = DeploymentUpdate()\n    data = update.model_dump()\n    assert data[\"appserver_version\"] is None\n    assert data[\"llama_deploy_version\"] is None\n\n\ndef test_deployment_response_accepts_old_server_llama_deploy_version() -> None:\n    \"\"\"An old server emits only llama_deploy_version; new client should map it through.\"\"\"\n    resp = DeploymentResponse.model_validate(\n        {\n            \"id\": \"dep-1\",\n            \"display_name\": \"App\",\n            \"project_id\": \"proj-1\",\n            \"repo_url\": \"https://example.com\",\n            \"git_ref\": \"main\",\n            \"deployment_file_path\": \"\",\n            \"status\": \"Running\",\n            \"has_personal_access_token\": False,\n            \"llama_deploy_version\": \"0.4.2\",\n        }\n    )\n    assert resp.appserver_version == \"0.4.2\"\n\n\ndef test_deployment_response_serializes_both_version_fields() -> None:\n    \"\"\"Responses include both keys so old clients keep working.\"\"\"\n    resp = DeploymentResponse(\n        id=\"dep-1\",\n        display_name=\"App\",\n        project_id=\"proj-1\",\n        repo_url=\"https://example.com\",\n        git_ref=\"main\",\n        deployment_file_path=\"\",\n        status=\"Running\",\n        appserver_version=\"0.4.2\",\n    )\n    data = resp.model_dump()\n    assert data[\"appserver_version\"] == \"0.4.2\"\n    assert data[\"llama_deploy_version\"] == \"0.4.2\"\n\n\ndef test_deployment_create_optional_fields() -> None:\n    \"\"\"Test DeploymentCreate with optional fields\"\"\"\n\n    deployment = DeploymentCreate(\n        display_name=\"Test Service\",\n        repo_url=\"https://github.com/user/repo.git\",\n        secrets={\"GITHUB_PAT\": \"token\"},\n        deployment_file_path=\"custom_deploy.py\",\n        personal_access_token=\"ghp_token123\",\n    )\n    assert deployment.deployment_file_path == \"custom_deploy.py\"\n    assert deployment.personal_access_token == \"ghp_token123\"\n\n\ndef test_deployment_response() -> None:\n    \"\"\"Test DeploymentResponse creation\"\"\"\n    response = DeploymentResponse(\n        id=\"deploy1-123\",\n        display_name=\"deploy1\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo1.git\",\n        git_ref=\"abc123\",\n        deployment_file_path=\"llama_deployment.yml\",\n        status=\"Running\",\n        apiserver_url=HttpUrl(\"http://test-deploy.127.0.0.1.nip.io\"),\n    )\n    assert response.display_name == \"deploy1\"\n    assert response.project_id == \"test-project\"\n    assert response.status == \"Running\"\n    assert response.apiserver_url == HttpUrl(\"http://test-deploy.127.0.0.1.nip.io\")\n\n\ndef test_apply_deployment_update_basic_fields() -> None:\n    \"\"\"Test updating basic spec fields\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/old-repo.git\",\n        deploymentFilePath=\"old_deploy.yml\",\n    )\n\n    update = DeploymentUpdate(\n        repo_url=\"https://github.com/user/new-repo.git\",\n        deployment_file_path=\"new_deploy.yml\",\n    )\n\n    result = apply_deployment_update(update, existing_spec)\n\n    # Check updated spec\n    assert result.updated_spec.projectId == \"test-project\"  # unchanged\n    assert (\n        result.updated_spec.repoUrl == \"https://github.com/user/new-repo.git\"\n    )  # updated\n    assert result.updated_spec.deploymentFilePath == \"new_deploy.yml\"  # updated\n\n    # No secret changes for basic field updates\n    assert result.secret_adds == {}\n    assert result.secret_removes == []\n\n    # Original spec should be unchanged\n    assert existing_spec.repoUrl == \"https://github.com/user/old-repo.git\"\n    assert existing_spec.deploymentFilePath == \"old_deploy.yml\"\n\n\ndef test_apply_deployment_update_personal_access_token() -> None:\n    \"\"\"Test PAT updates (stored as GITHUB_PAT secret)\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n    )\n\n    # Test adding/updating PAT\n    update_add = DeploymentUpdate(personal_access_token=\"ghp_newtoken123\")\n    result = apply_deployment_update(update_add, existing_spec)\n\n    assert result.secret_adds == {\"GITHUB_PAT\": \"ghp_newtoken123\"}\n    assert result.secret_removes == []\n\n    # Test removing PAT (empty string)\n    update_remove = DeploymentUpdate(personal_access_token=\"\")\n    result = apply_deployment_update(update_remove, existing_spec)\n\n    assert result.secret_adds == {}\n    assert result.secret_removes == [\"GITHUB_PAT\"]\n\n\ndef test_apply_deployment_update_secrets() -> None:\n    \"\"\"Test explicit secret additions and removals\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n    )\n\n    update = DeploymentUpdate(\n        secrets={\n            \"DATABASE_URL\": \"postgresql://new-db\",  # add/update\n            \"API_KEY\": \"new-key-123\",  # add/update\n            \"OLD_SECRET\": None,  # remove\n        }\n    )\n\n    result = apply_deployment_update(update, existing_spec)\n\n    assert result.secret_adds == {\n        \"DATABASE_URL\": \"postgresql://new-db\",\n        \"API_KEY\": \"new-key-123\",\n    }\n    assert result.secret_removes == [\"OLD_SECRET\"]\n\n\ndef test_apply_deployment_update_combined() -> None:\n    \"\"\"Test combining field updates, PAT, and secrets\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/old-repo.git\",\n        deploymentFilePath=\"old_deploy.yml\",\n    )\n\n    update = DeploymentUpdate(\n        repo_url=\"https://github.com/user/new-repo.git\",\n        personal_access_token=\"ghp_newtoken\",\n        secrets={\n            \"DATABASE_URL\": \"postgresql://db\",\n            \"REMOVE_ME\": None,\n        },\n    )\n\n    result = apply_deployment_update(update, existing_spec)\n\n    # Spec updates\n    assert result.updated_spec.repoUrl == \"https://github.com/user/new-repo.git\"\n    assert result.updated_spec.deploymentFilePath == \"old_deploy.yml\"  # unchanged\n\n    # Secret changes\n    assert result.secret_adds == {\n        \"GITHUB_PAT\": \"ghp_newtoken\",\n        \"DATABASE_URL\": \"postgresql://db\",\n    }\n    assert result.secret_removes == [\"REMOVE_ME\"]\n\n\ndef test_apply_deployment_update_no_changes() -> None:\n    \"\"\"Test update with no actual changes\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n    )\n\n    update = DeploymentUpdate()  # All fields None\n\n    result = apply_deployment_update(update, existing_spec)\n\n    # Spec should be unchanged\n    assert result.updated_spec.projectId == existing_spec.projectId\n    assert result.updated_spec.repoUrl == existing_spec.repoUrl\n\n    # No secret changes\n    assert result.secret_adds == {}\n    assert result.secret_removes == []\n\n\ndef test_apply_deployment_update_none_fields() -> None:\n    \"\"\"Test that None values in update don't override spec fields\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n        deploymentFilePath=\"deploy.yml\",\n        gitRef=\"main\",\n        displayName=\"my-deployment\",\n    )\n\n    update = DeploymentUpdate(\n        repo_url=None,  # Should not override existing value\n        deployment_file_path=None,  # Should not override existing value\n        personal_access_token=None,  # Should not add/remove PAT\n        secrets=None,  # Should not modify secrets\n    )\n\n    result = apply_deployment_update(update, existing_spec)\n\n    # All spec fields should remain unchanged\n    assert result.updated_spec.projectId == \"test-project\"\n    assert result.updated_spec.repoUrl == \"https://github.com/user/repo.git\"\n    assert result.updated_spec.deploymentFilePath == \"deploy.yml\"\n    assert result.updated_spec.gitRef == \"main\"\n    assert result.updated_spec.displayName == \"my-deployment\"\n\n    # No secret changes\n    assert result.secret_adds == {}\n    assert result.secret_removes == []\n\n\ndef test_project_summary() -> None:\n    \"\"\"Test ProjectSummary creation\"\"\"\n    project = ProjectSummary(\n        project_id=\"test-project\", deployment_count=5, project_name=\"test-project\"\n    )\n    assert project.project_id == \"test-project\"\n    assert project.deployment_count == 5\n\n\ndef test_deployments_list_response() -> None:\n    \"\"\"Test DeploymentsListResponse creation\"\"\"\n    deployments = [\n        DeploymentResponse(\n            id=\"deploy1\",\n            display_name=\"deploy1\",\n            project_id=\"test-project\",\n            repo_url=\"https://github.com/user/repo1.git\",\n            git_ref=\"abc123\",\n            deployment_file_path=\"llama_deployment.yml\",\n            status=\"Running\",\n            apiserver_url=HttpUrl(\"http://deploy1.example.com\"),\n        ),\n        DeploymentResponse(\n            id=\"deploy2\",\n            display_name=\"deploy2\",\n            project_id=\"test-project\",\n            repo_url=\"https://github.com/user/repo2.git\",\n            git_ref=\"def456\",\n            deployment_file_path=\"llama_deployment.yml\",\n            status=\"Pending\",\n            apiserver_url=HttpUrl(\"http://deploy2.example.com\"),\n        ),\n    ]\n\n    response = DeploymentsListResponse(deployments=deployments)\n    assert len(response.deployments) == 2\n    assert response.deployments[0].display_name == \"deploy1\"\n    assert response.deployments[1].display_name == \"deploy2\"\n\n\ndef test_apply_deployment_update_git_ref() -> None:\n    \"\"\"Test updating git_ref field\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n        gitRef=\"main\",\n    )\n\n    update = DeploymentUpdate(git_ref=\"feature-branch\")\n    result = apply_deployment_update(update, existing_spec)\n\n    # Check that git_ref was updated\n    assert result.updated_spec.gitRef == \"feature-branch\"\n    assert (\n        result.updated_spec.repoUrl == \"https://github.com/user/repo.git\"\n    )  # unchanged\n    assert result.updated_spec.projectId == \"test-project\"  # unchanged\n\n    # No secret changes\n    assert result.secret_adds == {}\n    assert result.secret_removes == []\n\n    # Original spec should be unchanged\n    assert existing_spec.gitRef == \"main\"\n\n\ndef test_release_history_models_roundtrip() -> None:\n    # CRD-style entry\n    dt = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)\n    entry = ReleaseHistoryEntry(gitSha=\"abc\", releasedAt=dt)\n    assert entry.gitSha == \"abc\"\n    # API response types\n    item = ReleaseHistoryItem(git_sha=\"abc\", released_at=dt)\n    resp = DeploymentHistoryResponse(deployment_id=\"d1\", history=[item])\n    assert resp.deployment_id == \"d1\"\n    assert resp.history[0].git_sha == \"abc\"\n\n\ndef test_deployment_create_with_git_ref() -> None:\n    \"\"\"Test DeploymentCreate with git_ref field\"\"\"\n    deployment = DeploymentCreate(\n        display_name=\"Test Service\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"feature-branch\",\n        secrets={\"API_KEY\": \"secret_value\"},\n    )\n\n    assert deployment.display_name == \"Test Service\"\n    assert deployment.repo_url == \"https://github.com/user/repo.git\"\n    assert deployment.git_ref == \"feature-branch\"\n    assert deployment.secrets == {\"API_KEY\": \"secret_value\"}\n\n\ndef test_projects_list_response() -> None:\n    \"\"\"Test ProjectsListResponse creation\"\"\"\n    projects = [\n        ProjectSummary(\n            project_id=\"project1\", deployment_count=3, project_name=\"project1\"\n        ),\n        ProjectSummary(\n            project_id=\"project2\", deployment_count=1, project_name=\"project2\"\n        ),\n    ]\n\n    response = ProjectsListResponse(projects=projects)\n    assert len(response.projects) == 2\n    assert response.projects[0].project_id == \"project1\"\n    assert response.projects[1].deployment_count == 1\n\n\ndef test_deployment_phases_all_valid() -> None:\n    \"\"\"Test that all deployment phases are valid literal values\"\"\"\n    valid_phases: list[LlamaDeploymentPhase] = [\n        \"Pending\",\n        \"Running\",\n        \"Failed\",\n        \"RollingOut\",\n        \"RolloutFailed\",\n    ]\n\n    for phase in valid_phases:\n        # Test that we can create DeploymentResponse with each phase\n        response = DeploymentResponse(\n            id=f\"deploy-{phase.lower()}\",\n            display_name=f\"test-{phase.lower()}\",\n            project_id=\"test-project\",\n            repo_url=\"https://github.com/user/repo.git\",\n            git_ref=\"main\",\n            deployment_file_path=\"llama_deployment.yml\",\n            status=phase,\n            apiserver_url=HttpUrl(\"http://test.example.com\"),\n        )\n        assert response.status == phase\n\n\ndef test_unknown_phase_value_accepted() -> None:\n    \"\"\"An unknown phase value (e.g. emitted by a newer server) is accepted as a\n    plain string instead of failing validation on older clients.\"\"\"\n    response = DeploymentResponse(\n        id=\"deploy-future\",\n        display_name=\"future-phase-deployment\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"main\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"SomeFuturePhaseTheClientDoesNotKnow\",\n        apiserver_url=HttpUrl(\"http://future.example.com\"),\n    )\n    assert response.status == \"SomeFuturePhaseTheClientDoesNotKnow\"\n\n\ndef test_rollingout_phase_response() -> None:\n    \"\"\"Test DeploymentResponse with RollingOut phase\"\"\"\n    response = DeploymentResponse(\n        id=\"deploy-rolling\",\n        display_name=\"rolling-deployment\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"feature-branch\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"RollingOut\",\n        apiserver_url=HttpUrl(\"http://rolling.example.com\"),\n    )\n    assert response.status == \"RollingOut\"\n    assert response.display_name == \"rolling-deployment\"\n\n\ndef test_rolloutfailed_phase_response() -> None:\n    \"\"\"Test DeploymentResponse with RolloutFailed phase\"\"\"\n    response = DeploymentResponse(\n        id=\"deploy-failed\",\n        display_name=\"failed-deployment\",\n        project_id=\"test-project\",\n        repo_url=\"https://github.com/user/repo.git\",\n        git_ref=\"broken-branch\",\n        deployment_file_path=\"deploy.yml\",\n        status=\"RolloutFailed\",\n        apiserver_url=HttpUrl(\"http://failed.example.com\"),\n    )\n    assert response.status == \"RolloutFailed\"\n    assert response.display_name == \"failed-deployment\"\n\n\ndef test_deployment_phases_in_list_response() -> None:\n    \"\"\"Test that new phases work in DeploymentsListResponse\"\"\"\n    deployments = [\n        DeploymentResponse(\n            id=\"deploy1\",\n            display_name=\"running-deploy\",\n            project_id=\"test-project\",\n            repo_url=\"https://github.com/user/repo1.git\",\n            git_ref=\"main\",\n            deployment_file_path=\"deploy.yml\",\n            status=\"Running\",\n            apiserver_url=HttpUrl(\"http://running.example.com\"),\n        ),\n        DeploymentResponse(\n            id=\"deploy2\",\n            display_name=\"rolling-deploy\",\n            project_id=\"test-project\",\n            repo_url=\"https://github.com/user/repo2.git\",\n            git_ref=\"feature\",\n            deployment_file_path=\"deploy.yml\",\n            status=\"RollingOut\",\n            apiserver_url=HttpUrl(\"http://rolling.example.com\"),\n        ),\n        DeploymentResponse(\n            id=\"deploy3\",\n            display_name=\"failed-deploy\",\n            project_id=\"test-project\",\n            repo_url=\"https://github.com/user/repo3.git\",\n            git_ref=\"broken\",\n            deployment_file_path=\"deploy.yml\",\n            status=\"RolloutFailed\",\n            apiserver_url=HttpUrl(\"http://failed.example.com\"),\n        ),\n    ]\n\n    response = DeploymentsListResponse(deployments=deployments)\n    assert len(response.deployments) == 3\n    assert response.deployments[0].status == \"Running\"\n    assert response.deployments[1].status == \"RollingOut\"\n    assert response.deployments[2].status == \"RolloutFailed\"\n\n\n# ===== Image tag / version conversion helpers =====\n\n\ndef test_version_to_image_tag() -> None:\n    assert version_to_image_tag(\"0.4.2\") == \"0.4.2\"\n    assert version_to_image_tag(\"1.0.0\") == \"1.0.0\"\n    assert version_to_image_tag(\"latest\") == \"latest\"\n\n\ndef test_image_tag_to_version_plain() -> None:\n    \"\"\"New-style plain version tags are recognized.\"\"\"\n    assert image_tag_to_version(\"0.4.2\") == \"0.4.2\"\n    assert image_tag_to_version(\"1.0.0\") == \"1.0.0\"\n\n\ndef test_image_tag_to_version_legacy_prefix() -> None:\n    \"\"\"Legacy appserver-prefixed tags still work for backward compat.\"\"\"\n    assert image_tag_to_version(\"appserver-0.4.2\") == \"0.4.2\"\n    assert image_tag_to_version(\"appserver-latest\") == \"latest\"\n\n\ndef test_image_tag_to_version_non_conforming() -> None:\n    # Hash-based tags from dev builds, custom tags, etc.\n    assert image_tag_to_version(\"abc123def\") is None\n    assert image_tag_to_version(\"my-custom-tag\") is None\n    assert image_tag_to_version(\"\") is None\n\n\ndef test_appserver_tag_prefix_constant() -> None:\n    assert APPSERVER_TAG_PREFIX == \"appserver-\"\n\n\ndef test_apply_deployment_update_image_tag_precedence() -> None:\n    \"\"\"image_tag takes precedence over appserver_version\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n    )\n\n    update = DeploymentUpdate(\n        appserver_version=\"0.3.0\",\n        image_tag=\"appserver-0.4.2\",\n    )\n\n    result = apply_deployment_update(update, existing_spec)\n    assert result.updated_spec.imageTag == \"appserver-0.4.2\"\n\n\ndef test_apply_deployment_update_image_tag_only() -> None:\n    \"\"\"image_tag alone sets the spec imageTag\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n    )\n\n    update = DeploymentUpdate(image_tag=\"custom-hash-tag\")\n    result = apply_deployment_update(update, existing_spec)\n    assert result.updated_spec.imageTag == \"custom-hash-tag\"\n\n\ndef test_apply_deployment_update_suspended() -> None:\n    spec = LlamaDeploymentSpec(projectId=\"p\", repoUrl=\"https://r\", displayName=\"n\")\n\n    # Suspend\n    result = apply_deployment_update(DeploymentUpdate(suspended=True), spec)\n    assert result.updated_spec.suspended is True\n\n    # Unsuspend\n    result2 = apply_deployment_update(\n        DeploymentUpdate(suspended=False), result.updated_spec\n    )\n    assert result2.updated_spec.suspended is False\n\n    # No change (None = don't touch)\n    result3 = apply_deployment_update(DeploymentUpdate(), result2.updated_spec)\n    assert result3.updated_spec.suspended is False\n\n\ndef test_apply_deployment_update_auto_resume_git_ref() -> None:\n    \"\"\"PATCH with git_ref on a suspended deployment auto-resumes it.\"\"\"\n    spec = LlamaDeploymentSpec(\n        projectId=\"p\", repoUrl=\"https://r\", displayName=\"n\", suspended=True\n    )\n    update = DeploymentUpdate(git_ref=\"new-branch\")\n    result = apply_deployment_update(update, spec)\n    assert result.updated_spec.suspended is False\n    assert result.updated_spec.gitRef == \"new-branch\"\n\n\ndef test_apply_deployment_update_auto_resume_secrets() -> None:\n    \"\"\"PATCH with secrets on a suspended deployment auto-resumes it.\"\"\"\n    spec = LlamaDeploymentSpec(\n        projectId=\"p\", repoUrl=\"https://r\", displayName=\"n\", suspended=True\n    )\n    update = DeploymentUpdate(secrets={\"KEY\": \"val\"})\n    result = apply_deployment_update(update, spec)\n    assert result.updated_spec.suspended is False\n\n\ndef test_apply_deployment_update_auto_resume_image_tag() -> None:\n    \"\"\"PATCH with image_tag on a suspended deployment auto-resumes it.\"\"\"\n    spec = LlamaDeploymentSpec(\n        projectId=\"p\", repoUrl=\"https://r\", displayName=\"n\", suspended=True\n    )\n    update = DeploymentUpdate(image_tag=\"appserver-0.5.0\")\n    result = apply_deployment_update(update, spec)\n    assert result.updated_spec.suspended is False\n    assert result.updated_spec.imageTag == \"appserver-0.5.0\"\n\n\ndef test_apply_deployment_update_auto_resume_explicit_suspended_true() -> None:\n    \"\"\"PATCH with git_ref + suspended=True stays suspended.\"\"\"\n    spec = LlamaDeploymentSpec(\n        projectId=\"p\", repoUrl=\"https://r\", displayName=\"n\", suspended=True\n    )\n    update = DeploymentUpdate(git_ref=\"new-branch\", suspended=True)\n    result = apply_deployment_update(update, spec)\n    assert result.updated_spec.suspended is True\n    assert result.updated_spec.gitRef == \"new-branch\"\n\n\ndef test_apply_deployment_update_auto_resume_only_suspended_true() -> None:\n    \"\"\"PATCH with only suspended=True stays suspended (no auto-resume).\"\"\"\n    spec = LlamaDeploymentSpec(\n        projectId=\"p\", repoUrl=\"https://r\", displayName=\"n\", suspended=True\n    )\n    update = DeploymentUpdate(suspended=True)\n    result = apply_deployment_update(update, spec)\n    assert result.updated_spec.suspended is True\n\n\ndef test_deployment_update_has_git_fields() -> None:\n    \"\"\"Test that has_git_fields() returns True for git-affecting fields and False for non-git fields.\"\"\"\n    assert DeploymentUpdate(git_ref=\"main\").has_git_fields() is True\n    assert DeploymentUpdate(repo_url=\"https://github.com/u/r\").has_git_fields() is True\n    assert DeploymentUpdate(deployment_file_path=\"deploy.yml\").has_git_fields() is True\n    assert DeploymentUpdate(personal_access_token=\"ghp_tok\").has_git_fields() is True\n    assert DeploymentUpdate(suspended=True).has_git_fields() is False\n    assert DeploymentUpdate(image_tag=\"appserver-0.5.0\").has_git_fields() is False\n    assert DeploymentUpdate(secrets={\"K\": \"V\"}).has_git_fields() is False\n    assert DeploymentUpdate().has_git_fields() is False\n\n\ndef test_apply_deployment_update_static_assets_path_not_cleared() -> None:\n    \"\"\"Test that static_assets_path is NOT cleared when update doesn't set it.\"\"\"\n    spec = LlamaDeploymentSpec(\n        projectId=\"p\",\n        repoUrl=\"https://r\",\n        displayName=\"n\",\n        staticAssetsPath=\"/existing/path\",\n    )\n    update = DeploymentUpdate(suspended=True)\n    result = apply_deployment_update(update, spec)\n    assert result.updated_spec.staticAssetsPath == \"/existing/path\"\n\n\ndef test_apply_deployment_update_version_sets_image_tag() -> None:\n    \"\"\"appserver_version is converted to imageTag when image_tag is not set\"\"\"\n    existing_spec = LlamaDeploymentSpec(\n        displayName=\"my-deployment\",\n        projectId=\"test-project\",\n        repoUrl=\"https://github.com/user/repo.git\",\n    )\n\n    update = DeploymentUpdate(appserver_version=\"0.3.1\")\n    result = apply_deployment_update(update, existing_spec)\n    assert result.updated_spec.imageTag == \"0.3.1\"\n"
  },
  {
    "path": "packages/llama-agents-core/tests/test_ssl_util.py",
    "content": "\"\"\"Tests for SSL/TLS utility functions.\"\"\"\n\nimport ssl\n\nimport httpx\nimport pytest\nfrom llama_agents.core.client.ssl_util import get_httpx_verify_param, get_ssl_context\n\n\n@pytest.fixture(autouse=True)\ndef clean_env(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Clean SSL-related environment variables before each test.\"\"\"\n    monkeypatch.delenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", raising=False)\n\n\ndef test_get_ssl_context_default() -> None:\n    \"\"\"Test that get_ssl_context returns True when no env var is set.\"\"\"\n    result = get_ssl_context()\n    assert result is True\n\n\ndef test_get_ssl_context_truststore_enabled(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that get_ssl_context returns SSLContext when truststore is enabled.\"\"\"\n    monkeypatch.setenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"1\")\n    result = get_ssl_context()\n    assert isinstance(result, ssl.SSLContext)\n\n\ndef test_get_httpx_verify_param_delegates() -> None:\n    \"\"\"Test that get_httpx_verify_param delegates to get_ssl_context.\"\"\"\n    # Default case - should return True\n    result = get_httpx_verify_param()\n    assert result is True\n\n\ndef test_get_httpx_verify_param_delegates_truststore(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that get_httpx_verify_param returns SSLContext when truststore enabled.\"\"\"\n    monkeypatch.setenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"1\")\n    result = get_httpx_verify_param()\n    assert isinstance(result, ssl.SSLContext)\n\n\ndef test_ssl_context_type_validation(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that truststore SSLContext has expected protocol.\"\"\"\n    monkeypatch.setenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"1\")\n    result = get_ssl_context()\n    assert isinstance(result, ssl.SSLContext)\n    # Verify it's configured with TLS client protocol\n    assert result.protocol == ssl.PROTOCOL_TLS_CLIENT\n\n\ndef test_httpx_clients_accept_ssl_context(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that httpx.AsyncClient accepts the SSL context from get_httpx_verify_param.\"\"\"\n    monkeypatch.setenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"1\")\n    verify = get_httpx_verify_param()\n\n    # Should not raise an error - just verify client creation succeeds\n    client = httpx.AsyncClient(verify=verify)\n    assert client is not None\n\n\ndef test_httpx_clients_accept_default_verify() -> None:\n    \"\"\"Test that httpx.AsyncClient accepts the default True verify param.\"\"\"\n    verify = get_httpx_verify_param()\n    assert verify is True\n\n    # Should not raise an error - just verify client creation succeeds\n    client = httpx.AsyncClient(verify=verify)\n    assert client is not None\n"
  },
  {
    "path": "packages/llama-agents-dbos/ARCHITECTURE.md",
    "content": "# DBOS Adapter Architecture\n\n## Model Overview\n\nDBOS is a **local runtime with database coordination**. Each DBOS process runs workflows and steps co-located in the same process. Coordination between replicas happens through a shared Postgres database.\n\nThere are no distributed step workers — a workflow and all its steps execute in the same process that started them.\n\n## Executor ID and Workflow Ownership\n\nEach DBOS replica is configured with a unique `executor_id` (e.g. `\"replica-8001\"`). This ID is recorded in the database for every workflow the replica starts, creating a natural ownership model:\n\n- A replica **owns** the workflows it started. On startup, DBOS automatically recovers and relaunches any incomplete workflows belonging to its `executor_id`.\n- This makes each replica a **stateful shard** — it's responsible for a specific subset of workflows, determined by which ones were routed to it.\n- Replicas share the same Postgres database and can communicate through it, but each replica only runs its own workflows.\n\nThe `executor_id` model means horizontal scaling works by adding replicas that each own a slice of the workload, not by distributing individual workflow steps across nodes.\n\n## Process Layout\n\n```\n┌──────── Replica A (executor_id: replica-8001) ────────┐\n│  WorkflowServer                                       │\n│  ├─ DBOSRuntime                                       │\n│  │  ├─ InternalAdapter (per run) ← runs the workflow  │\n│  │  └─ Workflow steps (co-located)                    │\n│  └─ ExternalAdapter (per run) ← receives HTTP calls   │\n└───────────────────────┬───────────────────────────────┘\n                        │\n                   Shared Postgres\n                   (DBOS tables + pg_notify)\n                        │\n┌──────── Replica B (executor_id: replica-8002) ────────┐\n│  WorkflowServer                                       │\n│  ├─ DBOSRuntime                                       │\n│  │  ├─ InternalAdapter (per run)                      │\n│  │  └─ Workflow steps (co-located)                    │\n│  └─ ExternalAdapter (per run)                         │\n└───────────────────────────────────────────────────────┘\n```\n\n## The Adapter Boundary\n\nThe core runtime exposes two adapter interfaces per workflow run:\n\n- **InternalRunAdapter** — used by the control loop running the workflow. Always in the same process as the workflow.\n- **ExternalRunAdapter** — used by callers (HTTP handlers, other services). May be in a different process.\n\nFor DBOS, this distinction maps to a **process boundary**. The internal adapter uses `DBOS.send()` / `DBOS.recv_async()` locally. The external adapter uses `DBOS.send_async()` which writes to Postgres, making it reachable from any replica.\n\n## Event Delivery (Cross-Process)\n\nWhen Replica B sends an event to a workflow owned by Replica A:\n\n1. Replica B's external adapter writes the event to Postgres via `DBOS.send_async()`\n2. Replica A's internal adapter picks it up via `DBOS.recv_async()` (polls Postgres)\n3. The event is delivered to the workflow's control loop in Replica A\n\n## Event Streaming (Cross-Process)\n\nWorkflow output events flow through `WorkflowStore` backed by Postgres:\n\n1. A workflow step publishes an event via `write_to_event_stream()`\n2. The store writes to Postgres and sends `pg_notify`\n3. Any replica calling `subscribe_events(run_id)` receives the event\n\n## Idle Release (Continue-as-New)\n\n`DBOSIdleReleaseDecorator` wraps the runtime to release idle workflows from memory using a \"continue-as-new\" approach. A distributed lifecycle lock (`RunLifecycleLock`) coordinates release and resume across replicas using a state machine: `active → releasing → released → active`.\n\n- **Release**: When a workflow goes idle, a process-local timer starts. After `idle_timeout` seconds, the decorator calls `begin_release(run_id)` to CAS `active → releasing`. If successful, it sends `TickIdleRelease` through the external adapter via `DBOS.send_async()`. The control loop processes it, completing the workflow with `IdleReleasedEvent`. A background task awaits workflow completion and then calls `complete_release` to transition to `released`, setting `idle_since` only at this point.\n- **Resume**: When `send_event` is called, it consults the lifecycle lock via `try_begin_resume`. If the state is `released`, the caller waits for the old DBOS workflow to finish (cross-replica via `DBOS.retrieve_workflow_async`), purges DBOS/journal state, rebuilds `BrokerState` from the tick log, and starts a fresh DBOS workflow with the same `run_id`. If the state is `releasing`, the caller polls with a crash timeout.\n- **Crash recovery**: If a releaser crashes mid-release (state stuck at `releasing` past a timeout), `try_begin_resume` detects the stale timestamp via `crash_timeout_seconds` and force-transitions to active.\n\nTick persistence is provided by `TickPersistenceDecorator` in the decorator chain, which stores ticks to the workflow store so they can be replayed on resume.\n\nBoth operations go through the database, so any replica can resume an idle-released workflow — the new DBOS workflow starts on whichever replica handles the incoming event.\n\n## Guidelines for DBOS Code\n\n**Process boundary awareness**: External adapter methods may execute in a different process from the workflow. They must communicate exclusively through the database — no local state, no asyncio task references. Internal adapter methods are co-located with the workflow and can use process-local state when needed.\n\n**Don't cancel workflows on shutdown**: DBOS automatically recovers incomplete workflows belonging to the replica's `executor_id` on startup. Cancelling them during shutdown would prevent recovery.\n\n**Use asyncio for process-local coordination**: DBOS durable messages persist in the DB and replay on recovery. Don't use them for ephemeral control flow — use normal asyncio primitives instead.\n\n**Be aware of the replica model**: Code that assumes single-process (e.g. in-memory tracking of all active runs, direct asyncio Future manipulation across adapter boundaries) will break when replicas are involved. Always consider whether the code path might cross a process boundary.\n"
  },
  {
    "path": "packages/llama-agents-dbos/CHANGELOG.md",
    "content": "# llama-agents-dbos\n\n## 0.3.1\n\n### Patch Changes\n\n- 83d5f9f: Use DBOS async send for internal workflow ticks.\n\n## 0.3.0\n\n### Minor Changes\n\n- 56701a9: Add `max_recovery_attempts` to `DBOSRuntimeConfig`. When set, it is forwarded to the `@DBOS.workflow` decorator wrapping the runtime's control loop.\n\n## 0.2.3\n\n### Patch Changes\n\n- 95d8c2b: Share a single asyncpg pool across DBOSRuntime, PostgresWorkflowStore, and ExecutorLeaseManager instead of each opening their own. Pool size is configurable via `DBOSRuntimeConfig.pool_size`. Also adds LISTEN reconnect with backoff to PostgresWorkflowStore.\n\n## 0.2.2\n\n### Patch Changes\n\n- f7e037e: Stream ticks during resume so peak memory is bounded by batch size rather than total tick history.\n\n## 0.2.1\n\n### Patch Changes\n\n- 85c78a2: Fix crash recovery determinism errors by trimming DBOS operation rows that ran ahead of the workflow journal\n\n## 0.2.0\n\n### Minor Changes\n\n- b32ec53: Drop python 3.9 support\n\n### Patch Changes\n\n- 2535e1f: fix dbos launch running multiple loops\n\n## 0.1.2\n\n### Patch Changes\n\n- 5e7f9e5: Add event input/output summaries to step spans and rehydrate span context across serialization boundaries. Log instead of fail cancelled steps from cancelled workflows. Do not fail from wait_for_event exceptions.\n\n## 0.1.1\n\n### Patch Changes\n\n- 6605457: Bump dependency requirements\n- 6ec262c: Fix graceful teardown leading to poisoned DBOS workflow\n\n## 0.1.0\n\n### Minor Changes\n\n- d56be47: Add postgres and DBOS support to the workflow server\n- 57902d5: Add alternate DBOS runtime plugin for running workflows against a DBOS backend\n\n### Patch Changes\n\n- 77a3f9c: Add workflow release for idle DBOS workflows (with replica support)\n- 96e437e: Move task execution into the runtime, for maximal control of specific runtime semantics around determinism\n\n## 0.1.0-rc.1\n\n### Patch Changes\n\n- 3720c61: Add workflow release for idle DBOS workflows (with replica support)\n- a2aad32: Move task execution into the runtime, for maximal control of specific runtime semantics around determinism\n\n## 0.1.0-rc.0\n\n### Minor Changes\n\n- c2e7f17: Add postgres and DBOS support to the workflow server\n- 79159f0: Add alternate DBOS runtime plugin for running workflows against a DBOS backend\n"
  },
  {
    "path": "packages/llama-agents-dbos/README.md",
    "content": "# LlamaAgents DBOS Runtime\n\nDBOS durable runtime plugin for LlamaIndex Workflows.\n\n## Installation\n\n```bash\npip install llama-agents-dbos\n```\n\n## Usage\n\n```python\nimport asyncio\nfrom llama_agents.dbos import DBOSRuntime\nfrom dbos import DBOS, DBOSConfig\nfrom workflows import Workflow, step, StartEvent, StopEvent\n\n# Configure DBOS\nconfig: DBOSConfig = {\n    \"name\": \"my-app\",\n    \"system_database_url\": \"postgresql://...\",\n}\nDBOS(config=config)\n\n# Create runtime and workflow\nruntime = DBOSRuntime()\n\nclass MyWorkflow(Workflow):\n    @step\n    async def my_step(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\nworkflow = MyWorkflow(runtime=runtime)\n\n# launch_sync() works outside async contexts; use await runtime.launch() inside one\nruntime.launch_sync()\n\nasync def main():\n    result = await workflow.run()\n\nasyncio.run(main())\n```\n\n## Features\n\n- Durable workflow execution backed by DBOS\n- Automatic step recording and replay\n- Distributed workers and recovery support\n"
  },
  {
    "path": "packages/llama-agents-dbos/conftest.py",
    "content": "\"\"\"Root conftest.py - shared test utilities for all test directories.\n\nThis file is discovered by pytest and provides common utilities\nfor both tests/ (SQLite) and tests_postgres/ (PostgreSQL).\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom dbos import DBOSConfig\n\n\ndef make_test_dbos_config(\n    name: str,\n    db_path: Path,\n) -> DBOSConfig:\n    \"\"\"Create a DBOS config for testing with sensible defaults (SQLite backend).\n\n    Args:\n        name: The application name for DBOS.\n        db_path: Path to the SQLite database file.\n\n    Returns:\n        A DBOSConfig dictionary ready for use with DBOS().\n    \"\"\"\n    system_db_url = f\"sqlite+pysqlite:///{db_path}?check_same_thread=false\"\n    return {\n        \"name\": name,\n        \"system_database_url\": system_db_url,\n        \"run_admin_server\": False,\n        \"notification_listener_polling_interval_sec\": 0.01,\n    }\n"
  },
  {
    "path": "packages/llama-agents-dbos/package.json",
    "content": "{\n  \"name\": \"llama-agents-dbos\",\n  \"version\": \"0.3.1\",\n  \"private\": false,\n  \"license\": \"MIT\",\n  \"scripts\": {}\n}\n"
  },
  {
    "path": "packages/llama-agents-dbos/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.9.10,<0.10.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"basedpyright>=1.31.1\",\n  \"llama-agents-integration-tests\",\n  \"testcontainers[postgres]>=4.0.0\",\n  \"pytest>=8.4.0\",\n  \"pytest-asyncio>=1.0.0\",\n  \"pytest-cov>=6.1.1\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.0.0\",\n  \"ty>=0.0.15\"\n]\n\n[project]\nname = \"llama-agents-dbos\"\nversion = \"0.3.1\"\ndescription = \"DBOS durable runtime plugin for LlamaIndex Workflows\"\nreadme = \"README.md\"\nlicense = \"MIT\"\nrequires-python = \">=3.10\"\ndependencies = [\n  \"dbos>=2.18.0\",\n  \"llama-agents-server[asyncpg]>=0.5.0\",\n  \"llama-index-workflows>=2.19.1,<3.0.0\"\n]\n\n[tool.basedpyright]\ntypeCheckingMode = \"standard\"\npythonVersion = \"3.10\"\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"module\"\nasyncio_default_test_loop_scope = \"module\"\ntestpaths = [\"tests\"]\naddopts = \"-nauto --timeout=60 -m 'not docker'\"\nmarkers = [\n  \"docker: marks tests as requiring Docker (testcontainers/PostgreSQL)\"\n]\n\n[tool.uv.build-backend]\nmodule-name = \"llama_agents.dbos\"\n\n[tool.uv.sources]\nllama-index-workflows = {workspace = true}\nllama-agents-server = {workspace = true}\nllama-agents-integration-tests = {workspace = true}\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nDBOS plugin for LlamaIndex Workflows.\n\nProvides durable workflow execution backed by DBOS with SQL state storage.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom .runtime import DBOSRuntime\n\n__all__ = [\"DBOSRuntime\"]\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/_store/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nSQLITE_MIGRATION_SOURCE: tuple[str, str] = (\n    \"dbos\",\n    \"llama_agents.dbos._store.sqlite.migrations\",\n)\n\nPOSTGRES_MIGRATION_SOURCE: tuple[str, str] = (\n    \"dbos\",\n    \"llama_agents.dbos._store.postgres.migrations\",\n)\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/_store/postgres/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/_store/postgres/migrations/0001_init.sql",
    "content": "-- migration: 1\n\nCREATE TABLE IF NOT EXISTS workflow_journal (\n    id SERIAL PRIMARY KEY,\n    run_id VARCHAR(255) NOT NULL,\n    seq_num INTEGER NOT NULL,\n    task_key VARCHAR(512) NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_workflow_journal_run_id ON workflow_journal (run_id);\n\nCREATE TABLE IF NOT EXISTS run_lifecycle (\n    run_id VARCHAR(255) PRIMARY KEY,\n    state VARCHAR(20) NOT NULL DEFAULT 'active',\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE IF NOT EXISTS executor_leases (\n    slot_id TEXT PRIMARY KEY,\n    holder TEXT,\n    heartbeat_at TIMESTAMPTZ,\n    acquired_at TIMESTAMPTZ\n);\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/_store/postgres/migrations/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/_store/sqlite/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/_store/sqlite/migrations/0001_init.sql",
    "content": "-- migration: 1\n\nCREATE TABLE IF NOT EXISTS workflow_journal (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    run_id TEXT NOT NULL,\n    seq_num INTEGER NOT NULL,\n    task_key TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_workflow_journal_run_id ON workflow_journal (run_id);\n\nCREATE TABLE IF NOT EXISTS run_lifecycle (\n    run_id TEXT PRIMARY KEY,\n    state TEXT NOT NULL DEFAULT 'active',\n    updated_at TEXT NOT NULL\n);\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/_store/sqlite/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/executor_lease.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport uuid\n\nimport asyncpg\nfrom llama_agents.dbos.journal.crud import _qualified_table_ref\nfrom llama_agents.server._pool import PoolProvider\n\nlogger = logging.getLogger(__name__)\n\n\nclass ExecutorLeaseManager:\n    \"\"\"Manages exclusive executor slot leases backed by a Postgres table.\"\"\"\n\n    def __init__(\n        self,\n        pool: PoolProvider,\n        # ``pool_size`` here is the executor slot count (rows in\n        # executor_leases), NOT the asyncpg connection pool size.\n        pool_size: int,\n        heartbeat_interval: float = 10.0,\n        lease_timeout: float = 30.0,\n        slot_prefix: str = \"executor\",\n        schema: str = \"dbos\",\n    ) -> None:\n        self._pool_provider = pool\n        self._pool_size = pool_size\n        self._heartbeat_interval = heartbeat_interval\n        self._lease_timeout = lease_timeout\n        self._slot_prefix = slot_prefix\n        self._schema = schema\n\n        self._holder = str(uuid.uuid4())\n        self._table = _qualified_table_ref(\"executor_leases\", schema)\n        self._pool: asyncpg.Pool | None = None\n        self._slot_id: str | None = None\n        self._heartbeat_task: asyncio.Task[None] | None = None\n        self._lease_lost_event = asyncio.Event()\n\n    @property\n    def executor_id(self) -> str:\n        if self._slot_id is None:\n            raise RuntimeError(\"Lease not acquired\")\n        return self._slot_id\n\n    @property\n    def lease_lost_event(self) -> asyncio.Event:\n        return self._lease_lost_event\n\n    async def _seed_slots(self) -> None:\n        assert self._pool is not None\n        async with self._pool.acquire() as conn:\n            for i in range(self._pool_size):\n                slot_id = f\"{self._slot_prefix}-{i}\"\n                await conn.execute(\n                    f\"\"\"\n                    INSERT INTO {self._table} (slot_id, holder, heartbeat_at, acquired_at)\n                    VALUES ($1, NULL, NULL, NULL)\n                    ON CONFLICT (slot_id) DO NOTHING\n                    \"\"\",\n                    slot_id,\n                )\n\n    async def acquire(self, timeout: float | None = None) -> str:\n        self._pool = await self._pool_provider.get()\n        await self._seed_slots()\n\n        poll = 0.1\n        elapsed = 0.0\n\n        while True:\n            async with self._pool.acquire() as conn:\n                async with conn.transaction():\n                    row = await conn.fetchrow(\n                        f\"\"\"\n                        SELECT slot_id FROM {self._table}\n                        WHERE holder IS NULL\n                           OR heartbeat_at < NOW() - make_interval(secs => $1)\n                        ORDER BY slot_id\n                        LIMIT 1\n                        FOR UPDATE SKIP LOCKED\n                        \"\"\",\n                        self._lease_timeout,\n                    )\n                    if row is not None:\n                        slot_id: str = row[\"slot_id\"]\n                        await conn.execute(\n                            f\"\"\"\n                            UPDATE {self._table}\n                            SET holder = $1, heartbeat_at = NOW(), acquired_at = NOW()\n                            WHERE slot_id = $2\n                            \"\"\",\n                            self._holder,\n                            slot_id,\n                        )\n                        self._slot_id = slot_id\n                        self._heartbeat_task = asyncio.create_task(\n                            self._heartbeat_loop()\n                        )\n                        logger.info(\"Acquired lease on slot %s\", slot_id)\n                        return slot_id\n\n            if timeout is not None and elapsed >= timeout:\n                await self._pool_provider.close()\n                self._pool = None\n                raise TimeoutError(\n                    f\"Could not acquire executor lease within {timeout}s\"\n                )\n\n            await asyncio.sleep(poll)\n            elapsed += poll\n            poll = min(poll * 2, 2.0)\n\n    async def release(self) -> None:\n        if self._heartbeat_task is not None:\n            self._heartbeat_task.cancel()\n            try:\n                await self._heartbeat_task\n            except asyncio.CancelledError:\n                pass\n            self._heartbeat_task = None\n\n        if self._pool is not None and self._slot_id is not None:\n            async with self._pool.acquire() as conn:\n                await conn.execute(\n                    f\"\"\"\n                    UPDATE {self._table}\n                    SET holder = NULL, heartbeat_at = NULL\n                    WHERE slot_id = $1 AND holder = $2\n                    \"\"\",\n                    self._slot_id,\n                    self._holder,\n                )\n            logger.info(\"Released lease on slot %s\", self._slot_id)\n\n        self._slot_id = None\n\n        if self._pool is not None:\n            await self._pool_provider.close()\n            self._pool = None\n\n    async def _heartbeat_loop(self) -> None:\n        assert self._pool is not None\n        while True:\n            await asyncio.sleep(self._heartbeat_interval)\n            try:\n                async with self._pool.acquire() as conn:\n                    row = await conn.fetchrow(\n                        f\"\"\"\n                        UPDATE {self._table}\n                        SET heartbeat_at = NOW()\n                        WHERE slot_id = $1 AND holder = $2\n                        RETURNING slot_id\n                        \"\"\",\n                        self._slot_id,\n                        self._holder,\n                    )\n                    if row is None:\n                        logger.warning(\n                            \"Lease lost on slot %s — holder no longer matches\",\n                            self._slot_id,\n                        )\n                        self._lease_lost_event.set()\n                        return\n            except Exception:\n                logger.exception(\"Heartbeat failed for slot %s\", self._slot_id)\n\n    async def __aenter__(self) -> ExecutorLeaseManager:\n        await self.acquire()\n        return self\n\n    async def __aexit__(self, *exc: object) -> None:\n        await self.release()\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/idle_release.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Idle detection and release for DBOS-backed workflows.\n\nUses a ``RunLifecycleLock`` to coordinate the release/resume state machine\n(active → releasing → released → active) across replicas. See\n``packages/llama-agents-dbos/ARCHITECTURE.md`` for details.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom collections.abc import Awaitable, Callable, Coroutine\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nfrom llama_agents.dbos.journal.crud import JournalCrud\nfrom llama_agents.dbos.journal.lifecycle import RunLifecycleLock, RunLifecycleState\nfrom llama_agents.server._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    stream_workflow_ticks,\n)\nfrom typing_extensions import override\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import infer_state_type\nfrom workflows.events import Event, WorkflowIdleEvent\nfrom workflows.runtime.control_loop import (\n    rebuild_state_from_ticks,\n    rebuild_state_from_ticks_stream,\n)\nfrom workflows.runtime.runtime_decorators import (\n    BaseExternalRunAdapterDecorator,\n    BaseInternalRunAdapterDecorator,\n    BaseRuntimeDecorator,\n)\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    WaitResult,\n    WaitResultTick,\n)\nfrom workflows.runtime.types.ticks import (\n    TickIdleRelease,\n    WorkflowTick,\n)\nfrom workflows.workflow import Workflow\n\nfrom dbos import DBOS\n\nlogger = logging.getLogger(__name__)\n\n\n# How long to wait before declaring a \"releasing\" state as crashed\nCRASH_TIMEOUT_SECONDS = 120.0\n\n\nclass _DBOSIdleReleaseInternalRunAdapter(BaseInternalRunAdapterDecorator):\n    \"\"\"Internal adapter that detects idle events and schedules release.\"\"\"\n\n    def __init__(\n        self,\n        decorated: InternalRunAdapter,\n        runtime: DBOSIdleReleaseDecorator,\n        store: AbstractWorkflowStore,\n    ) -> None:\n        super().__init__(decorated)\n        self._runtime = runtime\n        self._store = store\n\n    @override\n    async def wait_receive(\n        self,\n        timeout_seconds: float | None = None,\n    ) -> WaitResult:\n        result = await super().wait_receive(timeout_seconds)\n        if isinstance(result, WaitResultTick):\n            self._runtime._cancel_deferred_release(self.run_id)\n        return result\n\n    @override\n    async def write_to_event_stream(self, event: Event) -> None:\n        await super().write_to_event_stream(event)\n        if isinstance(event, WorkflowIdleEvent):\n            self._runtime._schedule_deferred_release(self.run_id)\n\n\nclass DBOSIdleReleaseExternalRunAdapter(BaseExternalRunAdapterDecorator):\n    \"\"\"Proxy adapter that adds reload-on-demand for idle-released DBOS handlers.\n\n    The inner adapter is resolved lazily because ``get_external_adapter`` is\n    sync but reload (continue-as-new) is async.\n    \"\"\"\n\n    def __init__(self, runtime: DBOSIdleReleaseDecorator, run_id: str) -> None:\n        # Intentionally skip super().__init__ -- _decorated is a lazy property.\n        self._runtime = runtime\n        self._run_id = run_id\n\n    @property  # type: ignore[override]\n    def _decorated(self) -> ExternalRunAdapter:\n        return self._runtime._decorated.get_external_adapter(self._run_id)\n\n    @_decorated.setter\n    def _decorated(self, value: ExternalRunAdapter) -> None:\n        pass\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    @override\n    async def send_event(self, tick: WorkflowTick) -> None:\n        lifecycle = await self._runtime._get_lifecycle()\n        while True:\n            result = await lifecycle.try_begin_resume(\n                self.run_id, crash_timeout_seconds=CRASH_TIMEOUT_SECONDS\n            )\n            if result is None:\n                await self._decorated.send_event(tick)\n                return\n            if result == RunLifecycleState.released:\n                await self._runtime._do_resume(self.run_id, pending_tick=tick)\n                return\n            # releasing — poll until it completes or times out\n            await asyncio.sleep(0.5)\n\n\nclass DBOSIdleReleaseDecorator(BaseRuntimeDecorator):\n    \"\"\"Runtime decorator for idle detection, release via TickIdleRelease,\n    and reload via reusing the same run_id for DBOS-backed workflows.\n\n    Uses a distributed lifecycle lock to coordinate release/resume across\n    replicas. The state machine is: active → releasing → released → active.\n\n    Must wrap an EventInterceptorDecorator (or compatible runtime) that\n    wraps a DBOSRuntime.\n    \"\"\"\n\n    def __init__(\n        self,\n        decorated: BaseRuntimeDecorator,\n        store: AbstractWorkflowStore,\n        idle_timeout: float = 60.0,\n        journal_crud: Callable[[], JournalCrud] | None = None,\n        lifecycle_lock: Callable[[], Awaitable[RunLifecycleLock]]\n        | Callable[[], RunLifecycleLock]\n        | None = None,\n    ) -> None:\n        super().__init__(decorated)\n        self._store = store\n        self._deferred_release_tasks: dict[str, asyncio.Task[None]] = {}\n        self._background_tasks: set[asyncio.Task[None]] = set()\n        self._idle_timeout = idle_timeout\n        self._workflows: dict[str, Workflow] = {}\n        self._journal_crud_factory = journal_crud\n        self._journal_crud_instance: JournalCrud | None = None\n        if lifecycle_lock is None:\n            raise ValueError(\"lifecycle_lock is required\")\n        self._lifecycle_lock_factory = lifecycle_lock\n        self._lifecycle_lock_instance: RunLifecycleLock | None = None\n\n    @property\n    def _journal_crud(self) -> JournalCrud | None:\n        if self._journal_crud_factory is None:\n            return None\n        if self._journal_crud_instance is None:\n            self._journal_crud_instance = self._journal_crud_factory()\n        return self._journal_crud_instance\n\n    async def _get_lifecycle(self) -> RunLifecycleLock:\n        if self._lifecycle_lock_instance is None:\n            result = self._lifecycle_lock_factory()\n            if isinstance(result, Awaitable):\n                self._lifecycle_lock_instance = await result  # type: ignore[ty:invalid-assignment]\n            else:\n                self._lifecycle_lock_instance = result\n        return self._lifecycle_lock_instance  # type: ignore[ty:invalid-return-type]\n\n    @override\n    def track_workflow(self, workflow: Workflow) -> None:\n        self._workflows[workflow.workflow_name] = workflow\n        super().track_workflow(workflow)\n\n    @override\n    def untrack_workflow(self, workflow: Workflow) -> None:\n        self._workflows.pop(workflow.workflow_name, None)\n        super().untrack_workflow(workflow)\n\n    def _spawn_task(self, coro: Coroutine[Any, Any, None]) -> asyncio.Task[None]:\n        task = asyncio.create_task(coro)\n        self._background_tasks.add(task)\n        task.add_done_callback(self._background_tasks.discard)\n        return task\n\n    def _schedule_deferred_release(self, run_id: str) -> None:\n        \"\"\"Cancel any existing timer for run_id and schedule a new one.\"\"\"\n        self._cancel_deferred_release(run_id)\n        task = self._spawn_task(self._deferred_release(run_id))\n        self._deferred_release_tasks[run_id] = task\n\n    def _cancel_deferred_release(self, run_id: str) -> None:\n        \"\"\"Cancel a pending deferred release timer for run_id, if any.\"\"\"\n        task = self._deferred_release_tasks.pop(run_id, None)\n        if task is not None and not task.done():\n            task.cancel()\n\n    @override\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        inner_adapter = self._decorated.get_internal_adapter(workflow)\n        return _DBOSIdleReleaseInternalRunAdapter(inner_adapter, self, self._store)\n\n    @override\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        return DBOSIdleReleaseExternalRunAdapter(self, run_id)\n\n    async def _deferred_release(self, run_id: str) -> None:\n        \"\"\"Wait for idle_timeout then release the handler if still idle.\"\"\"\n        await asyncio.sleep(self._idle_timeout)\n        self._deferred_release_tasks.pop(run_id, None)\n        await self._release_idle_handler(run_id)\n\n    async def _release_idle_handler(self, run_id: str) -> None:\n        \"\"\"Release an idle handler by sending TickIdleRelease.\"\"\"\n        lifecycle = await self._get_lifecycle()\n        if not await lifecycle.begin_release(run_id):\n            return\n\n        external = self._decorated.get_external_adapter(run_id)\n        await external.send_event(TickIdleRelease())\n        logger.info(f\"Released idle DBOS handler [run_id={run_id}]\")\n\n        self._spawn_task(self._await_and_mark_released(run_id, external))\n\n    async def _await_and_mark_released(\n        self, run_id: str, external: ExternalRunAdapter\n    ) -> None:\n        \"\"\"Await workflow completion, then mark as released and set idle_since.\"\"\"\n        try:\n            await external.get_result()\n\n            lifecycle = await self._get_lifecycle()\n            await lifecycle.complete_release(run_id)\n\n            # Set idle_since NOW — after the workflow is fully released\n            await self._store.update_handler_status(\n                run_id, status=\"running\", idle_since=datetime.now(timezone.utc)\n            )\n\n            logger.info(f\"Marked handler as released [run_id={run_id}]\")\n        except Exception:\n            logger.warning(\n                f\"Failed to mark released for run_id={run_id}\", exc_info=True\n            )\n\n    async def _broker_state_from_ticks(\n        self, workflow: Workflow, run_id: str\n    ) -> BrokerState:\n        \"\"\"Rebuild BrokerState from persisted ticks.\"\"\"\n        init_state = BrokerState.from_workflow(workflow)\n        return await rebuild_state_from_ticks_stream(\n            init_state, stream_workflow_ticks(self._store, run_id)\n        )\n\n    async def _do_resume(\n        self,\n        run_id: str,\n        pending_tick: WorkflowTick | None = None,\n    ) -> tuple[str, ExternalRunAdapter]:\n        \"\"\"Resume a workflow that was previously idle-released.\n\n        Waits for the old DBOS workflow to finish (works cross-replica),\n        purges DBOS/journal state, rebuilds from ticks, and starts a fresh\n        DBOS workflow with the same run_id.\n\n        Args:\n            run_id: The workflow run ID to resume.\n            pending_tick: An optional tick to include in the rebuilt state\n                before starting the workflow. This avoids a race where the\n                resumed workflow goes idle again before the tick is delivered.\n\n        Returns (run_id, external_adapter).\n        \"\"\"\n        self._cancel_deferred_release(run_id)\n\n        # Wait for old DBOS workflow to finish (cross-replica safe)\n        try:\n            handle = await DBOS.retrieve_workflow_async(run_id)\n            await handle.get_result()\n        except Exception:\n            logger.warning(\n                f\"Failed to await old DBOS workflow for run_id={run_id}\",\n                exc_info=True,\n            )\n\n        # Look up handler to get workflow_name\n        handlers = await self._store.query(HandlerQuery(run_id_in=[run_id]))\n        if len(handlers) != 1:\n            raise ValueError(\n                f\"Expected 1 handler for run {run_id}, got {len(handlers)}\"\n            )\n        handler = handlers[0]\n\n        workflow = self._workflows.get(handler.workflow_name)\n        if workflow is None:\n            raise ValueError(f\"Workflow {handler.workflow_name} not found\")\n\n        # Rebuild BrokerState from persisted ticks\n        init_state = await self._broker_state_from_ticks(workflow, run_id)\n\n        # Include the pending tick in the rebuilt state so the control loop\n        # has it queued before it starts processing.\n        if pending_tick is not None:\n            init_state = rebuild_state_from_ticks(init_state, [pending_tick])\n\n        # Carry over state from old run's state store\n        serializer = JsonSerializer()\n        serialized_state: dict[str, Any] | None = None\n        state_type = infer_state_type(workflow)\n        if state_type is not None:\n            try:\n                old_state_store = self._store.create_state_store(\n                    run_id, state_type=state_type\n                )\n                serialized_state = old_state_store.to_dict(serializer)\n            except Exception:\n                logger.warning(\n                    f\"Failed to carry over state from run {run_id}\", exc_info=True\n                )\n\n        # Purge DBOS state and journal so the same run_id can be reused.\n        try:\n            await DBOS.delete_workflow_async(run_id)\n        except Exception:\n            logger.debug(\n                f\"DBOS state already purged for run_id={run_id}\", exc_info=True\n            )\n        if self._journal_crud is not None:\n            try:\n                await self._journal_crud.delete(run_id)\n            except Exception:\n                logger.debug(\n                    f\"Journal already purged for run_id={run_id}\", exc_info=True\n                )\n\n        # Start new workflow run with the same run_id.\n        new_adapter = self._decorated.run_workflow(\n            run_id,\n            workflow,\n            init_state,\n            serialized_state=serialized_state,\n            serializer=serializer,\n        )\n\n        handler.status = \"running\"\n        handler.updated_at = datetime.now(timezone.utc)\n        handler.idle_since = None\n        await self._store.update(handler)\n\n        logger.info(f\"Resumed DBOS workflow [run_id={run_id}]\")\n        return run_id, new_adapter\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/journal/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Journal module for recording task completion order.\"\"\"\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/journal/crud.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"CRUD operations for the workflow journal using native database drivers.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nimport sqlite3\nfrom abc import ABC, abstractmethod\nfrom contextlib import contextmanager\nfrom typing import Iterator\n\nimport asyncpg\n\nJOURNAL_TABLE_NAME = \"workflow_journal\"\n\n_VALID_IDENTIFIER = re.compile(r\"^[A-Za-z_][A-Za-z0-9_]*$\")\n\n\ndef _quote_identifier(name: str) -> str:\n    \"\"\"Quote a SQL identifier, raising on invalid names.\"\"\"\n    if not _VALID_IDENTIFIER.match(name):\n        msg = f\"Invalid SQL identifier: {name!r}\"\n        raise ValueError(msg)\n    return f'\"{name}\"'\n\n\ndef _qualified_table_ref(table_name: str, schema: str | None = None) -> str:\n    \"\"\"Build a safely-quoted qualified table reference.\"\"\"\n    ref = _quote_identifier(table_name)\n    if schema:\n        ref = f\"{_quote_identifier(schema)}.{ref}\"\n    return ref\n\n\nclass JournalCrud(ABC):\n    \"\"\"Abstract base for journal CRUD operations.\"\"\"\n\n    @abstractmethod\n    async def insert(self, run_id: str, seq_num: int, task_key: str) -> None: ...\n\n    @abstractmethod\n    async def load(self, run_id: str) -> list[str]: ...\n\n    @abstractmethod\n    async def delete(self, run_id: str) -> None: ...\n\n    @abstractmethod\n    async def truncate_from(self, run_id: str, seq_num: int) -> None: ...\n\n    @abstractmethod\n    async def purge_operations_from(self, run_id: str, function_id: int) -> None:\n        \"\"\"Delete DBOS operation_outputs rows beyond the given function_id.\"\"\"\n        ...\n\n\nclass PostgresJournalCrud(JournalCrud):\n    \"\"\"Journal CRUD using asyncpg.\"\"\"\n\n    def __init__(\n        self,\n        pool: asyncpg.Pool,\n        table_name: str = JOURNAL_TABLE_NAME,\n        schema: str | None = None,\n    ) -> None:\n        self._pool = pool\n        self._table_ref = _qualified_table_ref(table_name, schema)\n        self._ops_table_ref = _qualified_table_ref(\"operation_outputs\", schema)\n\n    async def insert(self, run_id: str, seq_num: int, task_key: str) -> None:\n        await self._pool.execute(\n            f\"INSERT INTO {self._table_ref} (run_id, seq_num, task_key) VALUES ($1, $2, $3)\",\n            run_id,\n            seq_num,\n            task_key,\n        )\n\n    async def load(self, run_id: str) -> list[str]:\n        rows = await self._pool.fetch(\n            f\"SELECT task_key FROM {self._table_ref} WHERE run_id = $1 ORDER BY seq_num ASC\",\n            run_id,\n        )\n        return [row[\"task_key\"] for row in rows]\n\n    async def delete(self, run_id: str) -> None:\n        await self._pool.execute(\n            f\"DELETE FROM {self._table_ref} WHERE run_id = $1\",\n            run_id,\n        )\n\n    async def truncate_from(self, run_id: str, seq_num: int) -> None:\n        await self._pool.execute(\n            f\"DELETE FROM {self._table_ref} WHERE run_id = $1 AND seq_num >= $2\",\n            run_id,\n            seq_num,\n        )\n\n    async def purge_operations_from(self, run_id: str, function_id: int) -> None:\n        await self._pool.execute(\n            f\"DELETE FROM {self._ops_table_ref} \"\n            f\"WHERE workflow_uuid = $1 AND function_id > $2\",\n            run_id,\n            function_id,\n        )\n\n\nclass SqliteJournalCrud(JournalCrud):\n    \"\"\"Journal CRUD using sqlite3.\"\"\"\n\n    def __init__(\n        self,\n        db_path: str,\n        table_name: str = JOURNAL_TABLE_NAME,\n    ) -> None:\n        self._db_path = db_path\n        self._table_ref = _quote_identifier(table_name)\n        self._ops_table_ref = _quote_identifier(\"operation_outputs\")\n\n    @contextmanager\n    def _connect(self) -> Iterator[sqlite3.Connection]:\n        conn = sqlite3.connect(self._db_path)\n        try:\n            yield conn\n        finally:\n            conn.close()\n\n    async def insert(self, run_id: str, seq_num: int, task_key: str) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                f\"INSERT INTO {self._table_ref} (run_id, seq_num, task_key) VALUES (?, ?, ?)\",\n                (run_id, seq_num, task_key),\n            )\n            conn.commit()\n\n    async def load(self, run_id: str) -> list[str]:\n        with self._connect() as conn:\n            cursor = conn.execute(\n                f\"SELECT task_key FROM {self._table_ref} WHERE run_id = ? ORDER BY seq_num ASC\",\n                (run_id,),\n            )\n            return [row[0] for row in cursor.fetchall()]\n\n    async def delete(self, run_id: str) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                f\"DELETE FROM {self._table_ref} WHERE run_id = ?\",\n                (run_id,),\n            )\n            conn.commit()\n\n    async def truncate_from(self, run_id: str, seq_num: int) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                f\"DELETE FROM {self._table_ref} WHERE run_id = ? AND seq_num >= ?\",\n                (run_id, seq_num),\n            )\n            conn.commit()\n\n    async def purge_operations_from(self, run_id: str, function_id: int) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                f\"DELETE FROM {self._ops_table_ref} \"\n                \"WHERE workflow_uuid = ? AND function_id > ?\",\n                (run_id, function_id),\n            )\n            conn.commit()\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/journal/lifecycle.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Distributed lifecycle lock for coordinating idle release/resume across replicas.\"\"\"\n\nfrom __future__ import annotations\n\nimport sqlite3\nfrom abc import ABC, abstractmethod\nfrom contextlib import contextmanager\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom typing import Iterator\n\nimport asyncpg\nfrom llama_agents.dbos.journal.crud import _qualified_table_ref, _quote_identifier\nfrom llama_agents.server._keyed_lock import KeyedLock\n\nLIFECYCLE_TABLE_NAME = \"run_lifecycle\"\n\n\nclass RunLifecycleState(str, Enum):\n    active = \"active\"\n    releasing = \"releasing\"\n    released = \"released\"\n\n\nclass RunLifecycleLock(ABC):\n    \"\"\"Abstract base for the run lifecycle lock.\n\n    State machine: active -> releasing -> released -> active\n    \"\"\"\n\n    @abstractmethod\n    async def create(self, run_id: str) -> None:\n        \"\"\"Insert row with state='active'. Called when workflow starts.\"\"\"\n        ...\n\n    @abstractmethod\n    async def begin_release(self, run_id: str) -> bool:\n        \"\"\"CAS: active -> releasing. Returns True on success.\"\"\"\n        ...\n\n    @abstractmethod\n    async def complete_release(self, run_id: str) -> None:\n        \"\"\"releasing -> released.\"\"\"\n        ...\n\n    @abstractmethod\n    async def try_begin_resume(\n        self, run_id: str, crash_timeout_seconds: float | None = None\n    ) -> RunLifecycleState | None:\n        \"\"\"Attempt to claim resume.\n\n        Returns:\n            None: no row or 'active' - send normally\n            released: transitioned to 'active', caller owns resume\n            releasing: in progress, caller should wait and retry\n\n        If crash_timeout_seconds is set and the current state is 'releasing'\n        with an updated_at older than the timeout, force-transitions to\n        'active' and returns 'released' (caller owns resume).\n        \"\"\"\n        ...\n\n\nclass PostgresRunLifecycleLock(RunLifecycleLock):\n    \"\"\"Lifecycle lock using asyncpg with SELECT FOR UPDATE.\"\"\"\n\n    def __init__(\n        self,\n        pool: asyncpg.Pool,\n        table_name: str = LIFECYCLE_TABLE_NAME,\n        schema: str | None = None,\n    ) -> None:\n        self._pool = pool\n        self._table_ref = _qualified_table_ref(table_name, schema)\n\n    async def create(self, run_id: str) -> None:\n        await self._pool.execute(\n            f\"INSERT INTO {self._table_ref} (run_id, state, updated_at) \"\n            f\"VALUES ($1, $2, $3) \"\n            f\"ON CONFLICT (run_id) DO UPDATE SET state = $2, updated_at = $3\",\n            run_id,\n            RunLifecycleState.active.value,\n            datetime.now(timezone.utc),\n        )\n\n    async def begin_release(self, run_id: str) -> bool:\n        row = await self._pool.fetchrow(\n            f\"UPDATE {self._table_ref} SET state = $1, updated_at = $2 \"\n            f\"WHERE run_id = $3 AND state = $4 RETURNING run_id\",\n            RunLifecycleState.releasing.value,\n            datetime.now(timezone.utc),\n            run_id,\n            RunLifecycleState.active.value,\n        )\n        return row is not None\n\n    async def complete_release(self, run_id: str) -> None:\n        await self._pool.execute(\n            f\"UPDATE {self._table_ref} SET state = $1, updated_at = $2 \"\n            f\"WHERE run_id = $3 AND state = $4\",\n            RunLifecycleState.released.value,\n            datetime.now(timezone.utc),\n            run_id,\n            RunLifecycleState.releasing.value,\n        )\n\n    async def try_begin_resume(\n        self, run_id: str, crash_timeout_seconds: float | None = None\n    ) -> RunLifecycleState | None:\n        async with self._pool.acquire() as conn:\n            async with conn.transaction():\n                row = await conn.fetchrow(\n                    f\"SELECT state, updated_at FROM {self._table_ref} \"\n                    f\"WHERE run_id = $1 FOR UPDATE\",\n                    run_id,\n                )\n                if row is None:\n                    return None\n                state = RunLifecycleState(row[\"state\"])\n                if state == RunLifecycleState.active:\n                    return None\n                if state == RunLifecycleState.released or (\n                    state == RunLifecycleState.releasing\n                    and crash_timeout_seconds is not None\n                    and (datetime.now(timezone.utc) - row[\"updated_at\"]).total_seconds()\n                    > crash_timeout_seconds\n                ):\n                    await conn.execute(\n                        f\"UPDATE {self._table_ref} SET state = $1, updated_at = $2 \"\n                        f\"WHERE run_id = $3\",\n                        RunLifecycleState.active.value,\n                        datetime.now(timezone.utc),\n                        run_id,\n                    )\n                    return RunLifecycleState.released\n                # releasing\n                return RunLifecycleState.releasing\n\n\nclass SqliteRunLifecycleLock(RunLifecycleLock):\n    \"\"\"Lifecycle lock using sqlite3 with process-local KeyedLock for serialization.\"\"\"\n\n    def __init__(\n        self,\n        db_path: str,\n        table_name: str = LIFECYCLE_TABLE_NAME,\n    ) -> None:\n        self._db_path = db_path\n        self._table_ref = _quote_identifier(table_name)\n        self._lock = KeyedLock()\n\n    @contextmanager\n    def _connect(self) -> Iterator[sqlite3.Connection]:\n        conn = sqlite3.connect(self._db_path)\n        conn.row_factory = sqlite3.Row\n        try:\n            yield conn\n        finally:\n            conn.close()\n\n    async def create(self, run_id: str) -> None:\n        async with self._lock(run_id):\n            with self._connect() as conn:\n                conn.execute(\n                    f\"INSERT OR REPLACE INTO {self._table_ref} (run_id, state, updated_at) \"\n                    f\"VALUES (?, ?, ?)\",\n                    (\n                        run_id,\n                        RunLifecycleState.active.value,\n                        datetime.now(timezone.utc).isoformat(),\n                    ),\n                )\n                conn.commit()\n\n    async def begin_release(self, run_id: str) -> bool:\n        async with self._lock(run_id):\n            with self._connect() as conn:\n                cursor = conn.execute(\n                    f\"UPDATE {self._table_ref} SET state = ?, updated_at = ? \"\n                    f\"WHERE run_id = ? AND state = ?\",\n                    (\n                        RunLifecycleState.releasing.value,\n                        datetime.now(timezone.utc).isoformat(),\n                        run_id,\n                        RunLifecycleState.active.value,\n                    ),\n                )\n                conn.commit()\n                return cursor.rowcount > 0\n\n    async def complete_release(self, run_id: str) -> None:\n        async with self._lock(run_id):\n            with self._connect() as conn:\n                conn.execute(\n                    f\"UPDATE {self._table_ref} SET state = ?, updated_at = ? \"\n                    f\"WHERE run_id = ? AND state = ?\",\n                    (\n                        RunLifecycleState.released.value,\n                        datetime.now(timezone.utc).isoformat(),\n                        run_id,\n                        RunLifecycleState.releasing.value,\n                    ),\n                )\n                conn.commit()\n\n    async def try_begin_resume(\n        self, run_id: str, crash_timeout_seconds: float | None = None\n    ) -> RunLifecycleState | None:\n        async with self._lock(run_id):\n            with self._connect() as conn:\n                row = conn.execute(\n                    f\"SELECT state, updated_at FROM {self._table_ref} WHERE run_id = ?\",\n                    (run_id,),\n                ).fetchone()\n                if row is None:\n                    return None\n                state = RunLifecycleState(row[\"state\"])\n                if state == RunLifecycleState.active:\n                    return None\n                if state == RunLifecycleState.released or (\n                    state == RunLifecycleState.releasing\n                    and crash_timeout_seconds is not None\n                    and (\n                        datetime.now(timezone.utc)\n                        - datetime.fromisoformat(row[\"updated_at\"])\n                    ).total_seconds()\n                    > crash_timeout_seconds\n                ):\n                    conn.execute(\n                        f\"UPDATE {self._table_ref} SET state = ?, updated_at = ? WHERE run_id = ?\",\n                        (\n                            RunLifecycleState.active.value,\n                            datetime.now(timezone.utc).isoformat(),\n                            run_id,\n                        ),\n                    )\n                    conn.commit()\n                    return RunLifecycleState.released\n                # releasing\n                return RunLifecycleState.releasing\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/journal/task_journal.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"TaskJournal for deterministic replay of task completion order.\"\"\"\n\nfrom __future__ import annotations\n\nfrom .crud import JournalCrud\n\n\nclass TaskJournal:\n    \"\"\"Records task completion order for deterministic replay.\n\n    Stores NamedTask string keys directly (e.g., \"step_name:0\", \"__pull__:1\").\n    During fresh execution, records which tasks complete and in what order.\n    During replay, returns the expected task key so the adapter can wait for\n    the specific task that completed in the original run.\n\n    Uses a dedicated workflow_journal table with one row per entry for efficient\n    append-only storage.\n    \"\"\"\n\n    def __init__(\n        self,\n        run_id: str,\n        crud: JournalCrud | None = None,\n    ) -> None:\n        \"\"\"Initialize the task journal.\n\n        Args:\n            run_id: Workflow run ID for this journal.\n            crud: Journal CRUD operations. If None, operates in-memory only.\n        \"\"\"\n        self._run_id = run_id\n        self._crud = crud\n        self._entries: list[str] | None = None  # Lazy loaded\n        self._replay_index: int = 0\n\n    async def load(self) -> None:\n        \"\"\"Load journal from database. Idempotent - only loads once.\"\"\"\n        if self._entries is not None:\n            return\n\n        if self._crud is None:\n            self._entries = []\n            return\n\n        self._entries = await self._crud.load(self._run_id)\n\n    def is_replaying(self) -> bool:\n        \"\"\"True if there are more journal entries to replay.\"\"\"\n        if self._entries is None:\n            return False\n        return self._replay_index < len(self._entries)\n\n    def next_expected_key(self) -> str | None:\n        \"\"\"Get the next expected task key during replay, or None if fresh execution.\"\"\"\n        if self._entries is None or self._replay_index >= len(self._entries):\n            return None\n        return self._entries[self._replay_index]\n\n    async def record(self, key: str) -> None:\n        \"\"\"Record a task completion and persist to database.\"\"\"\n        if self._entries is None:\n            self._entries = []\n\n        seq_num = len(self._entries)\n        self._entries.append(key)\n        self._replay_index += 1\n\n        if self._crud is not None:\n            await self._crud.insert(self._run_id, seq_num, key)\n\n    def advance(self) -> None:\n        \"\"\"Advance replay index after processing a replayed task.\"\"\"\n        self._replay_index += 1\n\n    @property\n    def has_entries(self) -> bool:\n        \"\"\"True if the journal has been loaded and contains at least one entry.\"\"\"\n        return self._entries is not None and len(self._entries) > 0\n\n    async def purge_stale(self, current_fid: int) -> None:\n        \"\"\"Purge stale journal rows and orphaned operation_outputs beyond current_fid.\n\n        Called at the replay-to-fresh transition to clean up rows left by a\n        previous crashed recovery.\n        \"\"\"\n        if not self.has_entries or self._crud is None or self._entries is None:\n            return\n        await self._crud.purge_operations_from(self._run_id, current_fid)\n        await self._crud.truncate_from(self._run_id, len(self._entries))\n"
  },
  {
    "path": "packages/llama-agents-dbos/src/llama_agents/dbos/runtime.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nDBOS Runtime for durable workflow execution.\n\nThis module provides the DBOSRuntime class for running LlamaIndex workflows\nwith durable execution backed by DBOS.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sqlite3\nimport threading\nimport time\nfrom collections.abc import AsyncIterator, Awaitable, Callable\nfrom typing import Any, AsyncGenerator, TypedDict, cast\n\nimport asyncpg\nfrom llama_agents.client.protocol.serializable_events import (\n    EventEnvelopeWithMetadata,\n)\nfrom llama_agents.dbos._store import POSTGRES_MIGRATION_SOURCE, SQLITE_MIGRATION_SOURCE\nfrom llama_agents.server._pool import PoolProvider\nfrom llama_agents.server._runtime.event_interceptor import EventInterceptorDecorator\nfrom llama_agents.server._runtime.persistence_runtime import TickPersistenceDecorator\nfrom llama_agents.server._store import (\n    POSTGRES_MIGRATION_SOURCE as SERVER_POSTGRES_MIGRATION_SOURCE,\n)\nfrom llama_agents.server._store import (\n    SQLITE_MIGRATION_SOURCE as SERVER_SQLITE_MIGRATION_SOURCE,\n)\nfrom llama_agents.server._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    PersistentHandler,\n    StoredEvent,\n    StoredTick,\n)\nfrom llama_agents.server._store.postgres.migrate import (\n    run_migrations as pg_run_migrations,\n)\nfrom llama_agents.server._store.postgres_state_store import PostgresStateStore\nfrom llama_agents.server._store.postgres_workflow_store import (\n    PostgresWorkflowStore,\n)\nfrom llama_agents.server._store.sqlite.migrate import (\n    run_migrations as sqlite_run_migrations,\n)\nfrom llama_agents.server._store.sqlite.sqlite_state_store import SqliteStateStore\nfrom llama_agents.server._store.sqlite.sqlite_workflow_store import SqliteWorkflowStore\nfrom llama_index_instrumentation import get_dispatcher\nfrom pydantic import BaseModel\nfrom sqlalchemy.engine import URL as SaURL\nfrom sqlalchemy.engine import Engine\nfrom typing_extensions import Unpack\nfrom workflows.context.serializers import BaseSerializer, JsonSerializer\nfrom workflows.context.state_store import (\n    StateStore,\n    deserialize_state_from_dict,\n    infer_state_type,\n)\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.named_task import (\n    NamedTask,\n    PendingStart,\n    all_tasks,\n    find_by_key,\n    get_key,\n)\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    WaitForNextTaskResult,\n    WaitResult,\n    WaitResultTick,\n    WaitResultTimeout,\n)\nfrom workflows.runtime.types.step_function import (\n    StepWorkerFunction,\n    as_step_worker_functions,\n    create_workflow_run_function,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\nfrom workflows.workflow import Workflow\n\nfrom dbos import DBOS, SetWorkflowID, WorkflowHandleAsync\nfrom dbos._context import get_local_dbos_context\nfrom dbos._dbos import _get_dbos_instance\nfrom dbos._error import DBOSNonExistentWorkflowError\n\nfrom .executor_lease import ExecutorLeaseManager\nfrom .idle_release import DBOSIdleReleaseDecorator\nfrom .journal.crud import (\n    JOURNAL_TABLE_NAME,\n    JournalCrud,\n    PostgresJournalCrud,\n    SqliteJournalCrud,\n)\nfrom .journal.lifecycle import (\n    PostgresRunLifecycleLock,\n    RunLifecycleLock,\n    SqliteRunLifecycleLock,\n)\nfrom .journal.task_journal import TaskJournal\n\nSTATE_TABLE_NAME = \"workflow_state\"\n\nlogger = logging.getLogger(__name__)\n\n\nclass DBOSWorkflowStore(AbstractWorkflowStore):\n    \"\"\"Lazy proxy that defers dialect resolution until first use.\n\n    Wraps a factory callable that produces the real store (Postgres or Sqlite).\n    The factory is called once on first access; all abstract methods delegate\n    to the resolved store.\n    \"\"\"\n\n    def __init__(self, factory: Callable[[], AbstractWorkflowStore]) -> None:\n        self._factory = factory\n        self._inner: AbstractWorkflowStore | None = None\n\n    def _resolve(self) -> AbstractWorkflowStore:\n        if self._inner is None:\n            self._inner = self._factory()\n        return self._inner\n\n    @property\n    def poll_interval(self) -> float:  # type: ignore[override]\n        return self._resolve().poll_interval\n\n    def create_state_store(\n        self,\n        run_id: str,\n        state_type: type[Any] | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> StateStore[Any]:\n        return self._resolve().create_state_store(\n            run_id, state_type, serialized_state, serializer\n        )\n\n    async def query(self, query: HandlerQuery) -> list[PersistentHandler]:\n        return await self._resolve().query(query)\n\n    async def update(self, handler: PersistentHandler) -> None:\n        await self._resolve().update(handler)\n\n    async def delete(self, query: HandlerQuery) -> int:\n        return await self._resolve().delete(query)\n\n    async def append_event(self, run_id: str, event: EventEnvelopeWithMetadata) -> None:\n        await self._resolve().append_event(run_id, event)\n\n    async def query_events(\n        self, run_id: str, after_sequence: int | None = None, limit: int | None = None\n    ) -> list[StoredEvent]:\n        return await self._resolve().query_events(run_id, after_sequence, limit)\n\n    async def append_tick(self, run_id: str, tick_data: dict[str, Any]) -> None:\n        await self._resolve().append_tick(run_id, tick_data)\n\n    async def get_ticks(self, run_id: str) -> list[StoredTick]:\n        return await self._resolve().get_ticks(run_id)\n\n    def stream_ticks(self, run_id: str) -> AsyncIterator[StoredTick]:\n        return self._resolve().stream_ticks(run_id)\n\n\nclass ExecutorLeaseConfig(TypedDict, total=False):\n    \"\"\"Configuration for automatic executor lease management.\n\n    When provided, DBOSRuntime will acquire a lease from a pool of executor\n    slots on launch and release it on destroy. The leased slot ID replaces\n    the DBOS executor_id.\n    \"\"\"\n\n    pool_size: int\n    \"\"\"Number of executor slots available. Required.\"\"\"\n    acquire_timeout: float\n    \"\"\"Max seconds to wait for a slot. Default 60.\"\"\"\n    heartbeat_interval: float\n    \"\"\"Seconds between heartbeats. Default 10.\"\"\"\n    lease_timeout: float\n    \"\"\"Seconds before a lease is considered stale. Default 30.\"\"\"\n    slot_prefix: str\n    \"\"\"Prefix for slot names. Default \"executor\".\"\"\"\n\n\nclass DBOSRuntimeConfig(TypedDict, total=False):\n    \"\"\"Configuration options for DBOSRuntime.\n\n    All fields are optional — defaults are resolved at launch time.\n    \"\"\"\n\n    polling_interval_sec: float\n    run_migrations_on_launch: bool\n    schema: str | None\n    state_table_name: str\n    journal_table_name: str\n    pool_size: int\n    pool_min_size: int\n    max_recovery_attempts: int\n    _experimental_executor_lease: ExecutorLeaseConfig | None\n\n\n# Final fallback if neither config nor DBOS sys_db config can be read.\n# Matches asyncpg's stock create_pool default (10) and the previous hardcoded\n# value used here.\n_DEFAULT_POOL_SIZE_FALLBACK = 10\n\n\nDEFAULT_STATE_TABLE_NAME = STATE_TABLE_NAME\nDEFAULT_JOURNAL_TABLE_NAME = JOURNAL_TABLE_NAME\n\n\ndef _resolve_schema(config: DBOSRuntimeConfig, engine: Engine) -> str | None:\n    \"\"\"Resolve schema from config, falling back to dialect-based default.\n\n    If \"schema\" was explicitly provided (even as None), uses that value.\n    Otherwise, defaults to \"dbos\" for PostgreSQL and None for SQLite.\n    \"\"\"\n    if \"schema\" in config:\n        return config[\"schema\"]\n    is_postgres = engine.dialect.name == \"postgresql\"\n    return \"dbos\" if is_postgres else None\n\n\ndef _sqlalchemy_url_to_asyncpg_dsn(url: SaURL) -> str:\n    \"\"\"Convert a SQLAlchemy URL to an asyncpg-compatible DSN.\n\n    Strips dialect driver suffixes (e.g. postgresql+psycopg2 -> postgresql)\n    and renders the URL as a plain connection string.\n    \"\"\"\n    # url is a sqlalchemy.engine.URL object\n    # Set the drivername to plain 'postgresql' for asyncpg\n    plain_url = url.set(drivername=\"postgresql\")\n    return plain_url.render_as_string(hide_password=False)\n\n\n# Very long timeout for unbounded waits - encourages workflow to sleep.\n# DBOS's default 60s is too short and gets recorded to event logs.\n_UNBOUNDED_WAIT_TIMEOUT_SECONDS = 60 * 60 * 24  # 1 day\n\n\n@DBOS.step()\ndef _durable_time() -> float:\n    \"\"\"\n    Get current timestamp, wrapped as a DBOS step so that it's snapshotted and replayed\n    This could be made more consistent if it got the timestamp from the DB.\n    \"\"\"\n    return time.time()\n\n\nclass DBOSRuntime(Runtime):\n    \"\"\"\n    DBOS-backed workflow runtime for durable execution.\n\n    Workflows are registered at launch() time with stable names,\n    enabling distributed workers and recovery.\n\n    State is persisted to the database using SQL state stores,\n    enabling state recovery across process restarts.\n    \"\"\"\n\n    def __init__(self, **kwargs: Unpack[DBOSRuntimeConfig]) -> None:\n        \"\"\"Initialize the DBOS runtime.\n\n        Args:\n            **kwargs: Configuration options. See DBOSRuntimeConfig for details.\n                polling_interval_sec: Interval for polling workflow results. Default 1.0.\n                run_migrations_on_launch: Auto-run migrations on launch(). Default True.\n                schema: Database schema name. Default: auto-detected at launch\n                    (\"dbos\" for PostgreSQL, None for SQLite). Pass None explicitly\n                    to force no schema even on PostgreSQL.\n                state_table_name: State table name. Default \"workflow_state\".\n                journal_table_name: Journal table name. Default \"workflow_journal\".\n                pool_size: Maximum size of the asyncpg pool shared across the\n                    runtime, workflow store, and (when configured) executor\n                    lease manager. Defaults to DBOS's configured ``sys_db``\n                    pool_size at launch, falling back to 10 when DBOS config\n                    is unavailable.\n                pool_min_size: Minimum size of the asyncpg pool. Defaults to\n                    ``pool_size``.\n                max_recovery_attempts: Forwarded to ``@DBOS.workflow``.\n                    Caps how many times a workflow is replayed after a crash\n                    before being marked ``MAX_RECOVERY_ATTEMPTS_EXCEEDED``.\n                    Defaults to DBOS's own default when unset.\n                _experimental_executor_lease: Lease-based executor identity.\n                    When set, the runtime acquires a named slot from a\n                    Postgres-backed pool on launch and uses it as the DBOS\n                    executor_id. This replaces the need for stable hostnames\n                    (e.g. from a StatefulSet) and allows plain Deployments to\n                    coordinate executor identity across replicas.\n\n                    Operational requirements:\n\n                    - The deploying orchestrator must not run more than\n                      ``pool_size`` replicas simultaneously. In Kubernetes\n                      this means setting ``maxSurge: 0`` on the Deployment\n                      rolling-update strategy so that new pods only start\n                      after old ones terminate and release their lease.\n                      Without this, new replicas block on lease acquisition\n                      and never pass health checks.\n                    - Scaling down below the number of replicas that hold\n                      active workflows will orphan those workflows — they\n                      remain assigned to an executor that no longer exists\n                      and won't resume until the lease expires and another\n                      replica reclaims the slot.\n        \"\"\"\n        super().__init__()\n        self.config: DBOSRuntimeConfig = dict(kwargs)  # type: ignore[assignment]  # ty: ignore[invalid-assignment]\n\n        # Workflow tracking state\n        self._tracked_workflows: list[Workflow] = []\n        self._tracked_workflow_ids: set[int] = set()  # Track by id for dedup\n        self._registered: dict[int, RegisteredWorkflow] = {}  # keyed by id(workflow)\n\n        self._dbos_launched = False\n        # Signaled once DBOS is launched and config (engine, schema, etc.) is\n        # resolved.  Recovery workflows on DBOS's background loop may call\n        # get_internal_adapter before our launch() method returns; this event\n        # lets them block briefly until the config is ready.\n        self._launch_ready = threading.Event()\n        self._tasks: list[asyncio.Task[None]] = []\n        self._sql_engine: Engine | None = None\n        self._migrations_run = False\n\n        # Native driver resources (resolved at launch time)\n        self._pool: asyncpg.Pool | None = None\n        self._pool_lock: asyncio.Lock = asyncio.Lock()\n        self._dsn: str | None = None  # asyncpg DSN for lazy pool creation\n        self._db_path: str | None = None  # sqlite path\n        self._schema: str | None = None\n        self._workflow_store: AbstractWorkflowStore | None = None\n        self._lease_manager: ExecutorLeaseManager | None = None\n        self._lease_watch_task: asyncio.Task[None] | None = None\n\n    def _track_task(self, task: asyncio.Task[Any]) -> None:\n        self._tasks.append(task)\n        task.add_done_callback(self._tasks.remove)\n\n    def track_workflow(self, workflow: Workflow) -> None:\n        \"\"\"Track a workflow for registration at launch time.\n\n        If launch() was already called, registers the workflow immediately.\n        This allows late registration for testing scenarios.\n        \"\"\"\n        if self._dbos_launched:\n            # Already launched - register immediately\n            registered = self.register(workflow)\n            self._registered[id(workflow)] = registered\n        else:\n            wf_id = id(workflow)\n            if wf_id not in self._tracked_workflow_ids:\n                self._tracked_workflows.append(workflow)\n                self._tracked_workflow_ids.add(wf_id)\n\n    def get_registered(self, workflow: Workflow) -> RegisteredWorkflow | None:\n        \"\"\"Get the registered workflow if available.\"\"\"\n        return self._registered.get(id(workflow))\n\n    def register(self, workflow: Workflow) -> RegisteredWorkflow:\n        \"\"\"\n        Wrap workflow with DBOS decorators.\n\n        Called at launch() time for each tracked workflow.\n        Uses workflow.workflow_name for stable DBOS registration names.\n        Idempotent: returns existing registration if already registered.\n        \"\"\"\n        # Return existing registration if already registered\n        existing = self._registered.get(id(workflow))\n        if existing is not None:\n            return existing\n\n        # Use workflow's name directly\n        name = workflow.workflow_name\n\n        # Create DBOS-wrapped control loop with stable name\n        wf_kwargs: dict[str, Any] = {\"name\": f\"{name}.control_loop\"}\n        if \"max_recovery_attempts\" in self.config:\n            wf_kwargs[\"max_recovery_attempts\"] = self.config[\"max_recovery_attempts\"]\n\n        @DBOS.workflow(**wf_kwargs)\n        async def _dbos_control_loop(\n            init_state: BrokerState,\n            start_event: StartEvent | None = None,\n            tags: dict[str, Any] | None = None,\n        ) -> StopEvent:\n            if tags is None:\n                tags = {}\n            # Eagerly resolve the asyncpg pool so the adapter can use it\n            # synchronously in get_state_store / is_replaying.\n            if self._dsn is not None:\n                await self._ensure_pool()\n            workflow_run_fn = create_workflow_run_function(workflow)\n            return await workflow_run_fn(init_state, start_event, tags)\n\n        # Wrap steps with stable names\n        wrapped_steps: dict[str, StepWorkerFunction] = {\n            step_name: DBOS.step(name=f\"{name}.{step_name}\")(step)\n            for step_name, step in as_step_worker_functions(workflow).items()\n        }\n\n        registered = RegisteredWorkflow(\n            workflow=workflow, workflow_run_fn=_dbos_control_loop, steps=wrapped_steps\n        )\n        self._registered[id(workflow)] = registered\n        return registered\n\n    def _get_sql_engine(self) -> Engine:\n        \"\"\"Get the SQLAlchemy engine from DBOS for state storage.\n\n        Uses DBOS's app database if configured, otherwise falls back to sys database.\n\n        Returns:\n            SQLAlchemy Engine for state storage.\n\n        Raises:\n            RuntimeError: If no database is available.\n        \"\"\"\n        if self._sql_engine is not None:\n            return self._sql_engine\n\n        dbos = _get_dbos_instance()\n\n        # Try app database first, fall back to system database\n        app_db = dbos._app_db\n        if app_db is not None:\n            self._sql_engine = app_db.engine\n            return self._sql_engine\n\n        # Fall back to system database\n        sys_db = dbos._sys_db\n        self._sql_engine = sys_db.engine\n        return self._sql_engine\n\n    def _resolve_pool_sizes(self) -> tuple[int, int]:\n        \"\"\"Return ``(min_size, max_size)`` for the asyncpg pool.\n\n        Resolution order for max:\n          1. ``pool_size`` from DBOSRuntimeConfig.\n          2. DBOS's configured sys_db pool_size, if DBOS is constructed.\n          3. ``_DEFAULT_POOL_SIZE_FALLBACK``.\n\n        Min defaults to ``pool_min_size`` if explicitly set, else equals max.\n        \"\"\"\n        max_size = self.config.get(\"pool_size\")\n        if max_size is None:\n            try:\n                dbos_inst = _get_dbos_instance()\n                sys_kwargs = dbos_inst._config.get(\"sys_db_engine_kwargs\") or {}\n                max_size = sys_kwargs.get(\"pool_size\")\n            except Exception:\n                max_size = None\n        if max_size is None:\n            max_size = _DEFAULT_POOL_SIZE_FALLBACK\n        # The workflow store permanently holds one connection for LISTEN/NOTIFY,\n        # so the pool must have at least 2 to avoid deadlocking queries.\n        if max_size < 2:\n            max_size = 2\n\n        min_size = self.config.get(\"pool_min_size\", max_size)\n        # Clamp min ≤ max defensively in case both were set.\n        if min_size > max_size:\n            min_size = max_size\n        return min_size, max_size\n\n    async def _ensure_pool(self) -> asyncpg.Pool:\n        \"\"\"Get or lazily create the asyncpg connection pool.\n\n        Only valid for postgres dialect. Raises RuntimeError for sqlite.\n        \"\"\"\n        if self._pool is not None:\n            return self._pool\n        async with self._pool_lock:\n            if self._pool is not None:\n                return self._pool\n            if self._dsn is None:\n                raise RuntimeError(\n                    \"No asyncpg DSN configured. Either not launched or using sqlite dialect.\"\n                )\n            min_size, max_size = self._resolve_pool_sizes()\n            self._pool = await asyncpg.create_pool(\n                dsn=self._dsn,\n                min_size=min_size,\n                max_size=max_size,\n            )\n            return self._pool\n\n    async def run_migrations(self) -> None:\n        \"\"\"Run database migrations for all workflow tables.\n\n        Uses the file-based migration system to create/update workflow store,\n        state, and journal tables. Idempotent - safe to call multiple times.\n\n        Can be called explicitly before launch() when run_migrations_on_launch=False,\n        allowing for custom migration timing (e.g., during application startup).\n\n        Requires DBOS to be launched first (calls _get_sql_engine internally).\n        \"\"\"\n        if self._migrations_run:\n            return\n\n        engine = self._get_sql_engine()\n        schema = _resolve_schema(self.config, engine)\n\n        _PG_SOURCES = [\n            SERVER_POSTGRES_MIGRATION_SOURCE,\n            POSTGRES_MIGRATION_SOURCE,\n        ]\n        _SQLITE_SOURCES = [\n            SERVER_SQLITE_MIGRATION_SOURCE,\n            SQLITE_MIGRATION_SOURCE,\n        ]\n\n        if engine.dialect.name == \"postgresql\":\n            dsn = _sqlalchemy_url_to_asyncpg_dsn(engine.url)\n            conn = await asyncpg.connect(dsn)\n            try:\n                await pg_run_migrations(conn, schema=schema, sources=_PG_SOURCES)\n            finally:\n                await conn.close()\n        else:\n            db_path = str(engine.url.database) if engine.url.database else \":memory:\"\n            conn = sqlite3.connect(db_path)\n            try:\n                sqlite_run_migrations(conn, sources=_SQLITE_SOURCES)\n            finally:\n                conn.close()\n\n        self._migrations_run = True\n        logger.info(\"Database migrations completed\")\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n        adapter_state: dict[str, Any] | None = None,\n    ) -> ExternalRunAdapter:\n        \"\"\"Set up a workflow run with SQL-backed state storage.\n\n        State is persisted to the database, enabling recovery across\n        process restarts and distributed execution.\n\n        Args:\n            run_id: Unique identifier for this workflow run.\n            workflow: The workflow to run.\n            init_state: Initial broker state for the control loop.\n            start_event: Optional start event to kick off the workflow.\n            serialized_state: Optional pre-populated state from InMemoryStateStore.to_dict().\n                If provided, this state is written to the database before the workflow\n                starts, allowing workflows to begin with pre-set initial values.\n            serializer: Serializer for state data. Defaults to JsonSerializer.\n            adapter_state: Optional adapter state (unused for DBOS).\n        \"\"\"\n        if not self._dbos_launched:\n            raise RuntimeError(\n                \"DBOS runtime not launched. Call runtime.launch() before running workflows.\"\n            )\n\n        registered = self.get_registered(workflow)\n        if registered is None:\n            raise RuntimeError(\n                \"DBOSRuntime workflows must be registered before running. Did you forget to call runtime.launch()?\"\n            )\n\n        # Capture values needed in the async task closure\n        active_serializer = serializer or JsonSerializer()\n\n        async def _run_workflow() -> WorkflowHandleAsync[Any]:\n            with SetWorkflowID(run_id):\n                # Write initial state to DB before starting workflow (non-blocking to caller)\n                if serialized_state:\n                    if self._dsn is not None:\n                        pool = await self._ensure_pool()\n                        store: StateStore[Any] = PostgresStateStore(\n                            pool=pool,\n                            run_id=run_id,\n                            state_type=infer_state_type(workflow),\n                            serializer=active_serializer,\n                            schema=self._schema,\n                        )\n                    elif self._db_path is not None:\n                        store = SqliteStateStore(\n                            db_path=self._db_path,\n                            run_id=run_id,\n                            state_type=infer_state_type(workflow),\n                            serializer=active_serializer,\n                        )\n                    else:\n                        raise RuntimeError(\"No pool or db_path configured.\")\n                    # Deserialize and save the initial state\n                    state = deserialize_state_from_dict(\n                        serialized_state,\n                        active_serializer,\n                        state_type=infer_state_type(workflow),\n                    )\n                    await store.set_state(state)\n\n                try:\n                    return await DBOS.start_workflow_async(\n                        registered.workflow_run_fn,\n                        init_state,\n                        start_event,\n                        get_dispatcher().capture_propagation_context(),\n                    )\n                except Exception as e:\n                    logger.error(\n                        f\"Failed to submit work to DBOS for {run_id} with start event: {start_event} and init state: {init_state}. Error: {e}\",\n                        exc_info=True,\n                    )\n                    raise e\n\n        # Create startup task and pass to adapter so it can await workflow readiness\n        startup_task = asyncio.create_task(_run_workflow())\n        self._track_task(startup_task)\n\n        return ExternalDBOSAdapter(\n            run_id,\n            self.config.get(\"polling_interval_sec\", 1.0),\n            startup_task,\n        )\n\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        # Wait for launch config to be ready. Recovery workflows on DBOS's\n        # background loop may arrive before launch() finishes setting up.\n        self._launch_ready.wait(timeout=30)\n        if not self._dbos_launched:\n            raise RuntimeError(\n                \"DBOS runtime not launched. Call runtime.launch() before running workflows.\"\n            )\n        run_id = DBOS.workflow_id\n        if run_id is None:\n            raise RuntimeError(\n                \"No current run id. Must be called within a workflow run.\"\n            )\n\n        # Infer state_type from the workflow for typed state support\n        state_type = infer_state_type(workflow)\n\n        engine = self._get_sql_engine()\n        return InternalDBOSAdapter(\n            run_id,\n            engine,\n            state_type,\n            schema=self._schema,\n            state_table_name=self.config.get(\n                \"state_table_name\", DEFAULT_STATE_TABLE_NAME\n            ),\n            journal_table_name=self.config.get(\n                \"journal_table_name\", DEFAULT_JOURNAL_TABLE_NAME\n            ),\n            pool=PoolProvider.borrowed(self._ensure_pool)\n            if self._dsn is not None\n            else None,\n            resolved_pool=self._pool,\n            db_path=self._db_path,\n        )\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        if not self._dbos_launched:\n            raise RuntimeError(\n                \"DBOS runtime not launched. Call runtime.launch() before running workflows.\"\n            )\n        return ExternalDBOSAdapter(run_id, self.config.get(\"polling_interval_sec\", 1.0))\n\n    def create_workflow_store(self) -> AbstractWorkflowStore:\n        \"\"\"Return the cached workflow store, creating it on first call.\n\n        Detects the engine dialect and creates the appropriate store:\n        - PostgreSQL: PostgresWorkflowStore using asyncpg with LISTEN/NOTIFY\n        - SQLite: SqliteWorkflowStore using raw sqlite3\n\n        Returns a lazy proxy so this can be called before launch(). The real\n        store is resolved on first use (which happens after launch()).\n        \"\"\"\n        if self._workflow_store is not None:\n            return self._workflow_store\n\n        def _factory() -> AbstractWorkflowStore:\n            engine = self._get_sql_engine()\n            schema = _resolve_schema(self.config, engine)\n\n            if engine.dialect.name == \"postgresql\":\n                dsn = _sqlalchemy_url_to_asyncpg_dsn(engine.url)\n                logger.info(\n                    \"Using PostgresWorkflowStore (asyncpg) for workflow storage\"\n                )\n                # Share the runtime's asyncpg pool — PostgresWorkflowStore\n                # borrows it via the factory and never owns its lifecycle.\n                return PostgresWorkflowStore(\n                    dsn=dsn,\n                    schema=schema,\n                    pool=PoolProvider.borrowed(self._ensure_pool),\n                )\n\n            db_path = str(engine.url.database) if engine.url.database else \":memory:\"\n            logger.info(\"Using SqliteWorkflowStore for workflow storage\")\n            return SqliteWorkflowStore(db_path=db_path, auto_migrate=False)\n\n        self._workflow_store = DBOSWorkflowStore(_factory)\n        return self._workflow_store\n\n    def _create_journal_crud_factory(self) -> Callable[[], JournalCrud]:\n        \"\"\"Create a factory for JournalCrud that resolves the database backend.\n\n        Returns a factory rather than an instance because DB config (db_path,\n        dsn, pool) isn't available until ``launch()`` runs, but the decorator\n        chain is built before launch via ``build_server_runtime()``.\n        \"\"\"\n\n        def _factory() -> JournalCrud:\n            journal_table_name = self.config.get(\n                \"journal_table_name\", DEFAULT_JOURNAL_TABLE_NAME\n            )\n            if self._db_path is not None:\n                return SqliteJournalCrud(\n                    db_path=self._db_path,\n                    table_name=journal_table_name,\n                )\n            if self._pool is not None:\n                return PostgresJournalCrud(\n                    self._pool,\n                    table_name=journal_table_name,\n                    schema=self._schema,\n                )\n            raise RuntimeError(\n                \"No database configured for journal. Was launch() called?\"\n            )\n\n        return _factory\n\n    def _create_lifecycle_lock_factory(\n        self,\n    ) -> Callable[[], Awaitable[RunLifecycleLock]]:\n        \"\"\"Create an async factory for RunLifecycleLock that resolves the database backend.\"\"\"\n\n        async def _factory() -> RunLifecycleLock:\n            if self._db_path is not None:\n                return SqliteRunLifecycleLock(db_path=self._db_path)\n            if self._dsn is not None:\n                pool = await self._ensure_pool()\n                return PostgresRunLifecycleLock(pool, schema=self._schema)\n            raise RuntimeError(\n                \"No database configured for lifecycle lock. Was launch() called?\"\n            )\n\n        return _factory\n\n    def _finalize_launch(self) -> None:\n        \"\"\"Resolve DB-backed resources after DBOS.launch() completes.\"\"\"\n        engine = self._get_sql_engine()\n        self._schema = _resolve_schema(self.config, engine)\n        if engine.dialect.name == \"postgresql\":\n            self._dsn = _sqlalchemy_url_to_asyncpg_dsn(engine.url)\n        else:\n            self._db_path = (\n                str(engine.url.database) if engine.url.database else \":memory:\"\n            )\n        self._dbos_launched = True\n        self._launch_ready.set()\n\n    async def _prepare_launch(self, *, start_lease_watch: bool) -> None:\n        \"\"\"Run the async setup required before calling DBOS.launch().\"\"\"\n        if self._dbos_launched:\n            return  # Already launched\n\n        # Set self._dsn early when the system database is postgres so the\n        # shared asyncpg pool is reachable before DBOS.launch completes\n        # (executor lease pre-launch path needs this). Best-effort — if DBOS\n        # isn't constructed yet (e.g. tests that monkeypatch the launch path),\n        # _finalize_launch will populate self._dsn after launch completes.\n        try:\n            dbos_inst_pre = _get_dbos_instance()\n            sys_db_url = dbos_inst_pre._config.get(\"system_database_url\", \"\") or \"\"\n            if sys_db_url and not sys_db_url.startswith(\"sqlite\"):\n                self._dsn = sys_db_url\n        except Exception:\n            logger.debug(\n                \"Could not pre-resolve DSN before DBOS construction\", exc_info=True\n            )\n\n        # Acquire executor lease if configured.\n        # Migrations must run first so the executor_leases table exists.\n        lease_config = self.config.get(\"_experimental_executor_lease\")\n        if lease_config is not None:\n            pool_size = lease_config.get(\"pool_size\")\n            if pool_size is None:\n                raise ValueError(\"_experimental_executor_lease.pool_size is required\")\n\n            # Get DSN from DBOS config before launch\n            dbos_inst = _get_dbos_instance()\n            dsn = dbos_inst._config.get(\"system_database_url\", \"\")\n            if not dsn or dsn.startswith(\"sqlite\"):\n                raise ValueError(\n                    \"Executor leasing requires a PostgreSQL system_database_url in DBOS config\"\n                )\n\n            schema = self.config.get(\"schema\", \"dbos\") or \"dbos\"\n\n            # Run migrations before lease acquisition so the table exists\n            if self.config.get(\"run_migrations_on_launch\", True):\n                conn = await asyncpg.connect(dsn)\n                try:\n                    await pg_run_migrations(\n                        conn,\n                        schema=schema,\n                        sources=[\n                            SERVER_POSTGRES_MIGRATION_SOURCE,\n                            POSTGRES_MIGRATION_SOURCE,\n                        ],\n                    )\n                finally:\n                    await conn.close()\n                self._migrations_run = True\n                logger.info(\"Database migrations completed (pre-lease)\")\n\n            self._lease_manager = ExecutorLeaseManager(\n                pool=PoolProvider.borrowed(self._ensure_pool),\n                pool_size=pool_size,\n                heartbeat_interval=lease_config.get(\"heartbeat_interval\", 10.0),\n                lease_timeout=lease_config.get(\"lease_timeout\", 30.0),\n                slot_prefix=lease_config.get(\"slot_prefix\", \"executor\"),\n                schema=schema,\n            )\n            acquire_timeout = lease_config.get(\"acquire_timeout\", 60.0)\n            await self._lease_manager.acquire(timeout=acquire_timeout)\n\n            # Reinitialize DBOS with the leased executor_id.\n            # DBOS is a singleton and hasn't been launched yet, so reinit is safe.\n            config = dict(dbos_inst._config)\n            config[\"executor_id\"] = self._lease_manager.executor_id\n            DBOS(config=cast(Any, config))\n            logger.info(\"Acquired executor lease: %s\", self._lease_manager.executor_id)\n            if start_lease_watch:\n                self._lease_watch_task = asyncio.create_task(self._watch_lease())\n\n        # Register each pending workflow with DBOS\n        for workflow in self._tracked_workflows:\n            # Register with DBOS (this applies decorators)\n            registered = self.register(workflow)\n            self._registered[id(workflow)] = registered\n\n    async def _post_launch(self) -> None:\n        \"\"\"Run async work after DBOS.launch() has captured its target context.\"\"\"\n        # Run migrations after DBOS is launched (if configured)\n        if self.config.get(\"run_migrations_on_launch\", True):\n            await self.run_migrations()\n\n    def build_server_runtime(self, *, idle_timeout: float = 600.0) -> Runtime:\n        \"\"\"Build the decorator chain for use with WorkflowServer.\n\n        Wraps the DBOS runtime with:\n        - TickPersistenceDecorator (persists ticks to workflow store)\n        - EventInterceptorDecorator (blocks events from reaching DBOS streams)\n        - DBOSIdleReleaseDecorator (releases idle workflows after timeout)\n\n        Chain order (outermost first):\n        DBOSIdleReleaseDecorator → EventInterceptorDecorator → TickPersistenceDecorator → DBOSRuntime\n\n        Args:\n            idle_timeout: Seconds to wait after a workflow becomes idle before\n                releasing it. Defaults to 10 minutes.\n\n        The returned runtime should be passed as the ``runtime`` argument\n        to ``WorkflowServer``.\n        \"\"\"\n        store = self.create_workflow_store()\n        tick_persistence = TickPersistenceDecorator(self, store)\n        return DBOSIdleReleaseDecorator(\n            EventInterceptorDecorator(tick_persistence),\n            store=store,\n            idle_timeout=idle_timeout,\n            journal_crud=self._create_journal_crud_factory(),\n            lifecycle_lock=self._create_lifecycle_lock_factory(),\n        )\n\n    async def launch(self) -> None:\n        \"\"\"\n        Launch DBOS and register all tracked workflows.\n\n        Must be called before running any workflows.\n        Runs database migrations unless run_migrations_on_launch=False.\n        If ``_experimental_executor_lease`` is set in the config, acquires a\n        lease slot first.\n        \"\"\"\n        await self._prepare_launch(start_lease_watch=True)\n        if self._dbos_launched:\n            return\n        # Async server startup should keep DBOS on the caller's live loop so\n        # startup recovery and later HTTP handlers share the same long-lived\n        # application event loop.\n        DBOS.launch()\n        self._finalize_launch()\n        await self._post_launch()\n\n    def launch_sync(self) -> None:\n        \"\"\"Launch DBOS from synchronous code without capturing asyncio.run()'s loop.\"\"\"\n        try:\n            asyncio.get_running_loop()\n        except RuntimeError:\n            pass\n        else:\n            raise RuntimeError(\n                \"DBOSRuntime.launch_sync() cannot be called from an async context; \"\n                \"use 'await runtime.launch()' instead.\"\n            )\n\n        if self.config.get(\"_experimental_executor_lease\") is not None:\n            raise RuntimeError(\n                \"DBOSRuntime.launch_sync() does not support \"\n                \"'_experimental_executor_lease'; use 'await runtime.launch()' instead.\"\n            )\n\n        asyncio.run(self._prepare_launch(start_lease_watch=False))\n        if self._dbos_launched:\n            return\n        DBOS.launch()\n        self._finalize_launch()\n        asyncio.run(self._post_launch())\n\n    @property\n    def is_launched(self) -> bool:\n        return self._dbos_launched\n\n    @property\n    def lease_lost_event(self) -> asyncio.Event | None:\n        \"\"\"Event that is set when the executor lease is lost.\n\n        Returns None if executor leasing is not configured.\n        \"\"\"\n        if self._lease_manager is None:\n            return None\n        return self._lease_manager.lease_lost_event\n\n    async def _watch_lease(self) -> None:\n        assert self._lease_manager is not None\n        await self._lease_manager.lease_lost_event.wait()\n        logger.critical(\"Executor lease lost — shutting down runtime\")\n        await self.destroy()\n\n    async def destroy(self, destroy_dbos: bool = True) -> None:\n        \"\"\"Clean up DBOS runtime resources.\n\n        Args:\n            destroy_dbos: If True (default), also calls DBOS.destroy().\n                Set to False when DBOS lifecycle is managed externally\n                (e.g., shared across multiple runtimes in tests).\n        \"\"\"\n        if not self._dbos_launched:\n            return  # Already destroyed or never launched\n\n        # Cancel lease watcher first to avoid re-entrant destroy\n        if self._lease_watch_task is not None:\n            self._lease_watch_task.cancel()\n            try:\n                await self._lease_watch_task\n            except asyncio.CancelledError:\n                pass\n            self._lease_watch_task = None\n\n        if self._lease_manager is not None:\n            await self._lease_manager.release()\n            self._lease_manager = None\n\n        self._tracked_workflows.clear()\n        self._tracked_workflow_ids.clear()\n        self._registered.clear()\n        self._dbos_launched = False\n        self._launch_ready = threading.Event()\n        self._sql_engine = None\n        self._migrations_run = False\n        self._dsn = None\n        self._db_path = None\n        self._schema = None\n        # Shut down the workflow store's listener before terminating the pool.\n        # Otherwise pool.terminate() kills the LISTEN connection, which fires\n        # _on_listen_termination and spawns a reconnect task during shutdown.\n        if self._workflow_store is not None:\n            inner = (\n                self._workflow_store._inner\n                if isinstance(self._workflow_store, DBOSWorkflowStore)\n                else self._workflow_store\n            )\n            if isinstance(inner, PostgresWorkflowStore):\n                await inner.close()\n            self._workflow_store = None\n        if self._pool is not None:\n            try:\n                self._pool.terminate()\n            except Exception:\n                logger.debug(\n                    \"Failed to terminate asyncpg pool during destroy\", exc_info=True\n                )\n            self._pool = None\n        # Wait for cancelled tasks to unwind before destroying DBOS.\n        tasks_to_cancel = [t for t in self._tasks if not t.done()]\n        for task in tasks_to_cancel:\n            task.cancel()\n        if tasks_to_cancel:\n            await asyncio.gather(*tasks_to_cancel, return_exceptions=True)\n        if destroy_dbos:\n            DBOS.destroy()\n\n\n_IO_STREAM_PUBLISHED_EVENTS_NAME = \"published_events\"\n_IO_STREAM_TICK_TOPIC = \"ticks\"\n\n\nclass InternalDBOSAdapter(InternalRunAdapter):\n    \"\"\"\n    Internal DBOS adapter for the workflow control loop.\n\n    - send_event sends ticks via DBOS.send_async\n    - wait_receive receives ticks via DBOS.recv_async\n    - write_to_event_stream publishes events via DBOS streams\n    - get_now returns a durable timestamp\n    - close sends shutdown signal to wake blocked recv\n    - wait_for_next_task coordinates task completion ordering for deterministic replay\n    \"\"\"\n\n    def __init__(\n        self,\n        run_id: str,\n        engine: Engine,\n        state_type: type[BaseModel] | None = None,\n        schema: str | None = None,\n        state_table_name: str = DEFAULT_STATE_TABLE_NAME,\n        journal_table_name: str = DEFAULT_JOURNAL_TABLE_NAME,\n        pool: PoolProvider | None = None,\n        resolved_pool: asyncpg.Pool | None = None,\n        db_path: str | None = None,\n    ) -> None:\n        self._run_id = run_id\n        self._engine = engine\n        self._state_type = state_type\n        self._schema = schema\n        self._state_table_name = state_table_name\n        self._journal_table_name = journal_table_name\n        self._pool_provider = pool\n        self._resolved_pool = resolved_pool\n        self._db_path = db_path\n        self._closed = False\n        self._shutdown_event = asyncio.Event()\n        self._state_store: StateStore[Any] | None = None\n        # Journal for deterministic task ordering - lazily initialized\n        self._journal: TaskJournal | None = None\n        self._orphan_purge_done = False\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        await DBOS.write_stream_async(_IO_STREAM_PUBLISHED_EVENTS_NAME, event)\n\n    async def get_now(self) -> float:\n        return _durable_time()\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        await DBOS.send_async(self._run_id, tick, topic=_IO_STREAM_TICK_TOPIC)\n\n    async def wait_receive(\n        self,\n        timeout_seconds: float | None = None,\n    ) -> WaitResult:\n        \"\"\"Wait for tick via DBOS.recv_async. Raises CancelledError on shutdown.\"\"\"\n        if self._closed:\n            raise asyncio.CancelledError(\"Adapter closed\")\n\n        recv_task = asyncio.ensure_future(\n            DBOS.recv_async(\n                _IO_STREAM_TICK_TOPIC,\n                timeout_seconds=timeout_seconds or _UNBOUNDED_WAIT_TIMEOUT_SECONDS,\n            )\n        )\n        shutdown_task = asyncio.ensure_future(self._shutdown_event.wait())\n        try:\n            done, _ = await asyncio.wait(\n                {recv_task, shutdown_task}, return_when=asyncio.FIRST_COMPLETED\n            )\n        except asyncio.CancelledError:\n            recv_task.cancel()\n            shutdown_task.cancel()\n            raise\n\n        if shutdown_task in done:\n            recv_task.cancel()\n            raise asyncio.CancelledError(\"Adapter closed\")\n\n        shutdown_task.cancel()\n        result = recv_task.result()\n        if result is None:\n            return WaitResultTimeout()\n        return WaitResultTick(tick=result)\n\n    async def close(self) -> None:\n        \"\"\"Signal shutdown using process-local event to wake blocked recv.\"\"\"\n        if self._closed:\n            return\n        self._closed = True\n        self._shutdown_event.set()\n\n    async def _resolve_pool(self) -> asyncpg.Pool:\n        \"\"\"Resolve the asyncpg pool, lazily creating it via the runtime callback.\"\"\"\n        if self._resolved_pool is not None:\n            return self._resolved_pool\n        if self._pool_provider is None:\n            raise RuntimeError(\n                \"No asyncpg pool configured. Either not launched or using sqlite dialect.\"\n            )\n        self._resolved_pool = await self._pool_provider.get()\n        return self._resolved_pool\n\n    def _get_or_create_state_store(self) -> StateStore[Any]:\n        \"\"\"Get or lazily create the state store.\n\n        For PostgreSQL, the pool must be resolved first via _resolve_pool().\n        Call _ensure_resources() before accessing the state store.\n        \"\"\"\n        if self._state_store is None:\n            if self._resolved_pool is not None:\n                self._state_store = PostgresStateStore(\n                    pool=self._resolved_pool,\n                    run_id=self._run_id,\n                    state_type=cast(type[Any], self._state_type),\n                    schema=self._schema,\n                )\n            elif self._db_path is not None:\n                self._state_store = SqliteStateStore(\n                    db_path=self._db_path,\n                    run_id=self._run_id,\n                    state_type=cast(type[Any], self._state_type),\n                )\n            else:\n                raise RuntimeError(\n                    \"No pool or db_path configured for state store. \"\n                    \"Ensure the runtime pool is initialized before accessing state.\"\n                )\n        return self._state_store\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return self._get_or_create_state_store()\n\n    def is_replaying(self) -> bool:\n        if (\n            self._journal is None\n            and self._resolved_pool is None\n            and self._db_path is None\n        ):\n            return False\n        journal = self._get_or_create_journal()\n        return journal.is_replaying()\n\n    def _get_or_create_journal(self) -> TaskJournal:\n        \"\"\"Get or lazily create the task journal.\"\"\"\n        if self._journal is None:\n            if self._resolved_pool is not None:\n                crud = PostgresJournalCrud(\n                    pool=self._resolved_pool,\n                    table_name=self._journal_table_name,\n                    schema=self._schema,\n                )\n            elif self._db_path is not None:\n                crud = SqliteJournalCrud(\n                    db_path=self._db_path,\n                    table_name=self._journal_table_name,\n                )\n            else:\n                raise RuntimeError(\"No pool or db_path configured for journal.\")\n            self._journal = TaskJournal(self._run_id, crud)\n        return self._journal\n\n    async def _purge_orphaned_operations(self, journal: TaskJournal) -> None:\n        \"\"\"Purge orphaned operation_outputs beyond the current fid.\n\n        Called once at the replay→fresh transition to remove stale rows left by\n        a previous crashed recovery. Also truncates stale journal entries.\n        \"\"\"\n        if self._orphan_purge_done:\n            return\n        self._orphan_purge_done = True\n\n        if not journal.has_entries:\n            return\n\n        ctx = get_local_dbos_context()\n        assert ctx is not None, \"Expected DBOS context during workflow execution\"\n        current_fid = ctx.function_id\n\n        await journal.purge_stale(current_fid)\n\n        logger.debug(\n            \"Purged orphaned operation_outputs for %s beyond fid %d\",\n            self._run_id,\n            current_fid,\n        )\n\n    async def wait_for_next_task(\n        self,\n        running: list[NamedTask],\n        pending: list[PendingStart],\n        timeout: float | None = None,\n    ) -> WaitForNextTaskResult:\n        \"\"\"Wait for and return the next task that should complete.\n\n        Starts each pending coroutine with an ``asyncio.sleep(0)`` yield between\n        them so that every task's synchronous preamble (including DBOS function_id\n        acquisition) runs in deterministic order.\n\n        During replay, waits for the specific task that completed in the original run.\n        During fresh execution, waits for any task and records the completion order.\n\n        Args:\n            running: Already-started tasks from previous iterations.\n            pending: Coroutines to start this iteration.\n            timeout: Timeout in seconds, None for no timeout.\n\n        Returns:\n            WaitForNextTaskResult with completed task and newly started NamedTasks.\n        \"\"\"\n        # Resolve pool before journal creation (needed for postgres)\n        if self._pool_provider is not None and self._resolved_pool is None:\n            await self._resolve_pool()\n\n        # Load journal before starting pending coroutines so the orphan purge\n        # runs before new fids are consumed.\n        journal = self._get_or_create_journal()\n        await journal.load()\n        expected_key = journal.next_expected_key()\n\n        if expected_key is None and not self._orphan_purge_done:\n            await self._purge_orphaned_operations(journal)\n\n        # Start each pending coroutine with a yield between each to ensure\n        # deterministic function_id ordering for DBOS replay.\n        started: list[NamedTask] = []\n        for p in pending:\n            started.append(p.start(asyncio.create_task(p.coro)))\n            await asyncio.sleep(0)\n\n        all_named = running + started\n        tasks = all_tasks(all_named)\n        if not tasks:\n            return WaitForNextTaskResult(None, started)\n\n        if expected_key is not None:\n            # Replay mode: wait for specific task\n            target_task = find_by_key(all_named, expected_key)\n\n            if target_task is None:\n                logger.warning(\n                    f\"Non-deterministic execution detected during replay! \"\n                    f\"Expected task {expected_key} not in set yet. \"\n                    f\"Falling back to awaiting all tasks.\"\n                )\n            else:\n                try:\n                    await asyncio.wait_for(asyncio.shield(target_task), timeout=timeout)\n                except (asyncio.TimeoutError, TimeoutError):\n                    return WaitForNextTaskResult(None, started)\n                journal.advance()\n                return WaitForNextTaskResult(target_task, started)\n\n        # Fresh execution: wait for first, record it\n        done, _ = await asyncio.wait(\n            tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED\n        )\n        if not done:\n            return WaitForNextTaskResult(None, started)\n\n        completed = done.pop()\n        key = get_key(all_named, completed)\n        await journal.record(key)\n\n        return WaitForNextTaskResult(completed, started)\n\n\nclass ExternalDBOSAdapter(ExternalRunAdapter):\n    \"\"\"\n    External DBOS adapter for workflow interaction.\n\n    - send_event puts ticks into the shared mailbox queue\n    - stream_published_events reads from DBOS streams\n    - close is a no-op\n    \"\"\"\n\n    def __init__(\n        self,\n        run_id: str,\n        polling_interval_sec: float = 1.0,\n        startup_task: asyncio.Task[WorkflowHandleAsync[Any]] | None = None,\n    ) -> None:\n        self._run_id = run_id\n        self._polling_interval_sec = polling_interval_sec\n        self._startup_task = startup_task  # None means workflow already started\n        self._handle: WorkflowHandleAsync[Any] | None = None\n\n    @property\n    def run_id(self) -> str:\n        \"\"\"Get the workflow run ID.\"\"\"\n        return self._run_id\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        await self._ensure_workflow_started()\n        await DBOS.send_async(self._run_id, tick, topic=_IO_STREAM_TICK_TOPIC)\n\n    async def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        await self._ensure_workflow_started()\n\n        async for event in DBOS.read_stream_async(self.run_id, \"published_events\"):\n            yield event\n\n    async def get_result(self) -> StopEvent:\n        handle = await self._ensure_workflow_started()\n        return await handle.get_result(polling_interval_sec=self._polling_interval_sec)\n\n    async def _ensure_workflow_started(self) -> WorkflowHandleAsync[Any]:\n        \"\"\"Wait for the workflow startup task to complete and return the handle.\"\"\"\n        if self._startup_task is not None:\n            self._handle = await self._startup_task\n            self._startup_task = None  # Clear after awaiting\n        if self._handle is None:\n            # Fallback: workflow was started elsewhere, retrieve with retry since\n            # there can be a race between start_workflow_async completing and the\n            # workflow becoming retrievable in DBOS.\n\n            for attempt in range(20):\n                try:\n                    self._handle = await DBOS.retrieve_workflow_async(self.run_id)\n                    break\n                except DBOSNonExistentWorkflowError:\n                    if attempt == 19:\n                        raise\n                    await asyncio.sleep(0.1 * (attempt + 1))\n        assert self._handle is not None\n        return self._handle\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/conftest.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport json\nimport sqlite3\nimport subprocess\nimport sys\nimport tempfile\nfrom collections.abc import Generator\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nfrom llama_agents.dbos._store import SQLITE_MIGRATION_SOURCE\nfrom llama_agents.server._store import (\n    SQLITE_MIGRATION_SOURCE as SERVER_SQLITE_MIGRATION_SOURCE,\n)\nfrom llama_agents.server._store.sqlite.migrate import (\n    run_migrations as sqlite_run_migrations,\n)\nfrom llama_agents_integration_tests.postgres import (\n    get_asyncpg_dsn,\n)\nfrom llama_agents_integration_tests.postgres import (\n    postgres_container as _postgres_container,\n)\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.engine import Engine\nfrom sqlalchemy.pool import StaticPool\nfrom testcontainers.postgres import PostgresContainer\n\n_SQLITE_SOURCES = [\n    SERVER_SQLITE_MIGRATION_SOURCE,\n    SQLITE_MIGRATION_SOURCE,\n]\n\n\n@pytest.fixture\ndef journal_db_path() -> Generator[str]:\n    \"\"\"Create a temporary SQLite database with both server and DBOS migrations.\"\"\"\n    with tempfile.TemporaryDirectory() as tmp:\n        db_path = str(Path(tmp) / \"test.db\")\n        conn = sqlite3.connect(db_path)\n        try:\n            sqlite_run_migrations(conn, sources=_SQLITE_SOURCES)\n        finally:\n            conn.close()\n        yield db_path\n\n\n@pytest.fixture\ndef sqlite_engine(journal_db_path: str) -> Engine:\n    \"\"\"Create a SQLAlchemy engine pointing at the migrated test database.\"\"\"\n    engine = create_engine(\n        f\"sqlite:///{journal_db_path}\",\n        connect_args={\"check_same_thread\": False},\n        poolclass=StaticPool,\n    )\n    return engine\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_container() -> Generator[PostgresContainer, None, None]:\n    \"\"\"Module-scoped PostgreSQL container for docker-marked tests.\"\"\"\n    with _postgres_container() as pg:\n        yield pg\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_dsn(postgres_container: PostgresContainer) -> str:\n    \"\"\"Return a plain postgresql:// DSN suitable for asyncpg.\"\"\"\n    return get_asyncpg_dsn(postgres_container)\n\n\nRUNNER_PATH = str(Path(__file__).parent / \"fixtures\" / \"runner.py\")\n\n\ndef run_scenario(\n    workflow: str,\n    db_url: str,\n    run_id: str,\n    config: dict[str, Any] | None = None,\n    call_close: bool = False,\n    timeout: float = 45.0,\n) -> subprocess.CompletedProcess[str]:\n    \"\"\"Run a workflow scenario in a subprocess via runner.py.\"\"\"\n    cmd = [\n        sys.executable,\n        RUNNER_PATH,\n        \"--workflow\",\n        workflow,\n        \"--db-url\",\n        db_url,\n        \"--run-id\",\n        run_id,\n    ]\n    if config:\n        cmd.extend([\"--config\", json.dumps(config)])\n    if call_close:\n        cmd.append(\"--call-close\")\n    try:\n        return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)\n    except subprocess.TimeoutExpired as e:\n        stdout = e.stdout.decode() if isinstance(e.stdout, bytes) else (e.stdout or \"\")\n        stderr = e.stderr.decode() if isinstance(e.stderr, bytes) else (e.stderr or \"\")\n        msg = f\"Subprocess timed out after {timeout}s\\nstdout:\\n{stdout}\\nstderr:\\n{stderr}\"\n        pytest.fail(msg)\n        raise AssertionError(\"unreachable\")  # noqa: B904\n\n\ndef assert_no_determinism_errors(result: subprocess.CompletedProcess[str]) -> None:\n    \"\"\"Check subprocess result for crashes and DBOS determinism errors.\"\"\"\n    combined = result.stdout + result.stderr\n\n    if result.returncode != 0:\n        msg = f\"Subprocess exited with code {result.returncode}\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n        pytest.fail(msg)\n\n    if \"Traceback (most recent call last)\" in result.stdout:\n        msg = f\"Subprocess exception!\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n        pytest.fail(msg)\n\n    if \"DBOSUnexpectedStepError\" in combined or \"Error 11\" in combined:\n        msg = f\"DBOS determinism error on resume!\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n        pytest.fail(msg)\n\n\ndef log_on_failure(result: subprocess.CompletedProcess[str], label: str) -> None:\n    if result.returncode != 0:\n        print(f\"\\n=== {label} FAILED ===\")\n        print(f\"stdout: {result.stdout}\")\n        print(f\"stderr: {result.stderr}\")\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/replica_server.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Minimal WorkflowServer replica for cross-process integration tests.\n\nLaunches a WorkflowServer on the given port with a configurable workflow.\nPrints READY to stdout once listening. The test process drives it via HTTP.\n\nUsage:\n    python replica_server.py --workflow module.path:ClassName --db-url DSN --port 18001\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n\nfrom dbos import DBOS, DBOSConfig  # noqa: E402\nfrom llama_agents.dbos import DBOSRuntime  # noqa: E402\nfrom llama_agents.server import WorkflowServer  # noqa: E402\nfrom runner_common import import_workflow  # noqa: E402  # ty: ignore[unresolved-import]\nfrom workflows.events import Event  # noqa: E402\n\n\nasync def main() -> None:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--workflow\", required=True)\n    parser.add_argument(\"--db-url\", required=True)\n    parser.add_argument(\"--port\", type=int, required=True)\n    parser.add_argument(\"--idle-timeout\", type=float, default=None)\n    parser.add_argument(\"--executor-id\", default=None)\n    args = parser.parse_args()\n\n    workflow_class, _module = import_workflow(args.workflow)\n    config: DBOSConfig = {\n        \"name\": f\"test-replica-{args.port}\",\n        \"system_database_url\": args.db_url,\n        \"run_admin_server\": False,\n        \"notification_listener_polling_interval_sec\": 0.01,\n        \"executor_id\": args.executor_id or f\"test-replica-{args.port}\",\n    }\n    DBOS(config=config)\n    dbos_runtime = DBOSRuntime(polling_interval_sec=0.01)\n\n    wf = workflow_class(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    store = dbos_runtime.create_workflow_store()\n    idle_kwargs = (\n        {\"idle_timeout\": args.idle_timeout} if args.idle_timeout is not None else {}\n    )\n    server_runtime = dbos_runtime.build_server_runtime(**idle_kwargs)\n    server = WorkflowServer(runtime=server_runtime, workflow_store=store)\n    # Discover all Event subclasses in the workflow's module so that\n    # wait_for_event types (not in step signatures) are in the registry.\n    additional_events = [\n        attr\n        for name in dir(_module)\n        if isinstance(attr := getattr(_module, name), type)\n        and issubclass(attr, Event)\n        and attr is not Event\n    ]\n    server.add_workflow(\"test\", wf, additional_events=additional_events)\n\n    await server.start()\n    try:\n        print(f\"READY:{args.port}\", flush=True)\n        await server.serve(host=\"0.0.0.0\", port=args.port)\n    finally:\n        await server.stop()\n        await dbos_runtime.destroy()\n\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(main())\n    except KeyboardInterrupt:\n        sys.exit(0)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/runner.py",
    "content": "# ty: ignore[invalid-argument-type]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Subprocess runner for workflow tests with DBOS isolation.\n\nThis module provides a CLI runner for executing workflows in isolated\nsubprocesses, supporting interrupt/resume testing and human-in-the-loop\nresponse simulation.\n\nUsage:\n    python /path/to/packages/llama-agents-dbos/tests/fixtures/runner.py \\\n        --workflow \"tests.fixtures.sample_workflows.hitl:TestWorkflow\" \\\n        --db-url \"sqlite+pysqlite:///path/to/db\" \\\n        --run-id \"test-001\" \\\n        --config '{\"interrupt_on\": \"AskInputEvent\"}'\n\nConfig modes:\n    - interrupt_on: Interrupt when event type is seen (uses os._exit(0))\n      - String form: \"EventName\" - interrupt on any instance of EventName\n      - Dict form: {\"event\": \"EventName\", \"condition\": {\"field\": value}}\n                   - interrupt only when type matches AND all condition fields match\n    - respond: Respond to InputRequiredEvent subtypes with specified events\n    - run-to-completion: Empty config or omit both fields\n\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport json\nimport os\nimport sys\nimport threading\nfrom pathlib import Path\nfrom typing import Any\n\n# Add fixtures dir to find runner_common; safe now that fixtures/workflows\n# was renamed to fixtures/sample_workflows to avoid shadowing the real package\nsys.path.insert(0, str(Path(__file__).parent))\n\nfrom dbos import DBOS  # noqa: E402\nfrom runner_common import (  # noqa: E402  # ty: ignore[unresolved-import]\n    get_event_class_by_name,\n    import_workflow,\n    setup_dbos,\n)\nfrom workflows.events import Event, InputRequiredEvent, StartEvent  # noqa: E402\nfrom workflows.handler import WorkflowHandler  # noqa: E402\n\n\ndef _call_adapter_close(run_id: str) -> None:\n    \"\"\"Call adapter.close() while DBOS is still alive.\"\"\"\n    from llama_agents.dbos.runtime import InternalDBOSAdapter\n\n    adapter = InternalDBOSAdapter(\n        run_id=run_id,\n        engine=None,  # type: ignore[arg-type]\n        state_type=None,\n    )\n\n    def _run() -> None:\n        asyncio.run(adapter.close())\n\n    t = threading.Thread(target=_run)\n    t.start()\n    t.join(timeout=5)\n\n\nasync def run_workflow(\n    workflow_path: str,\n    db_url: str,\n    run_id: str,\n    config: dict[str, Any],\n    call_close: bool = False,\n) -> None:\n    \"\"\"Run the workflow with the specified configuration.\"\"\"\n    # Import workflow and get module for event class lookup\n    workflow_class, module = import_workflow(workflow_path)\n\n    # Parse config options\n    interrupt_on_config = config.get(\"interrupt_on\")\n    respond_config = config.get(\"respond\", {})\n\n    # Resolve interrupt config (can be string or dict with condition)\n    interrupt_event_class: type[Event] | None = None\n    interrupt_condition: dict[str, Any] | None = None\n    if interrupt_on_config:\n        if isinstance(interrupt_on_config, str):\n            interrupt_event_name = interrupt_on_config\n        else:\n            interrupt_event_name = interrupt_on_config.get(\"event\")\n            interrupt_condition = interrupt_on_config.get(\"condition\")\n        interrupt_event_class = get_event_class_by_name(module, interrupt_event_name)\n        if interrupt_event_class is None:\n            print(\n                f\"ERROR:ValueError:Event class '{interrupt_event_name}' not found in module\"\n            )\n            sys.exit(1)\n\n    # Build response event mapping: {trigger_class: (response_class, fields)}\n    response_map: dict[type[Event], tuple[type[Event], dict[str, Any]]] = {}\n    for trigger_name, response_info in respond_config.items():\n        trigger_class = get_event_class_by_name(module, trigger_name)\n        if trigger_class is None:\n            print(\n                f\"ERROR:ValueError:Trigger event class '{trigger_name}' not found in module\"\n            )\n            sys.exit(1)\n        response_event_name = response_info.get(\"event\")\n        response_fields = response_info.get(\"fields\", {})\n        response_class = get_event_class_by_name(module, response_event_name)\n        if response_class is None:\n            print(\n                f\"ERROR:ValueError:Response event class '{response_event_name}' not found in module\"\n            )\n            sys.exit(1)\n        assert trigger_class is not None\n        assert response_class is not None\n        response_map[trigger_class] = (response_class, response_fields)\n\n    # Set up DBOS and runtime\n    runtime = setup_dbos(db_url)\n\n    # Create workflow instance and launch\n    wf = workflow_class(runtime=runtime)\n    await runtime.launch()\n\n    try:\n        # Check if the workflow already exists (i.e., we're resuming after interrupt).\n        # DBOS auto-recovers pending workflows on launch(), so we just need to\n        # attach to the existing run instead of starting a new one.\n        existing = await DBOS.get_workflow_status_async(run_id)\n        if existing is not None:\n            # Attach to the auto-recovered workflow\n            external_adapter = runtime.get_external_adapter(run_id)\n            handler = WorkflowHandler(wf, external_adapter)\n        else:\n            # Fresh run - start the workflow\n            handler = wf.run(start_event=StartEvent(), run_id=run_id)\n\n        async for event in handler.stream_events():\n            event_name = type(event).__name__\n            print(f\"EVENT:{event_name}\", flush=True)\n\n            # Check for interrupt condition\n            if interrupt_event_class is not None and isinstance(\n                event, interrupt_event_class\n            ):\n                should_interrupt = True\n                if interrupt_condition:\n                    for field, expected_value in interrupt_condition.items():\n                        actual_value = getattr(event, field, None)\n                        if actual_value != expected_value:\n                            should_interrupt = False\n                            break\n                if should_interrupt:\n                    print(\"INTERRUPTING\", flush=True)\n                    if call_close:\n                        _call_adapter_close(run_id)\n                        print(\"CLOSE_CALLED\", flush=True)\n                    os._exit(0)\n\n            # Check for response condition (InputRequiredEvent subtypes)\n            if isinstance(event, InputRequiredEvent):\n                for trigger_class, (response_class, fields) in response_map.items():\n                    if isinstance(event, trigger_class):\n                        if handler.ctx:\n                            response_event = response_class(**fields)\n                            handler.ctx.send_event(response_event)\n                        break\n\n        result = await handler\n        print(f\"RESULT:{result}\", flush=True)\n        print(\"SUCCESS\", flush=True)\n\n    except Exception as e:\n        print(f\"ERROR:{type(e).__name__}:{e}\", flush=True)\n        raise\n\n    finally:\n        await runtime.destroy()\n\n\ndef main() -> None:\n    \"\"\"Entry point for the subprocess runner.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Run workflows in isolated subprocesses for testing\"\n    )\n    parser.add_argument(\"--workflow\", required=True)\n    parser.add_argument(\"--db-url\", required=True)\n    parser.add_argument(\"--run-id\", required=True)\n    parser.add_argument(\"--config\", default=None)\n    parser.add_argument(\"--call-close\", action=\"store_true\")\n\n    args = parser.parse_args()\n\n    asyncio.run(\n        run_workflow(\n            workflow_path=args.workflow,\n            db_url=args.db_url,\n            run_id=args.run_id,\n            config=json.loads(args.config) if args.config else {},\n            call_close=args.call_close,\n        )\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/runner_common.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Shared utilities for subprocess test runners.\n\nImporting this module adds all necessary package source directories to sys.path\nas a side effect, so that runner scripts can import workflows, llama_agents, etc.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib\nimport sys\nfrom pathlib import Path\nfrom types import ModuleType\n\n# Compute package source directories relative to this file\nTESTS_DIR = Path(__file__).parent.parent\nDBOS_PACKAGE_DIR = TESTS_DIR.parent\n_SYS_PATHS = [\n    str(DBOS_PACKAGE_DIR),\n    str(DBOS_PACKAGE_DIR / \"src\"),\n    str(DBOS_PACKAGE_DIR.parent / \"llama-index-workflows\" / \"src\"),\n    str(DBOS_PACKAGE_DIR.parent / \"llama-agents-server\" / \"src\"),\n    str(DBOS_PACKAGE_DIR.parent / \"llama-agents-client\" / \"src\"),\n]\n\nfor _p in _SYS_PATHS:\n    if _p not in sys.path:\n        sys.path.insert(0, _p)\n\nfrom dbos import DBOS, DBOSConfig  # noqa: E402\nfrom llama_agents.dbos import DBOSRuntime  # noqa: E402\nfrom workflows.events import Event  # noqa: E402\nfrom workflows.workflow import Workflow  # noqa: E402\n\n\ndef import_workflow(path: str) -> tuple[type[Workflow], ModuleType]:\n    \"\"\"Import a workflow class from a module path like 'module.path:ClassName'.\"\"\"\n    if \":\" not in path:\n        raise ValueError(f\"Invalid workflow path format: {path}\")\n    module_path, class_name = path.rsplit(\":\", 1)\n    module = importlib.import_module(module_path)\n    workflow_class = getattr(module, class_name)\n    if not (isinstance(workflow_class, type) and issubclass(workflow_class, Workflow)):\n        raise TypeError(f\"{class_name} is not a Workflow subclass\")\n    return workflow_class, module\n\n\ndef get_event_class_by_name(module: ModuleType, name: str) -> type[Event] | None:\n    \"\"\"Find an event class in a module by its name.\"\"\"\n    for attr_name in dir(module):\n        attr = getattr(module, attr_name)\n        if isinstance(attr, type) and issubclass(attr, Event) and attr.__name__ == name:\n            return attr\n    return None\n\n\ndef setup_dbos(db_url: str, app_name: str = \"test-workflow\") -> DBOSRuntime:\n    \"\"\"Set up DBOS with the given database URL and return a DBOSRuntime.\"\"\"\n    config: DBOSConfig = {\n        \"name\": app_name,\n        \"system_database_url\": db_url,\n        \"run_admin_server\": False,\n        \"notification_listener_polling_interval_sec\": 0.01,\n    }\n    DBOS(config=config)\n    return DBOSRuntime(polling_interval_sec=0.01)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/chained.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Chained workflow fixture with StepOneEvent, StepTwoEvent, and ChainedWorkflow.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\nclass StepOneEvent(Event):\n    value: str = Field(default=\"one\")\n\n\nclass StepTwoEvent(Event):\n    value: str = Field(default=\"two\")\n\n\nclass ChainedWorkflow(Workflow):\n    @step\n    async def step_one(self, ctx: Context, ev: StartEvent) -> StepOneEvent:\n        await ctx.store.set(\"step_one\", True)\n        print(\"STEP:one:complete\", flush=True)\n        return StepOneEvent()\n\n    @step\n    async def step_two(self, ctx: Context, ev: StepOneEvent) -> StepTwoEvent:\n        await ctx.store.set(\"step_two\", True)\n        print(\"STEP:two:complete\", flush=True)\n        return StepTwoEvent()\n\n    @step\n    async def step_three(self, ctx: Context, ev: StepTwoEvent) -> StopEvent:\n        await ctx.store.set(\"step_three\", True)\n        print(\"STEP:three:complete\", flush=True)\n        return StopEvent(result=\"done\")\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/concurrent_workers.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Concurrent workers workflow fixture with num_workers=2.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport random\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\nclass WorkItem(Event):\n    item_id: int = Field(default=0)\n\n\nclass WorkDone(Event):\n    item_id: int = Field(default=0)\n\n\nclass ConcurrentWorkersWorkflow(Workflow):\n    @step\n    async def dispatch(self, ctx: Context, ev: StartEvent) -> WorkItem:\n        # Dispatch work items that will be processed by concurrent workers\n        ctx.send_event(WorkItem(item_id=1))\n        ctx.send_event(WorkItem(item_id=2))\n        print(\"STEP:dispatch:complete\", flush=True)\n        return WorkItem(item_id=0)\n\n    @step(num_workers=2)\n    async def worker(self, ctx: Context, ev: WorkItem) -> WorkDone:\n        # Variable processing time for each item\n        await asyncio.sleep(random.uniform(0.01, 0.05))\n        print(f\"STEP:worker:{ev.item_id}:complete\", flush=True)\n        return WorkDone(item_id=ev.item_id)\n\n    @step\n    async def finish(self, ctx: Context, ev: WorkDone) -> StopEvent:\n        # First WorkDone to arrive ends the workflow\n        print(f\"STEP:finish:{ev.item_id}:complete\", flush=True)\n        return StopEvent(result={\"first_done\": ev.item_id})\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/counter.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"HITL counter workflow for double-restart testing.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import Any\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import (\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.workflow import Workflow\n\n\nclass CounterTickEvent(InputRequiredEvent):\n    count: int = Field(default=0)\n\n\nclass CounterContinueEvent(HumanResponseEvent):\n    pass\n\n\nclass CounterWorkflow(Workflow):\n    def __init__(self, target: int = 40, **kwargs: Any) -> None:\n        super().__init__(timeout=None, **kwargs)\n        self._target = target\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> CounterTickEvent:\n        await ctx.store.set(\"count\", 0)\n        print(\"STEP:start:complete\", flush=True)\n        return CounterTickEvent(count=0)\n\n    @step\n    async def on_tick(\n        self, ctx: Context, ev: CounterContinueEvent\n    ) -> CounterTickEvent | StopEvent:\n        count = await ctx.store.get(\"count\", default=0)\n        count += 1\n        await ctx.store.set(\"count\", count)\n        print(f\"STEP:increment:{count}\", flush=True)\n\n        if count >= self._target:\n            return StopEvent(result={\"count\": count})\n\n        await asyncio.sleep(0.2)\n        return CounterTickEvent(count=count)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/hitl.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Basic HITL workflow fixture with AskInputEvent and UserInput events.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import (\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.workflow import Workflow\n\n\nclass AskInputEvent(InputRequiredEvent):\n    prefix: str = Field(default=\"Enter: \")\n\n\nclass UserInput(HumanResponseEvent):\n    response: str = Field(default=\"\")\n\n\nclass TestWorkflow(Workflow):\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(timeout=None, **kwargs)\n\n    @step\n    async def ask(self, ctx: Context, ev: StartEvent) -> AskInputEvent:\n        await ctx.store.set(\"asked\", True)\n        print(\"STEP:ask:complete\", flush=True)\n        return AskInputEvent()\n\n    @step\n    async def process(self, ctx: Context, ev: UserInput) -> StopEvent:\n        await ctx.store.set(\"processed\", ev.response)\n        print(\"STEP:process:complete\", flush=True)\n        return StopEvent(result={\"response\": ev.response})\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/parallel.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Parallel workflow fixture with ResultAEvent, ResultBEvent, and ParallelWorkflow.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport random\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\nclass ResultAEvent(Event):\n    value: str = Field(default=\"\")\n\n\nclass ResultBEvent(Event):\n    value: str = Field(default=\"\")\n\n\nclass ParallelWorkflow(Workflow):\n    @step\n    async def branch_a(self, ctx: Context, ev: StartEvent) -> ResultAEvent:\n        # Variable processing time - may complete before or after branch_b\n        await asyncio.sleep(random.uniform(0.01, 0.05))\n        print(\"STEP:branch_a:complete\", flush=True)\n        return ResultAEvent(value=\"a_result\")\n\n    @step\n    async def branch_b(self, ctx: Context, ev: StartEvent) -> ResultBEvent:\n        # Variable processing time - may complete before or after branch_a\n        await asyncio.sleep(random.uniform(0.01, 0.05))\n        print(\"STEP:branch_b:complete\", flush=True)\n        return ResultBEvent(value=\"b_result\")\n\n    @step\n    async def finish_a(self, ctx: Context, ev: ResultAEvent) -> StopEvent:\n        print(\"STEP:finish_a:complete\", flush=True)\n        return StopEvent(result={\"winner\": \"a\", \"value\": ev.value})\n\n    @step\n    async def finish_b(self, ctx: Context, ev: ResultBEvent) -> StopEvent:\n        print(\"STEP:finish_b:complete\", flush=True)\n        return StopEvent(result={\"winner\": \"b\", \"value\": ev.value})\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/sequential_hitl.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Sequential HITL workflow fixture with ProcessedEvent and WaitForInputEvent.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport random\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.workflow import Workflow\n\n\nclass ProcessedEvent(Event):\n    value: str = Field(default=\"\")\n\n\nclass WaitForInputEvent(InputRequiredEvent):\n    prompt: str = Field(default=\"\")\n\n\nclass UserContinueEvent(HumanResponseEvent):\n    continue_value: str = Field(default=\"\")\n\n\nclass SequentialHITLWorkflow(Workflow):\n    @step\n    async def process(self, ctx: Context, ev: StartEvent) -> ProcessedEvent:\n        await asyncio.sleep(random.uniform(0.01, 0.05))\n        print(\"STEP:process:complete\", flush=True)\n        return ProcessedEvent(value=\"processed\")\n\n    @step\n    async def ask_user(self, ctx: Context, ev: ProcessedEvent) -> WaitForInputEvent:\n        print(\"STEP:ask_user:triggering_wait\", flush=True)\n        return WaitForInputEvent(prompt=f\"Got {ev.value}\")\n\n    @step\n    async def finalize(self, ctx: Context, ev: UserContinueEvent) -> StopEvent:\n        print(f\"STEP:finalize:complete:{ev.continue_value}\", flush=True)\n        return StopEvent(result={\"continue\": ev.continue_value})\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/slow_fan_out_hitl.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Fan-out HITL workflow for orphan-purge tests.\n\nFans out to 5 concurrent workers per round. Used to verify that orphaned\nDBOS operation_outputs rows are purged on recovery.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import Any\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.workflow import Workflow\n\nWORKER_NAMES = [\"alpha\", \"beta\", \"gamma\", \"delta\", \"epsilon\"]\nNUM_WORKERS = len(WORKER_NAMES)\n\n\nclass SlowFanOutTickEvent(InputRequiredEvent):\n    round: int = Field(default=0)\n\n\nclass SlowFanOutContinueEvent(HumanResponseEvent):\n    pass\n\n\nclass SlowWorkRequestEvent(Event):\n    worker_name: str = Field(default=\"\")\n    round: int = Field(default=0)\n\n\nclass SlowWorkResultEvent(Event):\n    worker_name: str = Field(default=\"\")\n    round: int = Field(default=0)\n    value: str = Field(default=\"\")\n\n\nclass SlowFanOutWorkflow(Workflow):\n    \"\"\"Fan-out workflow with concurrent workers for orphan-purge testing.\"\"\"\n\n    def __init__(self, num_rounds: int = 4, **kwargs: Any) -> None:\n        super().__init__(timeout=None, **kwargs)\n        self._num_rounds = num_rounds\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> SlowWorkRequestEvent:\n        print(\"STEP:start:complete\", flush=True)\n        for name in WORKER_NAMES[1:]:\n            ctx.send_event(SlowWorkRequestEvent(worker_name=name, round=0))\n        return SlowWorkRequestEvent(worker_name=WORKER_NAMES[0], round=0)\n\n    @step\n    async def worker_alpha(\n        self, ctx: Context, ev: SlowWorkRequestEvent\n    ) -> SlowWorkResultEvent | None:\n        if ev.worker_name != \"alpha\":\n            return None\n        await asyncio.sleep(0.01)\n        print(f\"STEP:worker_alpha:round={ev.round}\", flush=True)\n        return SlowWorkResultEvent(\n            worker_name=\"alpha\", round=ev.round, value=f\"alpha-r{ev.round}\"\n        )\n\n    @step\n    async def worker_beta(\n        self, ctx: Context, ev: SlowWorkRequestEvent\n    ) -> SlowWorkResultEvent | None:\n        if ev.worker_name != \"beta\":\n            return None\n        await asyncio.sleep(0.01)\n        print(f\"STEP:worker_beta:round={ev.round}\", flush=True)\n        return SlowWorkResultEvent(\n            worker_name=\"beta\", round=ev.round, value=f\"beta-r{ev.round}\"\n        )\n\n    @step\n    async def worker_gamma(\n        self, ctx: Context, ev: SlowWorkRequestEvent\n    ) -> SlowWorkResultEvent | None:\n        if ev.worker_name != \"gamma\":\n            return None\n        await asyncio.sleep(0.01)\n        print(f\"STEP:worker_gamma:round={ev.round}\", flush=True)\n        return SlowWorkResultEvent(\n            worker_name=\"gamma\", round=ev.round, value=f\"gamma-r{ev.round}\"\n        )\n\n    @step\n    async def worker_delta(\n        self, ctx: Context, ev: SlowWorkRequestEvent\n    ) -> SlowWorkResultEvent | None:\n        if ev.worker_name != \"delta\":\n            return None\n        await asyncio.sleep(0.01)\n        print(f\"STEP:worker_delta:round={ev.round}\", flush=True)\n        return SlowWorkResultEvent(\n            worker_name=\"delta\", round=ev.round, value=f\"delta-r{ev.round}\"\n        )\n\n    @step\n    async def worker_epsilon(\n        self, ctx: Context, ev: SlowWorkRequestEvent\n    ) -> SlowWorkResultEvent | None:\n        if ev.worker_name != \"epsilon\":\n            return None\n        await asyncio.sleep(0.01)\n        print(f\"STEP:worker_epsilon:round={ev.round}\", flush=True)\n        return SlowWorkResultEvent(\n            worker_name=\"epsilon\", round=ev.round, value=f\"epsilon-r{ev.round}\"\n        )\n\n    @step\n    async def collect(\n        self, ctx: Context, ev: SlowWorkResultEvent\n    ) -> SlowFanOutTickEvent | StopEvent | None:\n        key = f\"round_{ev.round}_count\"\n        count: int = await ctx.store.get(key, default=0)\n        count += 1\n        await ctx.store.set(key, count)\n        print(\n            f\"STEP:collect:{ev.worker_name}:round={ev.round}:{count}/{NUM_WORKERS}\",\n            flush=True,\n        )\n        if count < NUM_WORKERS:\n            return None\n        if ev.round + 1 >= self._num_rounds:\n            print(f\"STEP:collect:all_rounds_complete:{ev.round + 1}\", flush=True)\n            return StopEvent(result={\"rounds\": ev.round + 1})\n        return SlowFanOutTickEvent(round=ev.round)\n\n    @step\n    async def on_continue(\n        self, ctx: Context, ev: SlowFanOutContinueEvent\n    ) -> SlowWorkRequestEvent:\n        current_round: int = await ctx.store.get(\"next_round\", default=0)\n        next_round = current_round + 1\n        await ctx.store.set(\"next_round\", next_round)\n        print(f\"STEP:on_continue:round={next_round}\", flush=True)\n        for name in WORKER_NAMES[1:]:\n            ctx.send_event(SlowWorkRequestEvent(worker_name=name, round=next_round))\n        return SlowWorkRequestEvent(worker_name=WORKER_NAMES[0], round=next_round)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/streaming_interrupt.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Streaming workflow for interrupt/resume testing.\n\nLike streaming_stress but collects all WorkDone events before completing,\nensuring stream events (including the interrupt signal) are visible before\nthe workflow can finish.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\nclass ProgressEvent(Event):\n    progress: int = Field(default=0)\n\n\nclass WorkItem(Event):\n    item_id: int = Field(default=0)\n\n\nclass WorkDone(Event):\n    item_id: int = Field(default=0)\n\n\nclass FanOutComplete(Event):\n    pass\n\n\nclass StreamingInterruptWorkflow(Workflow):\n    @step\n    async def fan_out(self, ctx: Context, ev: StartEvent) -> WorkItem | FanOutComplete:\n        for i in range(15):\n            ctx.write_event_to_stream(ProgressEvent(progress=i))\n            ctx.send_event(WorkItem(item_id=i))\n        print(\"STEP:fan_out:dispatched_15_items\", flush=True)\n        ctx.write_event_to_stream(ProgressEvent(progress=999))\n        return FanOutComplete()\n\n    @step(num_workers=4)\n    async def process_work(self, ctx: Context, ev: WorkItem) -> WorkDone:\n        await asyncio.sleep(0.01)\n        ctx.write_event_to_stream(ProgressEvent(progress=100 + ev.item_id))\n        print(f\"STEP:process_work:{ev.item_id}:complete\", flush=True)\n        return WorkDone(item_id=ev.item_id)\n\n    @step\n    async def after_fanout(self, ctx: Context, ev: FanOutComplete) -> None:\n        print(\"STEP:after_fanout:complete\", flush=True)\n        return None\n\n    @step\n    async def collect(self, ctx: Context, ev: WorkDone) -> StopEvent | None:\n        # Wait for all workers to finish, preventing early completion\n        # that could race with stream event consumption\n        results = ctx.collect_events(ev, [WorkDone] * 15)\n        if results is None:\n            return None\n        print(\"STEP:collect:all_done\", flush=True)\n        return StopEvent(result={\"collected\": len(results)})\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/streaming_stress.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Streaming stress workflow fixture with many concurrent stream writes.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\nclass ProgressEvent(Event):\n    progress: int = Field(default=0)\n\n\nclass WorkItem(Event):\n    item_id: int = Field(default=0)\n\n\nclass WorkDone(Event):\n    item_id: int = Field(default=0)\n    total_processed: int = Field(default=0)\n\n\nclass FanOutComplete(Event):\n    pass\n\n\nclass StreamingStressWorkflow(Workflow):\n    @step\n    async def fan_out(self, ctx: Context, ev: StartEvent) -> WorkItem | FanOutComplete:\n        # Fire many stream writes and internal events concurrently\n        # This creates many background tasks that call DBOS operations\n        for i in range(15):\n            ctx.write_event_to_stream(ProgressEvent(progress=i))\n            ctx.send_event(WorkItem(item_id=i))\n        print(\"STEP:fan_out:dispatched_15_items\", flush=True)\n        # Write completion signal to stream for interrupt tests\n        ctx.write_event_to_stream(ProgressEvent(progress=999))\n        return FanOutComplete()\n\n    @step(num_workers=4)\n    async def process_work(self, ctx: Context, ev: WorkItem) -> WorkDone:\n        # Each worker also writes to stream, creating more concurrent DBOS ops\n        await asyncio.sleep(0.01)  # Small delay to increase interleaving\n        ctx.write_event_to_stream(ProgressEvent(progress=100 + ev.item_id))\n        print(f\"STEP:process_work:{ev.item_id}:complete\", flush=True)\n        return WorkDone(item_id=ev.item_id)\n\n    @step\n    async def after_fanout(self, ctx: Context, ev: FanOutComplete) -> None:\n        # Consume FanOutComplete, don't trigger anything\n        print(\"STEP:after_fanout:complete\", flush=True)\n        return None\n\n    @step\n    async def collect(self, ctx: Context, ev: WorkDone) -> StopEvent:\n        # First WorkDone ends the workflow\n        print(f\"STEP:collect:{ev.item_id}:complete\", flush=True)\n        return StopEvent(result={\"first_done\": ev.item_id})\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/sample_workflows/three_step_hitl.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Three-step HITL workflow fixture with name and quest input events.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import (\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.workflow import Workflow\n\n\nclass NameInputEvent(InputRequiredEvent):\n    prefix: str = Field(default=\"Name: \")\n\n\nclass NameResponseEvent(HumanResponseEvent):\n    response: str = Field(default=\"\")\n\n\nclass QuestInputEvent(InputRequiredEvent):\n    prefix: str = Field(default=\"Quest: \")\n\n\nclass QuestResponseEvent(HumanResponseEvent):\n    response: str = Field(default=\"\")\n\n\nclass HITLWorkflow(Workflow):\n    @step\n    async def ask_name(self, ctx: Context, ev: StartEvent) -> NameInputEvent:\n        await ctx.store.set(\"asked_name\", True)\n        print(\"STEP:ask_name:complete\", flush=True)\n        return NameInputEvent()\n\n    @step\n    async def ask_quest(self, ctx: Context, ev: NameResponseEvent) -> QuestInputEvent:\n        await ctx.store.set(\"name\", ev.response)\n        print(f\"STEP:ask_quest:got_name={ev.response}\", flush=True)\n        print(\"STEP:ask_quest:complete\", flush=True)\n        return QuestInputEvent()\n\n    @step\n    async def complete(self, ctx: Context, ev: QuestResponseEvent) -> StopEvent:\n        name = await ctx.store.get(\"name\", default=\"unknown\")\n        print(f\"STEP:complete:got_quest={ev.response}\", flush=True)\n        return StopEvent(result={\"name\": name, \"quest\": ev.response})\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/fixtures/server_runner.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Subprocess runner for DBOS + WorkflowServer integration tests.\n\nRuns a workflow through the full WorkflowServer decorator chain with the store\ncreated by DBOSRuntime, enabling end-to-end event flow testing including\ninterrupt/resume scenarios.\n\nUsage:\n    python server_runner.py \\\n        --workflow \"tests.fixtures.sample_workflows.chained:ChainedWorkflow\" \\\n        --db-url \"postgresql://user:pass@localhost/db\" \\\n        --run-id \"test-001\" \\\n        --check-streams --check-events\n\n    python server_runner.py \\\n        --workflow \"tests.fixtures.sample_workflows.chained:ChainedWorkflow\" \\\n        --db-url \"postgresql://user:pass@localhost/db\" \\\n        --run-id \"test-001\" \\\n        --interrupt-after StepTwoEvent\n\nFlags:\n    --check-streams: After workflow completes, query dbos.streams table\n                     and print STREAMS_COUNT:<N>\n    --check-events:  After workflow completes, query wf_events table\n                     and print EVENTS_COUNT:<N> and EVENT_JSON:<json>\n    --interrupt-after EVENT_NAME: Kill the process (os._exit) after seeing\n                     a StepStateChanged with output_event_name matching\n                     EVENT_NAME. Simulates a crash for resume testing.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport os\nimport sys\nfrom pathlib import Path\n\nimport asyncpg\n\n# Add fixtures dir to find runner_common; safe now that fixtures/workflows\n# was renamed to fixtures/sample_workflows to avoid shadowing the real package\nsys.path.insert(0, str(Path(__file__).parent))\n\nfrom llama_agents.server import WorkflowServer  # noqa: E402\nfrom runner_common import (  # noqa: E402  # ty: ignore[unresolved-import]\n    import_workflow,\n    setup_dbos,\n)\n\n\nasync def check_streams_count(db_url: str, run_id: str) -> None:\n    \"\"\"Query DBOS streams table and print count of published_events rows.\"\"\"\n    try:\n        dsn = db_url\n        if \"+psycopg2\" in dsn or \"+psycopg\" in dsn:\n            dsn = \"postgresql://\" + dsn.split(\"://\", 1)[1]\n\n        conn = await asyncpg.connect(dsn)\n        try:\n            count = await conn.fetchval(\n                \"SELECT COUNT(*) FROM dbos.streams \"\n                \"WHERE workflow_uuid = $1 AND key = 'published_events'\",\n                run_id,\n            )\n            print(f\"STREAMS_COUNT:{count}\", flush=True)\n        finally:\n            await conn.close()\n    except Exception as e:\n        print(f\"ERROR:{type(e).__name__}:Failed to check streams: {e}\", flush=True)\n\n\nasync def check_events(db_url: str, run_id: str, schema: str | None = None) -> None:\n    \"\"\"Query wf_events table and print all events.\"\"\"\n    try:\n        dsn = db_url\n        if \"+psycopg2\" in dsn or \"+psycopg\" in dsn:\n            dsn = \"postgresql://\" + dsn.split(\"://\", 1)[1]\n\n        events_table = \"wf_events\" if schema is None else f\"{schema}.wf_events\"\n\n        conn = await asyncpg.connect(dsn)\n        try:\n            rows = await conn.fetch(\n                f\"SELECT event_json FROM {events_table} \"\n                f\"WHERE run_id = $1 ORDER BY sequence\",\n                run_id,\n            )\n            print(f\"EVENTS_COUNT:{len(rows)}\", flush=True)\n            for row in rows:\n                print(f\"EVENT_JSON:{row['event_json']}\", flush=True)\n        finally:\n            await conn.close()\n    except Exception as e:\n        print(f\"ERROR:{type(e).__name__}:Failed to check events: {e}\", flush=True)\n\n\nasync def run_workflow_with_server(\n    workflow_path: str,\n    db_url: str,\n    run_id: str,\n    do_check_streams: bool,\n    do_check_events: bool,\n    interrupt_after: str | None = None,\n) -> None:\n    \"\"\"Run workflow through the full WorkflowServer + DBOS decorator chain.\"\"\"\n    workflow_class, _module = import_workflow(workflow_path)\n\n    dbos_runtime = setup_dbos(db_url, app_name=\"test-server-workflow\")\n\n    wf = workflow_class(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    store = dbos_runtime.create_workflow_store()\n\n    server_runtime = dbos_runtime.build_server_runtime()\n\n    server = WorkflowServer(\n        runtime=server_runtime,\n        workflow_store=store,\n        idle_timeout=60.0,\n    )\n    server.add_workflow(\"test\", wf)\n\n    schema = dbos_runtime._schema\n\n    try:\n        async with server.contextmanager():\n            wf_ref = server._service._runtime.get_workflow(\"test\")\n            assert wf_ref is not None\n\n            handler_data = await server._service.start_workflow(wf_ref, run_id)\n            actual_run_id = handler_data.run_id\n            assert actual_run_id is not None\n\n            async for stored_event in store.subscribe_events(actual_run_id):\n                event_type = stored_event.event.type\n                print(f\"EVENT:{event_type}\", flush=True)\n\n                # Check for interrupt: StepStateChanged events carry\n                # output_event_name as a class repr string.\n                if interrupt_after is not None:\n                    data = stored_event.event.value or {}\n                    output_name = data.get(\"output_event_name\") or \"\"\n                    if interrupt_after in output_name:\n                        print(\"INTERRUPTING\", flush=True)\n                        os._exit(0)\n\n            print(\"SUCCESS\", flush=True)\n\n            if do_check_streams:\n                await check_streams_count(db_url, actual_run_id)\n\n            if do_check_events:\n                await check_events(db_url, actual_run_id, schema)\n\n    except Exception as e:\n        print(f\"ERROR:{type(e).__name__}:{e}\", flush=True)\n        raise\n    finally:\n        await dbos_runtime.destroy()\n\n\ndef main() -> None:\n    \"\"\"Entry point for the subprocess runner.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Run workflows through WorkflowServer + DBOS for testing\"\n    )\n    parser.add_argument(\"--workflow\", required=True)\n    parser.add_argument(\"--db-url\", required=True)\n    parser.add_argument(\"--run-id\", required=True)\n    parser.add_argument(\"--check-streams\", action=\"store_true\")\n    parser.add_argument(\"--check-events\", action=\"store_true\")\n    parser.add_argument(\n        \"--interrupt-after\",\n        default=None,\n        help=\"Event name to interrupt after (simulates crash)\",\n    )\n\n    args = parser.parse_args()\n\n    asyncio.run(\n        run_workflow_with_server(\n            workflow_path=args.workflow,\n            db_url=args.db_url,\n            run_id=args.run_id,\n            do_check_streams=args.check_streams,\n            do_check_events=args.check_events,\n            interrupt_after=args.interrupt_after,\n        )\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_dbos_cross_process.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Cross-process integration tests for DBOS distributed model.\n\nTests verify that two WorkflowServer replicas sharing a Postgres database\ncan coordinate workflow execution: one replica runs the workflow while the\nother sends events and streams results.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\nimport sys\nimport time\nfrom pathlib import Path\n\nimport httpx\nimport pytest\nfrom llama_agents.client import WorkflowClient\n\nREPLICA_SERVER_PATH = str(Path(__file__).parent / \"fixtures\" / \"replica_server.py\")\nWORKFLOW_PATH = \"tests.fixtures.sample_workflows.hitl:TestWorkflow\"\n\npytestmark = [pytest.mark.docker]\n\nPORT_A = 18001\nPORT_B = 18002\n\n\ndef start_replica(port: int, db_url: str) -> subprocess.Popen[str]:\n    return subprocess.Popen(\n        [\n            sys.executable,\n            REPLICA_SERVER_PATH,\n            \"--workflow\",\n            WORKFLOW_PATH,\n            \"--db-url\",\n            db_url,\n            \"--port\",\n            str(port),\n        ],\n        text=True,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        start_new_session=True,\n    )\n\n\ndef wait_for_server(\n    proc: subprocess.Popen[str], port: int, timeout: float = 30.0\n) -> None:\n    deadline = time.monotonic() + timeout\n    while time.monotonic() < deadline:\n        # Check if process died\n        if proc.poll() is not None:\n            stdout = proc.stdout.read() if proc.stdout else \"\"\n            raise RuntimeError(\n                f\"Replica on port {port} exited with code {proc.returncode}\\n\"\n                f\"output: {stdout}\"\n            )\n        try:\n            resp = httpx.get(f\"http://localhost:{port}/workflows\", timeout=2.0)\n            if resp.status_code == 200:\n                return\n        except httpx.ConnectError:\n            pass\n        time.sleep(0.5)\n    # Grab whatever output we can\n    proc.kill()\n    stdout = proc.stdout.read() if proc.stdout else \"\"\n    raise RuntimeError(\n        f\"Server on port {port} did not start in {timeout}s\\noutput: {stdout}\"\n    )\n\n\n@pytest.mark.timeout(60)\nasync def test_cross_process_event_delivery(postgres_dsn: str) -> None:\n    \"\"\"Event sent via Replica B reaches workflow running in Replica A.\"\"\"\n    from tests.fixtures.sample_workflows.hitl import UserInput\n\n    replicas: list[subprocess.Popen[str]] = []\n    try:\n        # Start replicas sequentially to avoid DBOS CREATE SCHEMA race\n        replicas.append(start_replica(PORT_A, postgres_dsn))\n        wait_for_server(replicas[0], PORT_A)\n        replicas.append(start_replica(PORT_B, postgres_dsn))\n        wait_for_server(replicas[1], PORT_B)\n\n        client_a = WorkflowClient(base_url=f\"http://localhost:{PORT_A}\")\n        client_b = WorkflowClient(base_url=f\"http://localhost:{PORT_B}\")\n\n        # Start workflow on Replica A — step \"ask\" emits AskInputEvent\n        handler = await client_a.run_workflow_nowait(\"test\")\n        handler_id = handler.handler_id\n\n        # Send UserInput via Replica B — step \"process\" on Replica A receives it\n        await client_b.send_event(handler_id, UserInput(response=\"cross-process\"))\n\n        # Stream events from Replica A — should see StopEvent\n        got_stop = False\n        async for event in client_a.get_workflow_events(handler_id, after_sequence=-1):\n            if event.type == \"StopEvent\":\n                got_stop = True\n\n        assert got_stop, \"Workflow should have completed with StopEvent\"\n\n        # Stream events from Replica B — should also see the same events\n        got_stop_b = False\n        async for event in client_b.get_workflow_events(handler_id, after_sequence=-1):\n            if event.type == \"StopEvent\":\n                got_stop_b = True\n\n        assert got_stop_b, (\n            \"Replica B should also see StopEvent via shared Postgres store\"\n        )\n    finally:\n        for proc in replicas:\n            proc.kill()\n            proc.wait()\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_dbos_determinism_subprocess.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Test DBOS determinism with subprocess isolation and real interruption.\n\nThis test spawns subprocesses to properly isolate DBOS state and simulate\nreal Ctrl+C interruptions during workflow execution.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\nfrom conftest import (\n    assert_no_determinism_errors,  # pyright: ignore[reportAttributeAccessIssue]\n    log_on_failure,  # pyright: ignore[reportAttributeAccessIssue]\n    run_scenario,  # pyright: ignore[reportAttributeAccessIssue]\n)\n\n\n@pytest.fixture\ndef test_db_path(tmp_path: Path) -> Path:\n    \"\"\"Create a temporary database path.\"\"\"\n    return tmp_path / \"dbos_test.sqlite3\"\n\n\n# =============================================================================\n# Test 1: Basic interrupt/resume with input events\n# =============================================================================\n\n\ndef test_determinism_on_resume_after_interrupt(test_db_path: Path) -> None:\n    \"\"\"Test that resuming an interrupted workflow doesn't hit determinism errors.\"\"\"\n    run_id = \"test-determinism-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result1 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.hitl:TestWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\"interrupt_on\": \"AskInputEvent\"},\n    )\n    log_on_failure(result1, \"initial run\")\n\n    assert \"STEP:ask:complete\" in result1.stdout, \"First step should complete\"\n    assert \"INTERRUPTING\" in result1.stdout, \"Should have interrupted\"\n\n    result2 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.hitl:TestWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\n            \"respond\": {\n                \"AskInputEvent\": {\n                    \"event\": \"UserInput\",\n                    \"fields\": {\"response\": \"test_input\"},\n                }\n            }\n        },\n    )\n    log_on_failure(result2, \"resume\")\n\n    assert_no_determinism_errors(result2)\n    assert \"SUCCESS\" in result2.stdout, (\n        f\"Resume should succeed. stdout: {result2.stdout}, stderr: {result2.stderr}\"\n    )\n\n\n# =============================================================================\n# Test 2: Chained steps determinism\n# =============================================================================\n\n\ndef test_chained_steps_determinism_on_resume(test_db_path: Path) -> None:\n    \"\"\"Test determinism with chained steps that trigger each other.\"\"\"\n    run_id = \"test-chained-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result1 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.chained:ChainedWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\"interrupt_on\": \"StepTwoEvent\"},\n    )\n    log_on_failure(result1, \"initial run\")\n\n    assert \"STEP:one:complete\" in result1.stdout, \"Step one should complete\"\n\n    result2 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.chained:ChainedWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n    log_on_failure(result2, \"resume\")\n\n    assert_no_determinism_errors(result2)\n\n\n# =============================================================================\n# Test 3: Three-step HITL pattern\n# =============================================================================\n\n\ndef test_hitl_three_step_determinism(test_db_path: Path) -> None:\n    \"\"\"Test the exact HITL pattern with three steps and input events.\"\"\"\n    run_id = \"test-hitl-three-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result1 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.three_step_hitl:HITLWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\n            \"respond\": {\n                \"NameInputEvent\": {\n                    \"event\": \"NameResponseEvent\",\n                    \"fields\": {\"response\": \"Alice\"},\n                }\n            },\n            \"interrupt_on\": \"QuestInputEvent\",\n        },\n    )\n    log_on_failure(result1, \"initial run\")\n\n    assert \"STEP:ask_name:complete\" in result1.stdout, \"ask_name should complete\"\n    assert \"STEP:ask_quest\" in result1.stdout, \"ask_quest should start\"\n    assert \"INTERRUPTING\" in result1.stdout, \"Should interrupt at quest\"\n\n    result2 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.three_step_hitl:HITLWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\n            \"respond\": {\n                \"NameInputEvent\": {\n                    \"event\": \"NameResponseEvent\",\n                    \"fields\": {\"response\": \"Alice\"},\n                },\n                \"QuestInputEvent\": {\n                    \"event\": \"QuestResponseEvent\",\n                    \"fields\": {\"response\": \"seek the grail\"},\n                },\n            },\n        },\n    )\n    log_on_failure(result2, \"resume\")\n\n    assert_no_determinism_errors(result2)\n    assert \"SUCCESS\" in result2.stdout, (\n        f\"Resume should succeed.\\nstdout: {result2.stdout}\\nstderr: {result2.stderr}\"\n    )\n\n\n# =============================================================================\n# Test 4: Parallel steps - two steps triggered by StartEvent\n# =============================================================================\n\n\ndef test_parallel_steps_determinism(test_db_path: Path) -> None:\n    \"\"\"Test determinism with parallel steps completing in non-deterministic order.\"\"\"\n    run_id = \"test-parallel-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result1 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.parallel:ParallelWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n    log_on_failure(result1, \"parallel run\")\n\n    assert \"SUCCESS\" in result1.stdout, (\n        f\"Should complete successfully.\\nstdout: {result1.stdout}\\nstderr: {result1.stderr}\"\n    )\n    assert_no_determinism_errors(result1)\n\n\n# =============================================================================\n# Test 5: Concurrent workers on same step (num_workers=2)\n# =============================================================================\n\n\ndef test_concurrent_workers_determinism(test_db_path: Path) -> None:\n    \"\"\"Test determinism with multiple workers on same step (num_workers > 1).\"\"\"\n    run_id = \"test-concurrent-workers-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result1 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.concurrent_workers:ConcurrentWorkersWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n    log_on_failure(result1, \"concurrent workers run\")\n\n    assert \"SUCCESS\" in result1.stdout, (\n        f\"Should complete successfully.\\nstdout: {result1.stdout}\\nstderr: {result1.stderr}\"\n    )\n    assert_no_determinism_errors(result1)\n\n\n# =============================================================================\n# Test 6: Sequential steps with HITL\n# =============================================================================\n\n\ndef test_sequential_hitl_interrupt_resume(test_db_path: Path) -> None:\n    \"\"\"Test sequential steps with HITL interrupt and resume.\"\"\"\n    run_id = \"test-seq-hitl-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result1 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.sequential_hitl:SequentialHITLWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\"interrupt_on\": \"WaitForInputEvent\"},\n    )\n    log_on_failure(result1, \"initial run\")\n\n    assert \"STEP:process:complete\" in result1.stdout\n    assert \"INTERRUPTING\" in result1.stdout\n\n    result2 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.sequential_hitl:SequentialHITLWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\n            \"respond\": {\n                \"WaitForInputEvent\": {\n                    \"event\": \"UserContinueEvent\",\n                    \"fields\": {\"continue_value\": \"user_input\"},\n                }\n            }\n        },\n    )\n    log_on_failure(result2, \"resume\")\n\n    assert_no_determinism_errors(result2)\n    assert \"SUCCESS\" in result2.stdout, (\n        f\"Resume should succeed.\\nstdout: {result2.stdout}\\nstderr: {result2.stderr}\"\n    )\n\n\n# =============================================================================\n# Stress tests - run scenarios multiple times to catch flaky timing issues\n# =============================================================================\n\n\n@pytest.mark.timeout(60)\n@pytest.mark.parametrize(\"iteration\", range(5))\ndef test_parallel_steps_stress(test_db_path: Path, iteration: int) -> None:\n    \"\"\"Stress test parallel steps - run 5 times to catch timing issues.\"\"\"\n    run_id = f\"test-parallel-stress-{iteration}\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.parallel:ParallelWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n\n    assert \"SUCCESS\" in result.stdout, (\n        f\"Iteration {iteration} failed.\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n    )\n    assert_no_determinism_errors(result)\n\n\n@pytest.mark.timeout(60)\n@pytest.mark.parametrize(\"iteration\", range(5))\ndef test_concurrent_workers_stress(test_db_path: Path, iteration: int) -> None:\n    \"\"\"Stress test concurrent workers - run 5 times to catch timing issues.\"\"\"\n    run_id = f\"test-concurrent-stress-{iteration}\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.concurrent_workers:ConcurrentWorkersWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n\n    assert \"SUCCESS\" in result.stdout, (\n        f\"Iteration {iteration} failed.\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n    )\n    assert_no_determinism_errors(result)\n\n\n# =============================================================================\n# Test 7: Streaming stress test\n# =============================================================================\n\n\ndef test_streaming_stress_determinism(test_db_path: Path) -> None:\n    \"\"\"Test determinism with many concurrent stream writes and send_event calls.\"\"\"\n    run_id = \"test-streaming-stress-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.streaming_stress:StreamingStressWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n    log_on_failure(result, \"streaming stress\")\n\n    assert \"SUCCESS\" in result.stdout, (\n        f\"Should complete successfully.\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n    )\n    assert_no_determinism_errors(result)\n\n\ndef test_streaming_interrupt_resume(test_db_path: Path) -> None:\n    \"\"\"Test interrupt/resume with many concurrent stream writes in flight.\"\"\"\n    run_id = \"test-streaming-interrupt-001\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result1 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.streaming_interrupt:StreamingInterruptWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n        config={\n            \"interrupt_on\": {\"event\": \"ProgressEvent\", \"condition\": {\"progress\": 999}}\n        },\n    )\n    log_on_failure(result1, \"initial run\")\n\n    assert \"STEP:fan_out:dispatched_15_items\" in result1.stdout, (\n        \"Fan out should complete\"\n    )\n    assert \"INTERRUPTING\" in result1.stdout, \"Should have interrupted\"\n\n    result2 = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.streaming_interrupt:StreamingInterruptWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n    log_on_failure(result2, \"resume\")\n\n    assert_no_determinism_errors(result2)\n    assert \"SUCCESS\" in result2.stdout, (\n        f\"Resume should succeed.\\nstdout: {result2.stdout}\\nstderr: {result2.stderr}\"\n    )\n\n\n@pytest.mark.timeout(60)\n@pytest.mark.parametrize(\"iteration\", range(5))\ndef test_streaming_stress_repeated(test_db_path: Path, iteration: int) -> None:\n    \"\"\"Stress test streaming - run 5 times to catch timing issues.\"\"\"\n    run_id = f\"test-streaming-repeated-{iteration}\"\n    db_url = f\"sqlite+pysqlite:///{test_db_path}?check_same_thread=false\"\n\n    result = run_scenario(\n        workflow=\"tests.fixtures.sample_workflows.streaming_stress:StreamingStressWorkflow\",\n        db_url=db_url,\n        run_id=run_id,\n    )\n\n    assert \"SUCCESS\" in result.stdout, (\n        f\"Iteration {iteration} failed.\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n    )\n    assert_no_determinism_errors(result)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_dbos_idle_release.py",
    "content": "# ty: ignore[invalid-assignment]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Unit tests for DBOSIdleReleaseDecorator.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Any, AsyncGenerator\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom llama_agents.dbos.idle_release import (\n    CRASH_TIMEOUT_SECONDS,\n    DBOSIdleReleaseDecorator,\n    DBOSIdleReleaseExternalRunAdapter,\n    _DBOSIdleReleaseInternalRunAdapter,\n)\nfrom llama_agents.dbos.journal.crud import JournalCrud\nfrom llama_agents.dbos.journal.lifecycle import RunLifecycleLock, RunLifecycleState\nfrom llama_agents.server._store.abstract_workflow_store import (\n    PersistentHandler,\n)\nfrom llama_agents.server._store.memory_workflow_store import MemoryWorkflowStore\nfrom pydantic import BaseModel\nfrom workflows.context import Context\nfrom workflows.context.state_store import InMemoryStateStore, StateStore\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent, WorkflowIdleEvent\nfrom workflows.runtime.runtime_decorators import BaseRuntimeDecorator\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    WaitResult,\n    WaitResultTick,\n    WaitResultTimeout,\n)\nfrom workflows.runtime.types.ticks import (\n    TickAddEvent,\n    TickIdleCheck,\n    TickIdleRelease,\n    WorkflowTick,\n    WorkflowTickAdapter,\n)\nfrom workflows.workflow import Workflow\n\n# -- Stubs -----------------------------------------------------------------\n\n\nclass StubInternalAdapter(InternalRunAdapter):\n    def __init__(self, run_id: str = \"run-1\") -> None:\n        self._run_id = run_id\n        self.written_events: list[Event] = []\n        self.closed = False\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        self.written_events.append(event)\n\n    async def get_now(self) -> float:\n        return 1.0\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        pass\n\n    async def wait_receive(self, timeout_seconds: float | None = None) -> WaitResult:\n        return WaitResultTimeout()\n\n    async def close(self) -> None:\n        self.closed = True\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return None\n\n\nclass StubExternalAdapter(ExternalRunAdapter):\n    def __init__(self, run_id: str = \"run-1\", result: StopEvent | None = None) -> None:\n        self._run_id = run_id\n        self._result = result or StopEvent(result=\"done\")\n        self.sent_events: list[WorkflowTick] = []\n        self.closed = False\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        self.sent_events.append(tick)\n\n    async def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        yield self._result\n\n    async def close(self) -> None:\n        self.closed = True\n\n    async def get_result(self) -> StopEvent:\n        return self._result\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return None\n\n\nclass StubRuntime(Runtime):\n    def __init__(self) -> None:\n        super().__init__()\n        self.run_workflow_calls: list[dict[str, Any]] = []\n        self._external_adapters: dict[str, StubExternalAdapter] = {}\n\n    def register(self, workflow: Any) -> RegisteredWorkflow:\n        return RegisteredWorkflow(\n            workflow=workflow, workflow_run_fn=MagicMock(), steps={}\n        )\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Any,\n        init_state: Any,\n        start_event: Any = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: Any = None,\n    ) -> ExternalRunAdapter:\n        self.run_workflow_calls.append(\n            {\n                \"run_id\": run_id,\n                \"workflow\": workflow,\n                \"init_state\": init_state,\n                \"start_event\": start_event,\n                \"serialized_state\": serialized_state,\n                \"serializer\": serializer,\n            }\n        )\n        adapter = StubExternalAdapter(run_id=run_id)\n        self._external_adapters[run_id] = adapter\n        return adapter\n\n    def get_internal_adapter(self, workflow: Any) -> InternalRunAdapter:\n        return StubInternalAdapter()\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        if run_id in self._external_adapters:\n            return self._external_adapters[run_id]\n        adapter = StubExternalAdapter(run_id=run_id)\n        self._external_adapters[run_id] = adapter\n        return adapter\n\n    async def launch(self) -> None:\n        pass\n\n    async def destroy(self) -> None:\n        pass\n\n\nclass SimpleWorkflow(Workflow):\n    @step\n    async def process(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\nclass MyState(BaseModel):\n    counter: int = 0\n\n\nclass StatefulWorkflow(Workflow):\n    @step\n    async def process(self, ctx: Context[MyState], ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\nclass FakeLifecycleLock(RunLifecycleLock):\n    \"\"\"In-memory lifecycle lock that implements the real state machine.\"\"\"\n\n    def __init__(self) -> None:\n        self._states: dict[str, tuple[RunLifecycleState, datetime]] = {}\n\n    async def create(self, run_id: str) -> None:\n        self._states[run_id] = (RunLifecycleState.active, datetime.now(timezone.utc))\n\n    async def begin_release(self, run_id: str) -> bool:\n        entry = self._states.get(run_id)\n        if entry is None or entry[0] != RunLifecycleState.active:\n            return False\n        self._states[run_id] = (RunLifecycleState.releasing, datetime.now(timezone.utc))\n        return True\n\n    async def complete_release(self, run_id: str) -> None:\n        entry = self._states.get(run_id)\n        if entry is not None and entry[0] == RunLifecycleState.releasing:\n            self._states[run_id] = (\n                RunLifecycleState.released,\n                datetime.now(timezone.utc),\n            )\n\n    async def try_begin_resume(\n        self, run_id: str, crash_timeout_seconds: float | None = None\n    ) -> RunLifecycleState | None:\n        entry = self._states.get(run_id)\n        if entry is None:\n            return None\n        state, updated_at = entry\n        if state == RunLifecycleState.active:\n            return None\n        if state == RunLifecycleState.released or (\n            state == RunLifecycleState.releasing\n            and crash_timeout_seconds is not None\n            and (datetime.now(timezone.utc) - updated_at).total_seconds()\n            > crash_timeout_seconds\n        ):\n            self._states[run_id] = (\n                RunLifecycleState.active,\n                datetime.now(timezone.utc),\n            )\n            return RunLifecycleState.released\n        return RunLifecycleState.releasing\n\n    def get_state(self, run_id: str) -> tuple[RunLifecycleState, datetime] | None:\n        \"\"\"Test-only helper for assertions.\"\"\"\n        return self._states.get(run_id)\n\n    def set_updated_at(self, run_id: str, updated_at: datetime) -> None:\n        \"\"\"Test hook: override the updated_at timestamp for crash timeout testing.\"\"\"\n        entry = self._states.get(run_id)\n        if entry is not None:\n            self._states[run_id] = (entry[0], updated_at)\n\n\n# -- Fixtures --------------------------------------------------------------\n\n\n@pytest.fixture()\ndef store() -> MemoryWorkflowStore:\n    return MemoryWorkflowStore()\n\n\n@pytest.fixture()\ndef stub_runtime() -> StubRuntime:\n    return StubRuntime()\n\n\n@pytest.fixture()\ndef mock_journal_crud() -> AsyncMock:\n    return AsyncMock(spec=JournalCrud)\n\n\n@pytest.fixture()\ndef mock_dbos():\n    with patch(\"llama_agents.dbos.idle_release.DBOS\") as dbos:\n        dbos.delete_workflow_async = AsyncMock()\n        dbos.retrieve_workflow_async = AsyncMock()\n        dbos.retrieve_workflow_async.return_value = AsyncMock()\n        yield dbos\n\n\n@pytest.fixture()\ndef lifecycle() -> FakeLifecycleLock:\n    return FakeLifecycleLock()\n\n\ndef _make_decorator(\n    stub_runtime: StubRuntime,\n    store: MemoryWorkflowStore,\n    mock_journal_crud: AsyncMock,\n    lifecycle_lock: RunLifecycleLock,\n    idle_timeout: float = 0.1,\n) -> DBOSIdleReleaseDecorator:\n    return DBOSIdleReleaseDecorator(\n        BaseRuntimeDecorator(stub_runtime),\n        store,\n        idle_timeout=idle_timeout,\n        journal_crud=lambda: mock_journal_crud,\n        lifecycle_lock=lambda: lifecycle_lock,\n    )\n\n\n@pytest.fixture()\ndef decorator(\n    stub_runtime: StubRuntime,\n    store: MemoryWorkflowStore,\n    mock_journal_crud: AsyncMock,\n    lifecycle: FakeLifecycleLock,\n) -> DBOSIdleReleaseDecorator:\n    return _make_decorator(stub_runtime, store, mock_journal_crud, lifecycle)\n\n\n# -- Helpers ---------------------------------------------------------------\n\n\ndef _seed_handler(\n    store: MemoryWorkflowStore,\n    handler_id: str = \"handler-1\",\n    workflow_name: str = \"test_wf\",\n    run_id: str = \"run-1\",\n    idle_since: datetime | None = None,\n) -> PersistentHandler:\n    handler = PersistentHandler(\n        handler_id=handler_id,\n        workflow_name=workflow_name,\n        status=\"running\",\n        run_id=run_id,\n        idle_since=idle_since,\n        started_at=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n    store.handlers[handler_id] = handler\n    return handler\n\n\n# -- Tests -----------------------------------------------------------------\n\n\n@pytest.mark.asyncio()\nasync def test_idle_event_schedules_release_without_stamping_idle_since(\n    decorator: DBOSIdleReleaseDecorator, store: MemoryWorkflowStore\n) -> None:\n    \"\"\"WorkflowIdleEvent should schedule deferred release but NOT stamp idle_since.\"\"\"\n    inner_adapter = StubInternalAdapter(run_id=\"run-1\")\n    _seed_handler(store, run_id=\"run-1\")\n\n    adapter = _DBOSIdleReleaseInternalRunAdapter(inner_adapter, decorator, store)\n    event = WorkflowIdleEvent()\n\n    await adapter.write_to_event_stream(event)\n\n    # Should NOT have stamped idle_since\n    handler = store.handlers[\"handler-1\"]\n    assert handler.idle_since is None\n\n    # Should have forwarded the event\n    assert inner_adapter.written_events == [event]\n\n    # Should have a deferred release task tracked by run_id\n    assert \"run-1\" in decorator._deferred_release_tasks\n\n\n@pytest.mark.asyncio()\nasync def test_release_uses_lifecycle_lock(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    lifecycle: FakeLifecycleLock,\n) -> None:\n    \"\"\"Release should use begin_release and send TickIdleRelease.\"\"\"\n    await lifecycle.create(\"run-1\")\n\n    # Pre-populate an external adapter so get_external_adapter returns it\n    stub_runtime._external_adapters[\"run-1\"] = StubExternalAdapter(run_id=\"run-1\")\n\n    await decorator._release_idle_handler(\"run-1\")\n\n    state = lifecycle.get_state(\"run-1\")\n    assert state is not None\n    assert state[0] == RunLifecycleState.releasing\n    ext = stub_runtime._external_adapters[\"run-1\"]\n    assert len(ext.sent_events) == 1\n    assert isinstance(ext.sent_events[0], TickIdleRelease)\n\n\n@pytest.mark.asyncio()\nasync def test_release_skips_if_begin_release_fails(\n    decorator: DBOSIdleReleaseDecorator,\n    stub_runtime: StubRuntime,\n    lifecycle: FakeLifecycleLock,\n) -> None:\n    \"\"\"Release should skip if begin_release returns False.\"\"\"\n    # No row exists, so begin_release returns False already\n\n    await decorator._release_idle_handler(\"run-1\")\n\n    # No external adapter should have been accessed / no events sent\n    assert \"run-1\" not in stub_runtime._external_adapters\n\n\n@pytest.mark.asyncio()\nasync def test_await_and_mark_released_sets_idle_since(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    lifecycle: FakeLifecycleLock,\n) -> None:\n    \"\"\"After workflow completes, idle_since should be set and state marked released.\"\"\"\n    _seed_handler(store, run_id=\"run-1\")\n\n    await lifecycle.create(\"run-1\")\n    await lifecycle.begin_release(\"run-1\")\n\n    external = StubExternalAdapter(run_id=\"run-1\")\n\n    await decorator._await_and_mark_released(\"run-1\", external)\n\n    state = lifecycle.get_state(\"run-1\")\n    assert state is not None\n    assert state[0] == RunLifecycleState.released\n    handler = store.handlers[\"handler-1\"]\n    assert handler.idle_since is not None\n\n\n@pytest.mark.asyncio()\nasync def test_send_event_resumes_when_released(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    lifecycle: FakeLifecycleLock,\n    mock_dbos: AsyncMock,\n) -> None:\n    \"\"\"send_event should resume workflow if lifecycle state is released.\"\"\"\n    lifecycle._states[\"run-1\"] = (\n        RunLifecycleState.released,\n        datetime.now(timezone.utc),\n    )\n\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    adapter = DBOSIdleReleaseExternalRunAdapter(decorator, \"run-1\")\n\n    tick = TickIdleCheck()\n    await adapter.send_event(tick)\n\n    # Should have started a new workflow run with the same run_id\n    assert len(stub_runtime.run_workflow_calls) == 1\n    call = stub_runtime.run_workflow_calls[0]\n    assert call[\"run_id\"] == \"run-1\"\n\n    # Handler should have been updated in store\n    handler = store.handlers[\"handler-1\"]\n    assert handler.idle_since is None\n    assert handler.status == \"running\"\n    assert handler.run_id == \"run-1\"\n\n    # DBOS workflow record must be deleted before re-creating with same run_id\n    mock_dbos.delete_workflow_async.assert_called_once_with(\"run-1\")\n\n    # Journal should have been purged\n    mock_journal_crud.delete.assert_called_once_with(\"run-1\")\n\n\n@pytest.mark.asyncio()\nasync def test_send_event_sends_normally_when_active(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    lifecycle: FakeLifecycleLock,\n) -> None:\n    \"\"\"send_event should send normally if lifecycle returns None (active).\"\"\"\n    await lifecycle.create(\"run-1\")\n\n    # Pre-populate an external adapter\n    stub_runtime._external_adapters[\"run-1\"] = StubExternalAdapter(run_id=\"run-1\")\n\n    adapter = DBOSIdleReleaseExternalRunAdapter(decorator, \"run-1\")\n    await adapter.send_event(TickIdleCheck())\n\n    assert len(stub_runtime.run_workflow_calls) == 0\n    ext = stub_runtime._external_adapters[\"run-1\"]\n    assert len(ext.sent_events) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_send_event_waits_on_releasing_then_resumes(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    lifecycle: FakeLifecycleLock,\n    mock_dbos: AsyncMock,\n) -> None:\n    \"\"\"send_event should poll when releasing, then resume when released.\"\"\"\n    lifecycle._states[\"run-1\"] = (\n        RunLifecycleState.releasing,\n        datetime.now(timezone.utc),\n    )\n\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    adapter = DBOSIdleReleaseExternalRunAdapter(decorator, \"run-1\")\n\n    async def _complete_release_during_sleep(delay: float) -> None:\n        await lifecycle.complete_release(\"run-1\")\n\n    with patch(\"asyncio.sleep\", side_effect=_complete_release_during_sleep):\n        await adapter.send_event(TickIdleCheck())\n\n    state = lifecycle.get_state(\"run-1\")\n    assert state is not None\n    assert state[0] == RunLifecycleState.active\n    assert len(stub_runtime.run_workflow_calls) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_send_event_force_resumes_on_crash_timeout(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    lifecycle: FakeLifecycleLock,\n    mock_dbos: AsyncMock,\n) -> None:\n    \"\"\"send_event should force resume if releasing state is stale.\"\"\"\n    lifecycle._states[\"run-1\"] = (\n        RunLifecycleState.releasing,\n        datetime.now(timezone.utc),\n    )\n    stale_time = datetime.now(timezone.utc) - timedelta(\n        seconds=CRASH_TIMEOUT_SECONDS + 10\n    )\n    lifecycle.set_updated_at(\"run-1\", stale_time)\n\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    adapter = DBOSIdleReleaseExternalRunAdapter(decorator, \"run-1\")\n\n    await adapter.send_event(TickIdleCheck())\n\n    state = lifecycle.get_state(\"run-1\")\n    assert state is not None\n    assert state[0] == RunLifecycleState.active\n    assert len(stub_runtime.run_workflow_calls) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_wait_receive_cancels_pending_release_timer(\n    decorator: DBOSIdleReleaseDecorator, store: MemoryWorkflowStore\n) -> None:\n    \"\"\"wait_receive returning a tick should cancel the pending release timer.\"\"\"\n    decorator._schedule_deferred_release(\"run-1\")\n    assert \"run-1\" in decorator._deferred_release_tasks\n    task = decorator._deferred_release_tasks[\"run-1\"]\n\n    inner_adapter = StubInternalAdapter(run_id=\"run-1\")\n\n    # Override wait_receive to return a tick\n    tick_result = WaitResultTick(tick=TickIdleCheck())\n\n    async def _return_tick(timeout_seconds: float | None = None) -> WaitResult:\n        return tick_result\n\n    inner_adapter.wait_receive = _return_tick  # type: ignore[assignment]\n\n    adapter = _DBOSIdleReleaseInternalRunAdapter(inner_adapter, decorator, store)\n\n    result = await adapter.wait_receive(timeout_seconds=5.0)\n\n    assert result is tick_result\n    await asyncio.sleep(0)\n    assert task.cancelled()\n    assert \"run-1\" not in decorator._deferred_release_tasks\n\n\n@pytest.mark.asyncio()\nasync def test_no_destroy_or_shutdown_cancellation(\n    decorator: DBOSIdleReleaseDecorator,\n) -> None:\n    \"\"\"Decorator should not have destroy/shutdown methods that cancel workflows.\"\"\"\n    assert not hasattr(decorator, \"stop_task\")\n    assert not hasattr(decorator, \"_on_server_stop\")\n    assert not hasattr(decorator, \"_close_internal_adapter\")\n    assert not hasattr(decorator, \"_active_run_ids\")\n    assert not hasattr(decorator, \"_internal_adapters\")\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_carries_over_serialized_state(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    mock_dbos: AsyncMock,\n) -> None:\n    \"\"\"_do_resume should pass serialized_state from the old state store.\"\"\"\n    workflow = StatefulWorkflow()\n    decorator.track_workflow(workflow)\n\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    # Seed the state store with actual state data\n    state_store = InMemoryStateStore(MyState(counter=42))\n    store.state_stores[\"run-1\"] = state_store\n\n    await decorator._do_resume(\"run-1\")\n\n    assert len(stub_runtime.run_workflow_calls) == 1\n    call = stub_runtime.run_workflow_calls[0]\n    serialized_state = call[\"serialized_state\"]\n    assert serialized_state is not None\n    assert \"counter\" in serialized_state[\"state_data\"]\n\n\n@pytest.mark.asyncio()\nasync def test_send_event_polls_when_releasing_then_completes(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    lifecycle: FakeLifecycleLock,\n    mock_dbos: AsyncMock,\n) -> None:\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    # Start in releasing state\n    lifecycle._states[\"run-1\"] = (\n        RunLifecycleState.releasing,\n        datetime.now(timezone.utc),\n    )\n\n    sleep_count = 0\n\n    async def _on_sleep(delay: float) -> None:\n        nonlocal sleep_count\n        sleep_count += 1\n        # After first sleep, complete the release so next try_begin_resume returns released\n        await lifecycle.complete_release(\"run-1\")\n\n    adapter = DBOSIdleReleaseExternalRunAdapter(decorator, \"run-1\")\n    with patch(\"asyncio.sleep\", side_effect=_on_sleep):\n        await adapter.send_event(TickIdleCheck())\n\n    assert sleep_count >= 1\n    assert len(stub_runtime.run_workflow_calls) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_send_event_crash_timeout_boundary_does_not_force_resume(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    lifecycle: FakeLifecycleLock,\n    mock_dbos: AsyncMock,\n) -> None:\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    # Set exactly at boundary (not greater)\n    lifecycle._states[\"run-1\"] = (\n        RunLifecycleState.releasing,\n        datetime.now(timezone.utc),\n    )\n    boundary_time = datetime.now(timezone.utc) - timedelta(\n        seconds=CRASH_TIMEOUT_SECONDS\n    )\n    lifecycle.set_updated_at(\"run-1\", boundary_time)\n\n    async def _complete_release_during_sleep(delay: float) -> None:\n        await lifecycle.complete_release(\"run-1\")\n\n    adapter = DBOSIdleReleaseExternalRunAdapter(decorator, \"run-1\")\n    with patch(\"asyncio.sleep\", side_effect=_complete_release_during_sleep):\n        await adapter.send_event(TickIdleCheck())\n\n    # Should have resumed normally (not force), state should be active\n    state = lifecycle.get_state(\"run-1\")\n    assert state is not None\n    assert state[0] == RunLifecycleState.active\n    assert len(stub_runtime.run_workflow_calls) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_continues_when_old_dbos_workflow_gone(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    mock_dbos: AsyncMock,\n) -> None:\n    mock_dbos.retrieve_workflow_async.side_effect = Exception(\"workflow not found\")\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    await decorator._do_resume(\"run-1\")\n\n    assert len(stub_runtime.run_workflow_calls) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_continues_when_state_carryover_fails(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    mock_dbos: AsyncMock,\n) -> None:\n    workflow = StatefulWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    with patch.object(store, \"create_state_store\", side_effect=Exception(\"boom\")):\n        await decorator._do_resume(\"run-1\")\n\n    assert len(stub_runtime.run_workflow_calls) == 1\n    call = stub_runtime.run_workflow_calls[0]\n    assert call[\"serialized_state\"] is None\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_continues_when_journal_delete_fails(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    mock_dbos: AsyncMock,\n) -> None:\n    mock_journal_crud.delete.side_effect = Exception(\"db error\")\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    await decorator._do_resume(\"run-1\")\n\n    assert len(stub_runtime.run_workflow_calls) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_continues_when_dbos_delete_fails(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    mock_dbos: AsyncMock,\n) -> None:\n    mock_dbos.delete_workflow_async.side_effect = Exception(\"delete failed\")\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    await decorator._do_resume(\"run-1\")\n\n    assert len(stub_runtime.run_workflow_calls) == 1\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_raises_when_handler_not_found(\n    decorator: DBOSIdleReleaseDecorator,\n    mock_dbos: AsyncMock,\n) -> None:\n    with pytest.raises(ValueError, match=\"Expected 1 handler\"):\n        await decorator._do_resume(\"run-1\")\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_raises_when_workflow_not_tracked(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    mock_dbos: AsyncMock,\n) -> None:\n    _seed_handler(store, workflow_name=\"unknown_workflow\", run_id=\"run-1\")\n    with pytest.raises(ValueError, match=\"not found\"):\n        await decorator._do_resume(\"run-1\")\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_includes_pending_tick_in_rebuilt_state(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    mock_dbos: AsyncMock,\n) -> None:\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    # Use TickAddEvent with StartEvent — this sets is_running=True in BrokerState,\n    # giving us a concrete observable difference vs. no pending tick.\n    tick = TickAddEvent(event=StartEvent())\n    await decorator._do_resume(\"run-1\", pending_tick=tick)\n\n    assert len(stub_runtime.run_workflow_calls) == 1\n    call = stub_runtime.run_workflow_calls[0]\n    init_state = call[\"init_state\"]\n    assert init_state is not None\n    # Without the pending tick injection, is_running would remain False.\n    # StartEvent via TickAddEvent sets is_running=True in the rebuilt state.\n    assert init_state.is_running is True\n\n\n@pytest.mark.asyncio()\nasync def test_do_resume_replays_persisted_ticks(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    stub_runtime: StubRuntime,\n    mock_journal_crud: AsyncMock,\n    mock_dbos: AsyncMock,\n) -> None:\n    \"\"\"Persisted ticks must be replayed to rebuild BrokerState on resume (Fault 8).\"\"\"\n    workflow = SimpleWorkflow()\n    decorator.track_workflow(workflow)\n    _seed_handler(\n        store,\n        workflow_name=workflow.workflow_name,\n        run_id=\"run-1\",\n        idle_since=datetime(2020, 1, 1, tzinfo=timezone.utc),\n    )\n\n    # Seed a persisted TickAddEvent(StartEvent) — simulates a tick recorded\n    # before the workflow was released.\n    tick = TickAddEvent(event=StartEvent())\n    tick_data = WorkflowTickAdapter.dump_python(tick)\n    await store.append_tick(\"run-1\", tick_data)\n\n    await decorator._do_resume(\"run-1\")\n\n    assert len(stub_runtime.run_workflow_calls) == 1\n    call = stub_runtime.run_workflow_calls[0]\n    init_state = call[\"init_state\"]\n    assert init_state is not None\n    # Without tick replay, is_running would remain False (bare BrokerState).\n    # The persisted TickAddEvent(StartEvent) sets is_running=True.\n    assert init_state.is_running is True\n\n\n@pytest.mark.asyncio()\nasync def test_await_and_mark_released_handles_get_result_failure(\n    decorator: DBOSIdleReleaseDecorator,\n    store: MemoryWorkflowStore,\n    lifecycle: FakeLifecycleLock,\n) -> None:\n    _seed_handler(store, run_id=\"run-1\")\n\n    external = StubExternalAdapter(run_id=\"run-1\")\n    external.get_result = AsyncMock(side_effect=Exception(\"get_result failed\"))  # type: ignore[assignment]\n\n    await decorator._await_and_mark_released(\"run-1\", external)\n\n    # complete_release should not have been called since get_result failed\n    assert \"run-1\" not in lifecycle._states\n\n\n@pytest.mark.asyncio()\nasync def test_deferred_release_fires_after_timeout(\n    stub_runtime: StubRuntime,\n    store: MemoryWorkflowStore,\n    mock_journal_crud: AsyncMock,\n    lifecycle: FakeLifecycleLock,\n) -> None:\n    dec = _make_decorator(\n        stub_runtime, store, mock_journal_crud, lifecycle, idle_timeout=0.01\n    )\n    await lifecycle.create(\"run-1\")\n    stub_runtime._external_adapters[\"run-1\"] = StubExternalAdapter(run_id=\"run-1\")\n\n    dec._schedule_deferred_release(\"run-1\")\n    await asyncio.sleep(0.05)\n\n    state = lifecycle.get_state(\"run-1\")\n    assert state is not None\n    # After deferred release fires, the handler goes through releasing -> released\n    assert state[0] in (RunLifecycleState.releasing, RunLifecycleState.released)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_dbos_idle_release_e2e.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"End-to-end idle release tests over live HTTP with subprocess isolation.\n\nTests the full idle release → purge → resume cycle by starting a real HTTP\nserver (replica_server.py with --idle-timeout) and exercising it via\nWorkflowClient. Validates event stream continuity across the idle/resume\nboundary and handler completion.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport subprocess\nimport sys\nimport time\nfrom pathlib import Path\n\nimport httpx\nimport pytest\nfrom llama_agents.client import WorkflowClient\nfrom tests.fixtures.sample_workflows.hitl import UserInput\nfrom workflows.events import WorkflowIdleEvent\n\nREPLICA_SERVER_PATH = str(Path(__file__).parent / \"fixtures\" / \"replica_server.py\")\nWORKFLOW_PATH = \"tests.fixtures.sample_workflows.hitl:TestWorkflow\"\nIDLE_TIMEOUT = 0.5\nRESTART_EXECUTOR_ID = \"test-replica-restart\"\n\n\ndef _start_idle_server(\n    port: int,\n    db_url: str,\n    idle_timeout: float,\n    executor_id: str | None = None,\n) -> subprocess.Popen[str]:\n    cmd = [\n        sys.executable,\n        REPLICA_SERVER_PATH,\n        \"--workflow\",\n        WORKFLOW_PATH,\n        \"--db-url\",\n        db_url,\n        \"--port\",\n        str(port),\n        \"--idle-timeout\",\n        str(idle_timeout),\n    ]\n    if executor_id is not None:\n        cmd.extend([\"--executor-id\", executor_id])\n    return subprocess.Popen(\n        cmd,\n        text=True,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        start_new_session=True,\n    )\n\n\ndef _stop_server(proc: subprocess.Popen[str]) -> None:\n    if proc.poll() is not None:\n        return\n    proc.terminate()\n    try:\n        proc.wait(timeout=5)\n    except subprocess.TimeoutExpired:\n        proc.kill()\n        proc.wait(timeout=5)\n\n\ndef _wait_for_server(\n    proc: subprocess.Popen[str], port: int, timeout: float = 30.0\n) -> None:\n    deadline = time.monotonic() + timeout\n    while time.monotonic() < deadline:\n        if proc.poll() is not None:\n            stdout = proc.stdout.read() if proc.stdout else \"\"\n            raise RuntimeError(\n                f\"Server on port {port} exited with code {proc.returncode}\\n\"\n                f\"output: {stdout}\"\n            )\n        try:\n            resp = httpx.get(f\"http://localhost:{port}/workflows\", timeout=2.0)\n            if resp.status_code == 200:\n                return\n        except httpx.ConnectError:\n            pass\n        time.sleep(0.5)\n    proc.kill()\n    stdout = proc.stdout.read() if proc.stdout else \"\"\n    raise RuntimeError(\n        f\"Server on port {port} did not start in {timeout}s\\noutput: {stdout}\"\n    )\n\n\nasync def _run_idle_release_test(port: int, db_url: str) -> None:\n    \"\"\"Core test logic shared between SQLite and Postgres variants.\"\"\"\n    proc = _start_idle_server(port, db_url, IDLE_TIMEOUT)\n    try:\n        _wait_for_server(proc, port)\n        client = WorkflowClient(base_url=f\"http://localhost:{port}\")\n\n        # 1. Start workflow\n        handler = await client.run_workflow_nowait(\"test\")\n        handler_id = handler.handler_id\n\n        # 2. Stream events until WorkflowIdleEvent\n        stream = client.get_workflow_events(handler_id, include_internal_events=True)\n        async for env in stream:\n            event = env.load_event([WorkflowIdleEvent])\n            if isinstance(event, WorkflowIdleEvent):\n                break\n\n        last_seq = stream.last_sequence\n\n        # 3. Wait for idle timeout to elapse (release happens in background)\n        await asyncio.sleep(IDLE_TIMEOUT + 1.5)\n\n        # 4. Handler should still be \"running\" (released but not completed)\n        h = await client.get_handler(handler_id)\n        assert h.status == \"running\", f\"Expected 'running', got '{h.status}'\"\n\n        # 5. Send event to trigger resume\n        send_resp = await client.send_event(handler_id, UserInput(response=\"world\"))\n        assert send_resp.status == \"sent\"\n\n        # 6. Stream events after resume, expect StopEvent\n        got_stop = False\n        async for env in client.get_workflow_events(\n            handler_id, after_sequence=last_seq\n        ):\n            if env.type == \"StopEvent\":\n                got_stop = True\n                break\n        assert got_stop, \"Should see StopEvent after resume\"\n\n        # 7. Poll for handler completion\n        for _ in range(40):\n            h = await client.get_handler(handler_id)\n            if h.status == \"completed\":\n                break\n            await asyncio.sleep(0.25)\n        assert h.status == \"completed\", f\"Expected 'completed', got '{h.status}'\"\n        assert h.result is not None\n        assert h.result.value.get(\"result\", {}).get(\"response\") == \"world\"\n    finally:\n        _stop_server(proc)\n\n\nasync def _wait_for_handler_status(\n    client: WorkflowClient, handler_id: str, status: str, timeout: float = 30.0\n) -> None:\n    deadline = time.monotonic() + timeout\n    while time.monotonic() < deadline:\n        handler = await client.get_handler(handler_id)\n        if handler.status == status:\n            return\n        await asyncio.sleep(0.25)\n    raise AssertionError(f\"Handler {handler_id} did not reach status {status}\")\n\n\n@pytest.mark.timeout(45)\nasync def test_idle_release_e2e_sqlite(tmp_path: Path) -> None:\n    \"\"\"Full idle release cycle over HTTP with SQLite backend.\"\"\"\n    db_path = tmp_path / \"idle_e2e.sqlite3\"\n    db_url = f\"sqlite+pysqlite:///{db_path}?check_same_thread=false\"\n    await _run_idle_release_test(18010, db_url)\n\n\n@pytest.mark.docker\n@pytest.mark.timeout(45)\nasync def test_idle_release_e2e_postgres(postgres_dsn: str) -> None:\n    \"\"\"Full idle release cycle over HTTP with PostgreSQL backend.\"\"\"\n    db_url = postgres_dsn.replace(\"postgresql://\", \"postgresql+psycopg://\", 1)\n    await _run_idle_release_test(18011, db_url)\n\n\n@pytest.mark.timeout(60)\nasync def test_restart_and_recover_http_workflow(tmp_path: Path) -> None:\n    \"\"\"Recovered startup workflow survives a restart and accepts input.\"\"\"\n    port = 18012\n    db_path = tmp_path / \"restart_recovery.sqlite3\"\n    db_url = f\"sqlite+pysqlite:///{db_path}?check_same_thread=false\"\n\n    proc = _start_idle_server(\n        port,\n        db_url,\n        IDLE_TIMEOUT,\n        executor_id=RESTART_EXECUTOR_ID,\n    )\n    client = WorkflowClient(base_url=f\"http://localhost:{port}\")\n    handler_id = \"\"\n    run_id = \"\"\n    try:\n        _wait_for_server(proc, port)\n\n        handler = await client.run_workflow_nowait(\"test\")\n        handler_id = handler.handler_id\n        run_id = handler.run_id or \"\"\n        assert run_id, \"Workflow should have a run_id\"\n\n        stream = client.get_workflow_events(handler_id)\n        async for env in stream:\n            if env.type == \"AskInputEvent\":\n                break\n\n        last_sequence = stream.last_sequence\n        await stream.aclose()\n\n        _stop_server(proc)\n\n        proc = _start_idle_server(\n            port,\n            db_url,\n            IDLE_TIMEOUT,\n            executor_id=RESTART_EXECUTOR_ID,\n        )\n        _wait_for_server(proc, port)\n\n        recovered = await client.get_handler(handler_id)\n        assert recovered.status == \"running\"\n        assert recovered.run_id == run_id\n\n        send_resp = await client.send_event(handler_id, UserInput(response=\"world\"))\n        assert send_resp.status == \"sent\"\n\n        await _wait_for_handler_status(client, handler_id, \"completed\")\n        completed = await client.get_handler(handler_id)\n        assert completed.result is not None\n        assert completed.result.value.get(\"result\", {}).get(\"response\") == \"world\"\n\n        resumed_stream = client.get_workflow_events(\n            handler_id, after_sequence=last_sequence\n        )\n        saw_stop = False\n        async for env in resumed_stream:\n            if env.type == \"StopEvent\":\n                saw_stop = True\n                break\n        await resumed_stream.aclose()\n        assert saw_stop, \"Recovered workflow should emit StopEvent after restart\"\n    finally:\n        _stop_server(proc)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_dbos_runtime.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"DBOS-specific runtime tests for adapter behavior.\n\nThese tests focus on the internal mechanics of the DBOS adapter,\nparticularly around run_id matching and state store availability.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom contextlib import suppress\nfrom types import SimpleNamespace\nfrom typing import Any, Generator, cast\nfrom unittest.mock import patch\n\nimport asyncpg\nimport pytest\nfrom dbos import DBOS, DBOSConfig\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.dbos.journal.crud import SqliteJournalCrud\nfrom llama_agents.dbos.journal.task_journal import TaskJournal\nfrom llama_agents.dbos.runtime import InternalDBOSAdapter\nfrom llama_agents.server._pool import PoolProvider\nfrom llama_agents.server._store.postgres_state_store import PostgresStateStore\nfrom pydantic import Field\nfrom sqlalchemy.engine import Engine\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.runtime.types.named_task import WorkerTask\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.workflow import Workflow\n\n\ndef _fake_sqlite_engine() -> Engine:\n    return cast(\n        Engine,\n        SimpleNamespace(\n            dialect=SimpleNamespace(name=\"sqlite\"),\n            url=SimpleNamespace(database=\":memory:\"),\n        ),\n    )\n\n\ndef test_postgres_adapter_uses_resolved_pool_for_sync_state_store() -> None:\n    pool = cast(asyncpg.Pool, object())\n\n    async def factory() -> asyncpg.Pool:\n        raise AssertionError(\"resolved pool should be used synchronously\")\n\n    adapter = InternalDBOSAdapter(\n        run_id=\"run-1\",\n        engine=cast(\n            Engine, SimpleNamespace(dialect=SimpleNamespace(name=\"postgresql\"))\n        ),\n        pool=PoolProvider.borrowed(factory),\n        resolved_pool=pool,\n    )\n\n    state_store = adapter.get_state_store()\n\n    assert isinstance(state_store, PostgresStateStore)\n    assert state_store._pool is pool\n\n\n@pytest.fixture(scope=\"module\")\ndef dbos_config(tmp_path_factory: pytest.TempPathFactory) -> DBOSConfig:\n    \"\"\"Create DBOS config with a fresh SQLite database.\"\"\"\n    db_file = tmp_path_factory.mktemp(\"dbos\") / \"dbos_debug_test.sqlite3\"\n    system_db_url = f\"sqlite+pysqlite:///{db_file}?check_same_thread=false\"\n    return {\n        \"name\": \"workflows-dbos-debug\",\n        \"system_database_url\": system_db_url,\n        \"run_admin_server\": False,\n    }  # type: ignore[return-value]\n\n\n@pytest.fixture(scope=\"module\")\ndef dbos_runtime(\n    dbos_config: DBOSConfig,\n) -> Generator[DBOSRuntime, None, None]:\n    \"\"\"Module-scoped DBOS runtime with fast polling for tests.\"\"\"\n    DBOS(config=dbos_config)\n    runtime = DBOSRuntime(polling_interval_sec=0.01)\n    try:\n        yield runtime\n    finally:\n        runtime.destroy_sync()\n\n\nclass DebugEvent(Event):\n    captured_run_id: str = Field(default=\"\")\n    captured_dbos_workflow_id: str = Field(default=\"\")\n    state_store_available: bool = Field(default=False)\n\n\nclass RunIdCaptureWorkflow(Workflow):\n    \"\"\"Workflow that captures run_id info for debugging.\"\"\"\n\n    @step\n    async def capture_ids(self, ev: StartEvent) -> StopEvent:\n        dbos_workflow_id = DBOS.workflow_id or \"None\"\n        return StopEvent(result={\"dbos_workflow_id\": dbos_workflow_id})\n\n\nclass StateStoreAccessWorkflow(Workflow):\n    \"\"\"Workflow that attempts to access state store.\"\"\"\n\n    @step\n    async def access_store(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        dbos_workflow_id = DBOS.workflow_id or \"None\"\n\n        try:\n            await ctx.store.set(\"test_key\", \"test_value\")\n            value = await ctx.store.get(\"test_key\")\n            store_works = value == \"test_value\"\n        except Exception as e:\n            return StopEvent(\n                result={\n                    \"dbos_workflow_id\": dbos_workflow_id,\n                    \"store_works\": False,\n                    \"error\": str(e),\n                }\n            )\n\n        return StopEvent(\n            result={\n                \"dbos_workflow_id\": dbos_workflow_id,\n                \"store_works\": store_works,\n            }\n        )\n\n\nclass StateStoreCounterWorkflow(Workflow):\n    \"\"\"Workflow that increments a counter in state store.\"\"\"\n\n    @step\n    async def increment(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        cur = await ctx.store.get(\"counter\", default=0)\n        await ctx.store.set(\"counter\", cur + 1)\n        return StopEvent(result=cur + 1)\n\n\n@pytest.mark.asyncio\nasync def test_dbos_workflow_id_available(dbos_runtime: DBOSRuntime) -> None:\n    \"\"\"Verify DBOS.workflow_id is set inside workflow execution.\"\"\"\n    wf = RunIdCaptureWorkflow(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    r = await WorkflowTestRunner(wf).run()\n    result = r.result\n\n    assert result[\"dbos_workflow_id\"] != \"None\", (\n        \"DBOS.workflow_id should be set inside workflow\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_state_store_access_in_step(dbos_runtime: DBOSRuntime) -> None:\n    \"\"\"Test whether state store is accessible inside a workflow step.\"\"\"\n    wf = StateStoreAccessWorkflow(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    r = await WorkflowTestRunner(wf).run()\n    result = r.result\n\n    assert result[\"store_works\"], (\n        f\"State store should be accessible. Got error: {result.get('error', 'unknown')}\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_internal_adapter_run_id_matches(dbos_runtime: DBOSRuntime) -> None:\n    \"\"\"Verify internal adapter run_id matches DBOS.workflow_id.\"\"\"\n    captured_ids: dict[str, Any] = {}\n\n    class IdTracingWorkflow(Workflow):\n        @step\n        async def trace_ids(self, ev: StartEvent) -> StopEvent:\n            captured_ids[\"dbos_workflow_id\"] = DBOS.workflow_id\n\n            internal_adapter = dbos_runtime.get_internal_adapter(self)\n            captured_ids[\"adapter_run_id\"] = internal_adapter.run_id\n\n            store = internal_adapter.get_state_store()\n            captured_ids[\"state_store_found\"] = store is not None\n\n            return StopEvent(result=\"done\")\n\n    wf = IdTracingWorkflow(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    await WorkflowTestRunner(wf).run()\n\n    assert captured_ids[\"adapter_run_id\"] == captured_ids[\"dbos_workflow_id\"], (\n        f\"Adapter run_id '{captured_ids['adapter_run_id']}' should match \"\n        f\"DBOS.workflow_id '{captured_ids['dbos_workflow_id']}'\"\n    )\n    assert captured_ids[\"state_store_found\"], \"State store should be available\"\n\n\n@pytest.mark.asyncio\nasync def test_external_run_id_vs_internal(dbos_runtime: DBOSRuntime) -> None:\n    \"\"\"Compare external adapter run_id with what's seen internally.\"\"\"\n    internal_run_id: str | None = None\n\n    class CompareWorkflow(Workflow):\n        @step\n        async def capture(self, ev: StartEvent) -> StopEvent:\n            nonlocal internal_run_id\n            internal_run_id = DBOS.workflow_id\n            return StopEvent(result=\"done\")\n\n    wf = CompareWorkflow(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    handler = wf.run()\n    external_run_id = handler.run_id\n\n    await handler\n\n    assert external_run_id == internal_run_id, (\n        f\"External run_id '{external_run_id}' should match \"\n        f\"internal DBOS.workflow_id '{internal_run_id}'\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_state_store_lazy_creation(dbos_runtime: DBOSRuntime) -> None:\n    \"\"\"Test that state store is lazily created by the internal adapter.\"\"\"\n    store_info: dict[str, Any] = {}\n\n    class LazyStoreWorkflow(Workflow):\n        @step\n        async def check_store(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            internal_adapter = dbos_runtime.get_internal_adapter(self)\n\n            # First call should create the store\n            store1 = internal_adapter.get_state_store()\n            store_info[\"first_store_id\"] = id(store1)\n            store_info[\"first_store_exists\"] = store1 is not None\n\n            # Second call should return the same store\n            store2 = internal_adapter.get_state_store()\n            store_info[\"second_store_id\"] = id(store2)\n            store_info[\"same_store\"] = store1 is store2\n\n            # Store should work\n            await ctx.store.set(\"lazy_key\", \"lazy_value\")\n            value = await ctx.store.get(\"lazy_key\")\n            store_info[\"store_works\"] = value == \"lazy_value\"\n\n            return StopEvent(result=\"done\")\n\n    wf = LazyStoreWorkflow(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    await WorkflowTestRunner(wf).run()\n\n    assert store_info[\"first_store_exists\"], \"Store should be created on first access\"\n    assert store_info[\"same_store\"], \"Same store instance should be returned\"\n    assert store_info[\"store_works\"], \"Store should be functional\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_does_not_create_store(dbos_runtime: DBOSRuntime) -> None:\n    \"\"\"Verify run_workflow doesn't eagerly create a state store.\"\"\"\n    call_log: list[dict[str, Any]] = []\n    original_run_workflow = dbos_runtime.run_workflow\n\n    def patched_run_workflow(*args: Any, **kwargs: Any) -> Any:\n        call_log.append({\"run_id\": kwargs.get(\"run_id\")})\n        return original_run_workflow(*args, **kwargs)\n\n    class SimpleWf(Workflow):\n        @step\n        async def do_it(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = SimpleWf(runtime=dbos_runtime)\n    await dbos_runtime.launch()\n\n    with patch.object(dbos_runtime, \"run_workflow\", patched_run_workflow):\n        handler = wf.run()\n        await handler\n\n    assert len(call_log) == 1, \"run_workflow should be called exactly once\"\n\n\n@pytest.mark.asyncio\nasync def test_replay_wait_for_next_task_timeout_returns_none(\n    journal_db_path: str,\n    sqlite_engine: Engine,\n) -> None:\n    \"\"\"Replay wait timeout should return None and not raise.\"\"\"\n    run_id = \"replay-timeout-run\"\n\n    crud = SqliteJournalCrud(db_path=journal_db_path)\n    journal = TaskJournal(run_id, crud)\n    await journal.load()\n    await journal.record(\"step_a:0\")\n\n    adapter = InternalDBOSAdapter(\n        run_id=run_id, engine=sqlite_engine, db_path=journal_db_path\n    )\n    task = asyncio.create_task(asyncio.sleep(5.0))\n\n    try:\n        result = await adapter.wait_for_next_task(\n            [WorkerTask(\"step_a\", 0, task)],\n            [],\n            timeout=0.01,\n        )\n        assert result.completed is None\n    finally:\n        task.cancel()\n        with suppress(asyncio.CancelledError):\n            await task\n\n\n@pytest.mark.asyncio\nasync def test_async_launch_runs_dbos_launch_on_caller_loop(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Async launch should preserve the active application loop.\"\"\"\n    runtime = DBOSRuntime(run_migrations_on_launch=False)\n    observed: dict[str, asyncio.AbstractEventLoop | None] = {\"loop\": None}\n\n    def fake_launch() -> None:\n        observed[\"loop\"] = asyncio.get_running_loop()\n\n    fake_dbos = SimpleNamespace(launch=fake_launch, destroy=lambda: None)\n    monkeypatch.setattr(\"llama_agents.dbos.runtime.DBOS\", fake_dbos)\n    monkeypatch.setattr(\n        DBOSRuntime,\n        \"_get_sql_engine\",\n        lambda self: _fake_sqlite_engine(),\n    )\n\n    await runtime.launch()\n\n    assert observed[\"loop\"] is asyncio.get_running_loop()\n\n    await runtime.destroy()\n\n\ndef test_launch_sync_offloads_dbos_launch_from_asyncio_run_loop(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Sync launch should not bind DBOS to the temporary asyncio.run loop.\"\"\"\n    runtime = DBOSRuntime(run_migrations_on_launch=False)\n    observed: dict[str, bool] = {\"saw_running_loop\": False}\n\n    def fake_launch() -> None:\n        try:\n            asyncio.get_running_loop()\n        except RuntimeError:\n            observed[\"saw_running_loop\"] = False\n        else:\n            observed[\"saw_running_loop\"] = True\n\n    fake_dbos = SimpleNamespace(launch=fake_launch, destroy=lambda: None)\n    monkeypatch.setattr(\"llama_agents.dbos.runtime.DBOS\", fake_dbos)\n    monkeypatch.setattr(\n        DBOSRuntime,\n        \"_get_sql_engine\",\n        lambda self: _fake_sqlite_engine(),\n    )\n\n    runtime.launch_sync()\n\n    assert not observed[\"saw_running_loop\"]\n\n    runtime.destroy_sync()\n\n\n@pytest.mark.asyncio\nasync def test_launch_sync_raises_in_async_context() -> None:\n    \"\"\"Sync launch should fail loudly when called from an async context.\"\"\"\n    runtime = DBOSRuntime(run_migrations_on_launch=False)\n\n    with pytest.raises(RuntimeError, match=\"use 'await runtime.launch\\\\(\\\\)' instead\"):\n        runtime.launch_sync()\n\n\ndef test_launch_sync_raises_with_executor_lease() -> None:\n    \"\"\"Executor leasing requires async launch because it owns async tasks.\"\"\"\n    runtime = DBOSRuntime(\n        run_migrations_on_launch=False,\n        _experimental_executor_lease={\"pool_size\": 1},\n    )\n\n    with pytest.raises(RuntimeError, match=\"_experimental_executor_lease\"):\n        runtime.launch_sync()\n\n\ndef test_resolve_pool_sizes_explicit_config() -> None:\n    \"\"\"When pool_size is set in config it wins over DBOS sys_db config.\"\"\"\n    runtime = DBOSRuntime(pool_size=7)\n    min_size, max_size = runtime._resolve_pool_sizes()\n    assert min_size == 7\n    assert max_size == 7\n\n\ndef test_resolve_pool_sizes_explicit_min_and_max() -> None:\n    runtime = DBOSRuntime(pool_size=8, pool_min_size=2)\n    min_size, max_size = runtime._resolve_pool_sizes()\n    assert min_size == 2\n    assert max_size == 8\n\n\ndef test_resolve_pool_sizes_min_clamped_to_max() -> None:\n    runtime = DBOSRuntime(pool_size=4, pool_min_size=10)\n    min_size, max_size = runtime._resolve_pool_sizes()\n    assert min_size == 4\n    assert max_size == 4\n\n\ndef test_resolve_pool_sizes_floor_at_two() -> None:\n    \"\"\"pool_size=1 is bumped to 2 so the LISTEN connection doesn't starve queries.\"\"\"\n    runtime = DBOSRuntime(pool_size=1)\n    min_size, max_size = runtime._resolve_pool_sizes()\n    assert max_size == 2\n    assert min_size == 2\n\n\ndef test_resolve_pool_sizes_falls_back_to_dbos_sys_db_pool_size() -> None:\n    \"\"\"Without explicit config, picks up DBOS's configured sys_db pool_size.\"\"\"\n    runtime = DBOSRuntime()\n    fake_dbos = SimpleNamespace(\n        _config={\"sys_db_engine_kwargs\": {\"pool_size\": 17}},\n    )\n    with patch(\"llama_agents.dbos.runtime._get_dbos_instance\", return_value=fake_dbos):\n        min_size, max_size = runtime._resolve_pool_sizes()\n    assert max_size == 17\n    assert min_size == 17\n\n\ndef test_resolve_pool_sizes_falls_back_to_constant_when_dbos_unavailable() -> None:\n    \"\"\"If DBOS isn't constructed, defaults to the library's fallback constant.\"\"\"\n    runtime = DBOSRuntime()\n    with patch(\n        \"llama_agents.dbos.runtime._get_dbos_instance\",\n        side_effect=RuntimeError(\"not constructed\"),\n    ):\n        min_size, max_size = runtime._resolve_pool_sizes()\n    assert max_size == 10\n    assert min_size == 10\n\n\ndef test_register_forwards_max_recovery_attempts() -> None:\n    \"\"\"When set, max_recovery_attempts is forwarded to @DBOS.workflow.\"\"\"\n\n    class _W(Workflow):\n        @step\n        async def go(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n    runtime = DBOSRuntime(max_recovery_attempts=3)\n    captured: dict[str, Any] = {}\n\n    def _capture(**kwargs: Any) -> Any:\n        captured.update(kwargs)\n        return lambda fn: fn\n\n    with patch(\"llama_agents.dbos.runtime.DBOS.workflow\", _capture):\n        runtime.register(_W())\n    assert captured[\"max_recovery_attempts\"] == 3\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_dbos_server_postgres.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"End-to-end DBOS + WorkflowServer + PostgresWorkflowStore integration tests.\n\nThese tests verify:\n1. Event interceptor prevents published events from reaching dbos.streams\n2. Events are stored as clean JSON in our wf_events table\n3. subscribe_events works across the full server chain\n4. Interrupt/resume produces no duplicate events (replay safety)\n\nAll tests require Docker (testcontainers) and use subprocess isolation for\nDBOS global state safety.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nimport pytest\n\nSERVER_RUNNER_PATH = str(Path(__file__).parent / \"fixtures\" / \"server_runner.py\")\n\npytestmark = [pytest.mark.docker]\n\n\ndef run_server_scenario(\n    workflow: str,\n    db_url: str,\n    run_id: str,\n    check_streams: bool = False,\n    check_events: bool = False,\n    interrupt_after: str | None = None,\n    timeout: float = 60.0,\n) -> subprocess.CompletedProcess[str]:\n    \"\"\"Run a workflow scenario through the WorkflowServer + DBOS chain.\"\"\"\n    cmd = [\n        sys.executable,\n        SERVER_RUNNER_PATH,\n        \"--workflow\",\n        workflow,\n        \"--db-url\",\n        db_url,\n        \"--run-id\",\n        run_id,\n    ]\n    if check_streams:\n        cmd.append(\"--check-streams\")\n    if check_events:\n        cmd.append(\"--check-events\")\n    if interrupt_after:\n        cmd.extend([\"--interrupt-after\", interrupt_after])\n    return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)\n\n\ndef assert_no_errors(result: subprocess.CompletedProcess[str]) -> None:\n    \"\"\"Check subprocess result for crashes and errors.\"\"\"\n    if result.returncode != 0:\n        msg = f\"Subprocess exited with code {result.returncode}\\nstdout: {result.stdout}\\nstderr: {result.stderr}\"\n        pytest.fail(msg)\n    # Only fail on tracebacks in stdout — stderr tracebacks during DBOS\n    # shutdown are noisy but harmless when exit code is 0.\n    if \"Traceback (most recent call last)\" in result.stdout:\n        pytest.fail(f\"Exception!\\nstdout: {result.stdout}\\nstderr: {result.stderr}\")\n\n\ndef extract_line(output: str, prefix: str) -> str | None:\n    \"\"\"Extract a line starting with prefix from output.\"\"\"\n    for line in output.splitlines():\n        if line.startswith(prefix):\n            return line[len(prefix) :]\n    return None\n\n\ndef extract_all_lines(output: str, prefix: str) -> list[str]:\n    \"\"\"Extract all lines starting with prefix from output.\"\"\"\n    return [\n        line[len(prefix) :] for line in output.splitlines() if line.startswith(prefix)\n    ]\n\n\ndef test_event_interceptor_no_dbos_streams(postgres_dsn: str) -> None:\n    \"\"\"Run a workflow via the server chain and verify no events in dbos.streams.\"\"\"\n    result = run_server_scenario(\n        workflow=\"tests.fixtures.sample_workflows.chained:ChainedWorkflow\",\n        db_url=postgres_dsn,\n        run_id=\"test-interceptor-001\",\n        check_streams=True,\n        check_events=True,\n    )\n    assert_no_errors(result)\n    assert \"SUCCESS\" in result.stdout\n\n    streams_count = extract_line(result.stdout, \"STREAMS_COUNT:\")\n    assert streams_count is not None, (\n        f\"No STREAMS_COUNT found.\\nstdout: {result.stdout}\"\n    )\n    assert int(streams_count) == 0, (\n        f\"Expected 0 events in dbos.streams, got {streams_count}\"\n    )\n\n\ndef test_events_stored_as_json(postgres_dsn: str) -> None:\n    \"\"\"Verify events are stored in wf_events with valid JSON.\"\"\"\n    result = run_server_scenario(\n        workflow=\"tests.fixtures.sample_workflows.chained:ChainedWorkflow\",\n        db_url=postgres_dsn,\n        run_id=\"test-events-json-001\",\n        check_events=True,\n    )\n    assert_no_errors(result)\n    assert \"SUCCESS\" in result.stdout\n\n    events_count = extract_line(result.stdout, \"EVENTS_COUNT:\")\n    assert events_count is not None, f\"No EVENTS_COUNT.\\nstdout: {result.stdout}\"\n    count = int(events_count)\n    assert count >= 3, f\"Expected at least 3 events, got {count}\"\n\n    event_jsons = extract_all_lines(result.stdout, \"EVENT_JSON:\")\n    for i, event_json in enumerate(event_jsons):\n        try:\n            parsed = json.loads(event_json)\n            assert \"type\" in parsed, f\"Event {i} missing 'type' field\"\n        except json.JSONDecodeError:\n            pytest.fail(f\"Event {i} is not valid JSON: {event_json}\")\n\n\ndef test_subscribe_events_receives_all_events(postgres_dsn: str) -> None:\n    \"\"\"Verify subscribe_events receives all events in order during workflow execution.\"\"\"\n    result = run_server_scenario(\n        workflow=\"tests.fixtures.sample_workflows.chained:ChainedWorkflow\",\n        db_url=postgres_dsn,\n        run_id=\"test-subscribe-001\",\n    )\n    assert_no_errors(result)\n    assert \"SUCCESS\" in result.stdout\n\n    event_names = extract_all_lines(result.stdout, \"EVENT:\")\n    assert len(event_names) >= 3, f\"Expected at least 3 events, got {event_names}\"\n\n    # StopEvent should be the last event\n    assert event_names[-1] == \"StopEvent\", (\n        f\"Last event should be StopEvent, got {event_names[-1]}\"\n    )\n\n\ndef test_no_duplicate_events_after_replay(postgres_dsn: str) -> None:\n    \"\"\"Interrupt a workflow, resume it, and verify no duplicate events.\"\"\"\n    run_id = \"test-replay-dedup-001\"\n\n    # Run 1: interrupt after step_two produces StepTwoEvent\n    result1 = run_server_scenario(\n        workflow=\"tests.fixtures.sample_workflows.chained:ChainedWorkflow\",\n        db_url=postgres_dsn,\n        run_id=run_id,\n        interrupt_after=\"StepTwoEvent\",\n    )\n    assert \"INTERRUPTING\" in result1.stdout, (\n        f\"Should have interrupted.\\nstdout: {result1.stdout}\\nstderr: {result1.stderr}\"\n    )\n\n    # Run 2: resume to completion with same run_id, check events\n    result2 = run_server_scenario(\n        workflow=\"tests.fixtures.sample_workflows.chained:ChainedWorkflow\",\n        db_url=postgres_dsn,\n        run_id=run_id,\n        check_events=True,\n        check_streams=True,\n    )\n    assert_no_errors(result2)\n    assert \"SUCCESS\" in result2.stdout, (\n        f\"Resume should succeed.\\nstdout: {result2.stdout}\\nstderr: {result2.stderr}\"\n    )\n\n    # Verify no events in dbos.streams\n    streams_count = extract_line(result2.stdout, \"STREAMS_COUNT:\")\n    if streams_count is not None:\n        assert int(streams_count) == 0, (\n            f\"Expected 0 events in dbos.streams after replay, got {streams_count}\"\n        )\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_executor_lease.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ExecutorLeaseManager.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import AsyncGenerator\nfrom typing import cast\n\nimport asyncpg\nimport pytest\nfrom llama_agents.dbos.executor_lease import ExecutorLeaseManager\nfrom llama_agents.server._pool import PoolProvider\nfrom llama_agents.server._store.postgres.migrate import run_migrations\n\n\n@pytest.fixture\nasync def lease_dsn(postgres_dsn: str) -> AsyncGenerator[str]:\n    \"\"\"Set up a clean schema with migrations for lease tests.\"\"\"\n    conn = await asyncpg.connect(postgres_dsn)\n    try:\n        await conn.execute(\"DROP SCHEMA IF EXISTS test_lease CASCADE\")\n        await run_migrations(\n            conn,\n            schema=\"test_lease\",\n            sources=[(\"dbos\", \"llama_agents.dbos._store.postgres.migrations\")],\n        )\n    finally:\n        await conn.close()\n    yield postgres_dsn\n\n\ndef make_manager(\n    dsn: str,\n    pool_size: int = 3,\n    heartbeat_interval: float = 0.5,\n    lease_timeout: float = 5.0,\n) -> ExecutorLeaseManager:\n    return ExecutorLeaseManager(\n        pool=PoolProvider.create(dsn, min_size=1, max_size=5),\n        pool_size=pool_size,\n        schema=\"test_lease\",\n        heartbeat_interval=heartbeat_interval,\n        lease_timeout=lease_timeout,\n    )\n\n\ndef test_requires_pool_provider() -> None:\n    provider = PoolProvider.borrowed(\n        lambda: asyncio.sleep(0, result=cast(asyncpg.Pool, object()))\n    )\n\n    mgr = ExecutorLeaseManager(pool=provider, pool_size=1)\n\n    assert mgr._pool_provider is provider\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_acquire_returns_slot_id(lease_dsn: str) -> None:\n    mgr = make_manager(lease_dsn)\n    slot = await mgr.acquire()\n    try:\n        assert slot == \"executor-0\"\n        assert mgr.executor_id == \"executor-0\"\n    finally:\n        await mgr.release()\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_executor_id_raises_before_acquire(lease_dsn: str) -> None:\n    mgr = make_manager(lease_dsn)\n    with pytest.raises(RuntimeError, match=\"Lease not acquired\"):\n        mgr.executor_id\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_acquire_blocks_when_full(lease_dsn: str) -> None:\n    managers = [make_manager(lease_dsn, pool_size=2) for _ in range(2)]\n    slots = []\n    for m in managers:\n        slots.append(await m.acquire())\n\n    assert set(slots) == {\"executor-0\", \"executor-1\"}\n\n    # Third acquire should time out since pool is full\n    blocked = make_manager(lease_dsn, pool_size=2)\n    with pytest.raises(TimeoutError):\n        await blocked.acquire(timeout=0.3)\n\n    for m in managers:\n        await m.release()\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_release_frees_slot(lease_dsn: str) -> None:\n    m1 = make_manager(lease_dsn, pool_size=1)\n    await m1.acquire()\n    await m1.release()\n\n    m2 = make_manager(lease_dsn, pool_size=1)\n    slot = await m2.acquire()\n    assert slot == \"executor-0\"\n    await m2.release()\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_stale_heartbeat_allows_reclaim(lease_dsn: str) -> None:\n    m1 = make_manager(lease_dsn, pool_size=1, lease_timeout=1.0)\n    await m1.acquire()\n\n    # Stop heartbeat and wait for lease to go stale\n    assert m1._heartbeat_task is not None\n    m1._heartbeat_task.cancel()\n    try:\n        await m1._heartbeat_task\n    except asyncio.CancelledError:\n        pass\n    m1._heartbeat_task = None\n\n    # Manually set heartbeat_at to the past\n    assert m1._pool is not None\n    await m1._pool.execute(\n        f\"UPDATE {m1._table} SET heartbeat_at = NOW() - INTERVAL '10 seconds' \"\n        f\"WHERE slot_id = $1\",\n        m1._slot_id,\n    )\n\n    # Another manager should be able to claim the stale slot\n    m2 = make_manager(lease_dsn, pool_size=1, lease_timeout=1.0)\n    slot = await m2.acquire(timeout=2.0)\n    assert slot == \"executor-0\"\n\n    await m2.release()\n    # Clean up m1's pool without trying to release the slot (already taken)\n    if m1._pool is not None:\n        await m1._pool.close()\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_lost_lease_sets_event(lease_dsn: str) -> None:\n    m1 = make_manager(lease_dsn, pool_size=1, heartbeat_interval=0.1)\n    await m1.acquire()\n\n    # Simulate another process stealing the lease by changing the holder\n    assert m1._pool is not None\n    await m1._pool.execute(\n        f\"UPDATE {m1._table} SET holder = 'stolen' WHERE slot_id = $1\",\n        m1._slot_id,\n    )\n\n    # Wait for heartbeat to detect the loss\n    await asyncio.wait_for(m1.lease_lost_event.wait(), timeout=1.0)\n    assert m1.lease_lost_event.is_set()\n\n    # Clean up\n    if m1._pool is not None:\n        await m1._pool.close()\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_concurrent_acquires(lease_dsn: str) -> None:\n    \"\"\"Multiple concurrent acquires don't get the same slot.\"\"\"\n    pool_size = 3\n    managers = [make_manager(lease_dsn, pool_size=pool_size) for _ in range(pool_size)]\n\n    slots = await asyncio.gather(*(m.acquire() for m in managers))\n    assert len(set(slots)) == pool_size\n\n    await asyncio.gather(*(m.release() for m in managers))\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_context_manager_releases_on_exit(lease_dsn: str) -> None:\n    async with make_manager(lease_dsn, pool_size=1) as mgr:\n        assert mgr.executor_id == \"executor-0\"\n\n    # Slot should be free now\n    async with make_manager(lease_dsn, pool_size=1) as mgr2:\n        assert mgr2.executor_id == \"executor-0\"\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_journal_double_restart_hang.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Regression: HITL workflow must survive two interrupt-restart cycles.\"\"\"\n\nfrom __future__ import annotations\n\nimport sqlite3\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nfrom conftest import (\n    assert_no_determinism_errors,  # pyright: ignore[reportAttributeAccessIssue]\n    run_scenario,  # pyright: ignore[reportAttributeAccessIssue]\n)\n\nCOUNTER_WORKFLOW = \"tests.fixtures.sample_workflows.counter:CounterWorkflow\"\nRESPOND_CONFIG = {\n    \"respond\": {\n        \"CounterTickEvent\": {\n            \"event\": \"CounterContinueEvent\",\n            \"fields\": {},\n        }\n    }\n}\n\n\n@pytest.fixture\ndef test_db_path(tmp_path: Path) -> Path:\n    return tmp_path / \"double_restart_test.sqlite3\"\n\n\ndef _get_journal_rows(db_path: Path, run_id: str) -> list[tuple[Any, ...]]:\n    conn = sqlite3.connect(str(db_path))\n    try:\n        return conn.execute(\n            \"SELECT * FROM workflow_journal WHERE run_id=? ORDER BY seq_num\",\n            (run_id,),\n        ).fetchall()\n    finally:\n        conn.close()\n\n\ndef _log_journal(db_path: Path, run_id: str, label: str) -> None:\n    rows = _get_journal_rows(db_path, run_id)\n    pull_count = sum(1 for r in rows if \"__pull__\" in r[3])\n    print(f\"{label}: {len(rows)} journal rows ({pull_count} __pull__)\")\n\n\ndef _run_double_restart(\n    db_path: Path,\n    run_id: str,\n    call_close: bool = False,\n) -> None:\n    db_url = f\"sqlite+pysqlite:///{db_path}?check_same_thread=false\"\n\n    # --- Run 1: start, interrupt at tick 5 ---\n    result1 = run_scenario(\n        workflow=COUNTER_WORKFLOW,\n        db_url=db_url,\n        run_id=run_id,\n        config={\n            **RESPOND_CONFIG,\n            \"interrupt_on\": {\"event\": \"CounterTickEvent\", \"condition\": {\"count\": 5}},\n        },\n        call_close=call_close,\n    )\n    assert \"STEP:start:complete\" in result1.stdout, (\n        f\"Start step should complete.\\nstdout: {result1.stdout}\\nstderr: {result1.stderr}\"\n    )\n    assert \"INTERRUPTING\" in result1.stdout, (\n        f\"Should interrupt at tick 5.\\nstdout: {result1.stdout}\\nstderr: {result1.stderr}\"\n    )\n    if call_close:\n        assert \"CLOSE_CALLED\" in result1.stdout\n    _log_journal(db_path, run_id, \"After run 1\")\n\n    # --- Run 2: resume, interrupt at tick 15 ---\n    result2 = run_scenario(\n        workflow=COUNTER_WORKFLOW,\n        db_url=db_url,\n        run_id=run_id,\n        config={\n            **RESPOND_CONFIG,\n            \"interrupt_on\": {\"event\": \"CounterTickEvent\", \"condition\": {\"count\": 15}},\n        },\n        call_close=call_close,\n    )\n    assert \"INTERRUPTING\" in result2.stdout, (\n        f\"Should interrupt at tick 15.\\nstdout: {result2.stdout}\\nstderr: {result2.stderr}\"\n    )\n    if call_close:\n        assert \"CLOSE_CALLED\" in result2.stdout\n    _log_journal(db_path, run_id, \"After run 2\")\n\n    # --- Run 3: resume, should complete to 40 ---\n    result3 = run_scenario(\n        workflow=COUNTER_WORKFLOW,\n        db_url=db_url,\n        run_id=run_id,\n        config=RESPOND_CONFIG,\n        timeout=15.0,\n    )\n    _log_journal(db_path, run_id, \"After run 3\")\n\n    assert_no_determinism_errors(result3)\n    assert \"SUCCESS\" in result3.stdout, (\n        f\"Should complete successfully.\\n\"\n        f\"stdout: {result3.stdout}\\nstderr: {result3.stderr}\"\n    )\n\n\ndef test_double_restart_counter_workflow(test_db_path: Path) -> None:\n    _run_double_restart(test_db_path, run_id=\"counter-double-restart\")\n\n\ndef test_double_restart_with_close(test_db_path: Path) -> None:\n    \"\"\"Same as above, but calls adapter.close() before each exit.\"\"\"\n    _run_double_restart(test_db_path, run_id=\"close-double-restart\", call_close=True)\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_journal_orphan_determinism.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for orphan purge: orphaned DBOS operations are cleaned up on recovery.\"\"\"\n\nfrom __future__ import annotations\n\nimport sqlite3\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nfrom conftest import (\n    run_scenario,  # pyright: ignore[reportAttributeAccessIssue]\n)\n\nSLOW_FANOUT_WORKFLOW = (\n    \"tests.fixtures.sample_workflows.slow_fan_out_hitl:SlowFanOutWorkflow\"\n)\nRESPOND_CONFIG: dict[str, Any] = {\n    \"respond\": {\n        \"SlowFanOutTickEvent\": {\n            \"event\": \"SlowFanOutContinueEvent\",\n            \"fields\": {},\n        }\n    }\n}\nINTERRUPT_CONFIG: dict[str, Any] = {\n    \"respond\": RESPOND_CONFIG[\"respond\"],\n    \"interrupt_on\": {\"event\": \"SlowFanOutTickEvent\", \"condition\": {\"round\": 1}},\n}\n\n\n@pytest.fixture\ndef test_db_path(tmp_path: Path) -> Path:\n    return tmp_path / \"orphan_determinism_test.sqlite3\"\n\n\ndef _db_url(db_path: Path) -> str:\n    return f\"sqlite+pysqlite:///{db_path}?check_same_thread=false\"\n\n\ndef _query_scalar(db_path: Path, sql: str, params: tuple[Any, ...] = ()) -> Any:\n    \"\"\"Run a single-value query against the test DB, returning None if table missing.\"\"\"\n    if not db_path.exists():\n        return None\n    conn = sqlite3.connect(str(db_path))\n    try:\n        row = conn.execute(sql, params).fetchone()\n        return row[0] if row else None\n    except sqlite3.OperationalError:\n        return None\n    finally:\n        conn.close()\n\n\ndef _get_max_fid(db_path: Path, run_id: str) -> int:\n    result = _query_scalar(\n        db_path,\n        \"SELECT MAX(function_id) FROM operation_outputs WHERE workflow_uuid=?\",\n        (run_id,),\n    )\n    return result or 0\n\n\ndef _inject_orphaned_operation(\n    db_path: Path,\n    run_id: str,\n    function_id: int,\n    function_name: str,\n    output: str = \"null\",\n) -> None:\n    \"\"\"Insert a fake orphaned operation to simulate crash-left rows.\"\"\"\n    conn = sqlite3.connect(str(db_path))\n    try:\n        conn.execute(\n            \"INSERT INTO operation_outputs \"\n            \"(workflow_uuid, function_id, function_name, output, started_at_epoch_ms) \"\n            \"VALUES (?, ?, ?, ?, ?)\",\n            (run_id, function_id, function_name, output, 0),\n        )\n        conn.commit()\n    finally:\n        conn.close()\n\n\ndef _get_function_at_fid(db_path: Path, run_id: str, fid: int) -> str | None:\n    return _query_scalar(\n        db_path,\n        \"SELECT function_name FROM operation_outputs \"\n        \"WHERE workflow_uuid=? AND function_id=?\",\n        (run_id, fid),\n    )\n\n\ndef _run_interrupt_and_get_max_fid(db_path: Path, run_id: str) -> int:\n    \"\"\"Run the workflow, interrupt on round 1, return max_fid.\"\"\"\n    result = run_scenario(\n        workflow=SLOW_FANOUT_WORKFLOW,\n        db_url=_db_url(db_path),\n        run_id=run_id,\n        config=INTERRUPT_CONFIG,\n        timeout=30.0,\n    )\n    assert \"INTERRUPTING\" in result.stdout, (\n        f\"Run should have interrupted.\\nstdout: {result.stdout}\"\n    )\n    return _get_max_fid(db_path, run_id)\n\n\ndef test_injected_orphan_is_purged_on_recovery(test_db_path: Path) -> None:\n    \"\"\"Injected orphaned operations are purged on recovery — recovery succeeds\n    despite mismatched function_names at fids beyond the journal boundary.\"\"\"\n    run_id = \"injected-orphan-test\"\n    orphan_name = \"FAKE_ORPHANED_STEP\"\n\n    max_fid = _run_interrupt_and_get_max_fid(test_db_path, run_id)\n\n    orphan_fid = max_fid + 10\n    _inject_orphaned_operation(\n        test_db_path, run_id, orphan_fid, function_name=orphan_name\n    )\n\n    result = run_scenario(\n        workflow=SLOW_FANOUT_WORKFLOW,\n        db_url=_db_url(test_db_path),\n        run_id=run_id,\n        config=RESPOND_CONFIG,\n        timeout=30.0,\n    )\n\n    combined = result.stdout + result.stderr\n    assert \"SUCCESS\" in result.stdout, (\n        f\"Recovery should succeed after purging orphan.\\n\"\n        f\"stdout: {result.stdout}\\nstderr: {result.stderr}\"\n    )\n    assert \"DBOSUnexpectedStepError\" not in combined, (\n        \"Should NOT get determinism error after purge\"\n    )\n\n    orphan_fn = _get_function_at_fid(test_db_path, run_id, orphan_fid)\n    assert orphan_fn != orphan_name, (\n        f\"Orphaned operation at fid {orphan_fid} should have been purged, \"\n        f\"but found function_name={orphan_fn}\"\n    )\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_lifecycle_lock.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Unit tests for RunLifecycleLock implementations (SQLite + PostgreSQL).\"\"\"\n\nfrom __future__ import annotations\n\nimport sqlite3\nfrom collections.abc import AsyncGenerator\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Protocol\n\nimport asyncpg\nimport pytest\nfrom llama_agents.dbos._store import POSTGRES_MIGRATION_SOURCE\nfrom llama_agents.dbos.journal.lifecycle import (\n    LIFECYCLE_TABLE_NAME,\n    PostgresRunLifecycleLock,\n    RunLifecycleLock,\n    RunLifecycleState,\n    SqliteRunLifecycleLock,\n)\nfrom llama_agents.server._store import (\n    POSTGRES_MIGRATION_SOURCE as SERVER_POSTGRES_MIGRATION_SOURCE,\n)\nfrom llama_agents.server._store.postgres.migrate import run_migrations as pg_migrations\n\n\nclass UpdatedAtSetter(Protocol):\n    async def set_updated_at(self, run_id: str, updated_at: datetime) -> None: ...\n\n\nclass SqliteUpdatedAtSetter:\n    def __init__(self, db_path: str) -> None:\n        self._db_path = db_path\n\n    async def set_updated_at(self, run_id: str, updated_at: datetime) -> None:\n        conn = sqlite3.connect(self._db_path)\n        conn.execute(\n            f\"UPDATE {LIFECYCLE_TABLE_NAME} SET updated_at = ? WHERE run_id = ?\",\n            (updated_at.isoformat(), run_id),\n        )\n        conn.commit()\n        conn.close()\n\n\nclass PostgresUpdatedAtSetter:\n    def __init__(self, pool: asyncpg.Pool) -> None:\n        self._pool = pool\n\n    async def set_updated_at(self, run_id: str, updated_at: datetime) -> None:\n        await self._pool.execute(\n            f\"UPDATE {LIFECYCLE_TABLE_NAME} SET updated_at = $1 WHERE run_id = $2\",\n            updated_at,\n            run_id,\n        )\n\n\nLockFixture = tuple[RunLifecycleLock, UpdatedAtSetter]\n\nsqlite_param = pytest.param(\"sqlite\", id=\"sqlite\")\npostgres_param = pytest.param(\"postgres\", marks=pytest.mark.docker, id=\"postgres\")\n\n\n@pytest.fixture\nasync def lock_fixture(\n    request: pytest.FixtureRequest,\n    journal_db_path: str,\n) -> AsyncGenerator[LockFixture]:\n    backend = request.param\n    if backend == \"sqlite\":\n        yield (\n            SqliteRunLifecycleLock(db_path=journal_db_path),\n            SqliteUpdatedAtSetter(journal_db_path),\n        )\n    else:\n        dsn = request.getfixturevalue(\"postgres_dsn\")\n        conn = await asyncpg.connect(dsn)\n        schema = \"test_lifecycle_lock\"\n        try:\n            await conn.execute(f\"DROP SCHEMA IF EXISTS {schema} CASCADE\")\n            await pg_migrations(\n                conn,\n                schema=schema,\n                sources=[\n                    SERVER_POSTGRES_MIGRATION_SOURCE,\n                    POSTGRES_MIGRATION_SOURCE,\n                ],\n            )\n        finally:\n            await conn.close()\n        pool = await asyncpg.create_pool(dsn, server_settings={\"search_path\": schema})\n        assert pool is not None\n        try:\n            yield (\n                PostgresRunLifecycleLock(pool, schema=schema),\n                PostgresUpdatedAtSetter(pool),\n            )\n        finally:\n            await pool.close()\n\n\nboth = pytest.mark.parametrize(\n    \"lock_fixture\", [sqlite_param, postgres_param], indirect=True\n)\nsqlite_only = pytest.mark.parametrize(\"lock_fixture\", [sqlite_param], indirect=True)\n\n\n@both\n@pytest.mark.asyncio\nasync def test_create_sets_active(lock_fixture: LockFixture) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    assert await lock.try_begin_resume(\"run-1\") is None\n\n\n@both\n@pytest.mark.asyncio\nasync def test_begin_release_active_to_releasing(lock_fixture: LockFixture) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    assert await lock.begin_release(\"run-1\") is True\n    assert await lock.try_begin_resume(\"run-1\") == RunLifecycleState.releasing\n\n\n@both\n@pytest.mark.asyncio\nasync def test_begin_release_not_active_returns_false(\n    lock_fixture: LockFixture,\n) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.begin_release(\"run-1\")\n    assert await lock.begin_release(\"run-1\") is False\n\n\n@both\n@pytest.mark.asyncio\nasync def test_complete_release(lock_fixture: LockFixture) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.begin_release(\"run-1\")\n    await lock.complete_release(\"run-1\")\n    assert await lock.try_begin_resume(\"run-1\") == RunLifecycleState.released\n\n\n@both\n@pytest.mark.asyncio\nasync def test_try_begin_resume_no_row(lock_fixture: LockFixture) -> None:\n    lock, _ = lock_fixture\n    assert await lock.try_begin_resume(\"nonexistent\") is None\n\n\n@both\n@pytest.mark.asyncio\nasync def test_try_begin_resume_active_returns_none(lock_fixture: LockFixture) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    assert await lock.try_begin_resume(\"run-1\") is None\n\n\n@both\n@pytest.mark.asyncio\nasync def test_try_begin_resume_released_transitions_to_active(\n    lock_fixture: LockFixture,\n) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.begin_release(\"run-1\")\n    await lock.complete_release(\"run-1\")\n\n    result = await lock.try_begin_resume(\"run-1\")\n    assert result == RunLifecycleState.released\n    assert await lock.try_begin_resume(\"run-1\") is None\n\n\n@both\n@pytest.mark.asyncio\nasync def test_try_begin_resume_releasing_returns_releasing(\n    lock_fixture: LockFixture,\n) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.begin_release(\"run-1\")\n    assert await lock.try_begin_resume(\"run-1\") == RunLifecycleState.releasing\n\n\n@both\n@pytest.mark.asyncio\nasync def test_try_begin_resume_force_resumes_on_crash_timeout(\n    lock_fixture: LockFixture,\n) -> None:\n    lock, setter = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.begin_release(\"run-1\")\n\n    stale_time = datetime.now(timezone.utc) - timedelta(seconds=200)\n    await setter.set_updated_at(\"run-1\", stale_time)\n\n    result = await lock.try_begin_resume(\"run-1\", crash_timeout_seconds=120.0)\n    assert result == RunLifecycleState.released\n    assert await lock.try_begin_resume(\"run-1\") is None\n\n\n@both\n@pytest.mark.asyncio\nasync def test_try_begin_resume_releasing_no_force_without_timeout(\n    lock_fixture: LockFixture,\n) -> None:\n    lock, setter = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.begin_release(\"run-1\")\n\n    stale_time = datetime.now(timezone.utc) - timedelta(seconds=200)\n    await setter.set_updated_at(\"run-1\", stale_time)\n\n    assert await lock.try_begin_resume(\"run-1\") == RunLifecycleState.releasing\n\n\n@both\n@pytest.mark.asyncio\nasync def test_create_is_idempotent(lock_fixture: LockFixture) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.begin_release(\"run-1\")\n    await lock.complete_release(\"run-1\")\n    await lock.create(\"run-1\")\n    assert await lock.try_begin_resume(\"run-1\") is None\n\n\n@both\n@pytest.mark.asyncio\nasync def test_full_lifecycle(lock_fixture: LockFixture) -> None:\n    \"\"\"Test the full active -> releasing -> released -> active cycle.\"\"\"\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n\n    assert await lock.begin_release(\"run-1\") is True\n    assert await lock.try_begin_resume(\"run-1\") == RunLifecycleState.releasing\n\n    await lock.complete_release(\"run-1\")\n    assert await lock.try_begin_resume(\"run-1\") == RunLifecycleState.released\n    assert await lock.try_begin_resume(\"run-1\") is None\n\n\n@both\n@pytest.mark.asyncio\nasync def test_run_id_isolation(lock_fixture: LockFixture) -> None:\n    lock, _ = lock_fixture\n    await lock.create(\"run-1\")\n    await lock.create(\"run-2\")\n    await lock.begin_release(\"run-1\")\n\n    assert await lock.try_begin_resume(\"run-1\") == RunLifecycleState.releasing\n    assert await lock.try_begin_resume(\"run-2\") is None\n"
  },
  {
    "path": "packages/llama-agents-dbos/tests/test_task_journal.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Unit tests for TaskJournal class.\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\nfrom llama_agents.dbos.journal.crud import SqliteJournalCrud\nfrom llama_agents.dbos.journal.task_journal import TaskJournal\n\n\ndef _make_journal(run_id: str, db_path: str) -> TaskJournal:\n    crud = SqliteJournalCrud(db_path=db_path)\n    return TaskJournal(run_id, crud)\n\n\n@pytest.mark.asyncio\nasync def test_fresh_journal_has_no_entries(journal_db_path: str) -> None:\n    \"\"\"Fresh journal returns None for next_expected_key.\"\"\"\n    journal = _make_journal(\"test-run\", journal_db_path)\n    await journal.load()\n\n    assert journal.next_expected_key() is None\n    assert not journal.is_replaying()\n\n\n@pytest.mark.asyncio\nasync def test_record_adds_entry(journal_db_path: str) -> None:\n    \"\"\"Recording a key adds it to the journal.\"\"\"\n    journal = _make_journal(\"test-run\", journal_db_path)\n    await journal.load()\n\n    await journal.record(\"step_a:0\")\n\n    # Verify by loading a new journal for the same run\n    journal2 = _make_journal(\"test-run\", journal_db_path)\n    await journal2.load()\n    assert journal2.next_expected_key() == \"step_a:0\"\n\n\n@pytest.mark.asyncio\nasync def test_record_multiple_entries(journal_db_path: str) -> None:\n    \"\"\"Multiple records append to journal in order.\"\"\"\n    journal = _make_journal(\"test-run\", journal_db_path)\n    await journal.load()\n\n    await journal.record(\"step_a:0\")\n    await journal.record(\"__pull__:0\")\n    await journal.record(\"step_b:1\")\n\n    # Verify order by loading a new journal\n    journal2 = _make_journal(\"test-run\", journal_db_path)\n    await journal2.load()\n    assert journal2.next_expected_key() == \"step_a:0\"\n    journal2.advance()\n    assert journal2.next_expected_key() == \"__pull__:0\"\n    journal2.advance()\n    assert journal2.next_expected_key() == \"step_b:1\"\n    journal2.advance()\n    assert journal2.next_expected_key() is None\n\n\n@pytest.mark.asyncio\nasync def test_replay_returns_entries_in_order(journal_db_path: str) -> None:\n    \"\"\"Replaying journal returns entries in recorded order.\"\"\"\n    # Set up initial data\n    journal1 = _make_journal(\"replay-run\", journal_db_path)\n    await journal1.load()\n    await journal1.record(\"step_a:0\")\n    await journal1.record(\"step_b:1\")\n    await journal1.record(\"__pull__:2\")\n\n    # Load fresh journal and replay\n    journal = _make_journal(\"replay-run\", journal_db_path)\n    await journal.load()\n\n    assert journal.is_replaying()\n    assert journal.next_expected_key() == \"step_a:0\"\n\n    journal.advance()\n    assert journal.next_expected_key() == \"step_b:1\"\n\n    journal.advance()\n    assert journal.next_expected_key() == \"__pull__:2\"\n\n    journal.advance()\n    assert journal.next_expected_key() is None\n    assert not journal.is_replaying()\n\n\n@pytest.mark.asyncio\nasync def test_load_is_idempotent(journal_db_path: str) -> None:\n    \"\"\"Calling load() multiple times doesn't reset state.\"\"\"\n    # Set up initial data\n    journal1 = _make_journal(\"idempotent-run\", journal_db_path)\n    await journal1.load()\n    await journal1.record(\"step_a:0\")\n\n    # Load and advance\n    journal = _make_journal(\"idempotent-run\", journal_db_path)\n    await journal.load()\n    journal.advance()\n    assert journal.next_expected_key() is None\n\n    # Load again - should not reset\n    await journal.load()\n    assert journal.next_expected_key() is None\n\n\n@pytest.mark.asyncio\nasync def test_none_crud_works_in_memory() -> None:\n    \"\"\"Journal works without crud (in-memory only).\"\"\"\n    journal = TaskJournal(\"memory-run\", crud=None)\n    await journal.load()\n\n    assert journal.next_expected_key() is None\n\n    await journal.record(\"step_a:0\")\n    await journal.record(\"step_b:1\")\n\n    # New journal with same run_id but no crud won't see the entries\n    journal2 = TaskJournal(\"memory-run\", crud=None)\n    await journal2.load()\n    assert journal2.next_expected_key() is None\n\n\n@pytest.mark.asyncio\nasync def test_record_advances_index(journal_db_path: str) -> None:\n    \"\"\"Recording advances the replay index to stay in sync.\"\"\"\n    journal = _make_journal(\"index-run\", journal_db_path)\n    await journal.load()\n\n    # After recording, index should advance\n    await journal.record(\"step_a:0\")\n    # Record another\n    await journal.record(\"step_b:1\")\n\n    # The journal's internal state shows 2 entries recorded\n    assert journal._entries == [\"step_a:0\", \"step_b:1\"]\n    assert journal._replay_index == 2  # We've advanced past both\n\n\n@pytest.mark.asyncio\nasync def test_mixed_replay_and_fresh_execution(journal_db_path: str) -> None:\n    \"\"\"Journal transitions from replay to fresh execution correctly.\"\"\"\n    # Set up initial data\n    journal1 = _make_journal(\"mixed-run\", journal_db_path)\n    await journal1.load()\n    await journal1.record(\"step_a:0\")\n\n    # Load fresh journal\n    journal = _make_journal(\"mixed-run\", journal_db_path)\n    await journal.load()\n\n    # Replay the existing entry\n    assert journal.next_expected_key() == \"step_a:0\"\n    journal.advance()\n\n    # Now fresh execution\n    assert journal.next_expected_key() is None\n    await journal.record(\"step_b:1\")\n\n    # Verify both entries persisted\n    journal2 = _make_journal(\"mixed-run\", journal_db_path)\n    await journal2.load()\n    assert journal2.next_expected_key() == \"step_a:0\"\n    journal2.advance()\n    assert journal2.next_expected_key() == \"step_b:1\"\n\n\n@pytest.mark.asyncio\nasync def test_empty_journal_is_valid(journal_db_path: str) -> None:\n    \"\"\"Empty journal (no entries) is a valid state.\"\"\"\n    journal = _make_journal(\"empty-run\", journal_db_path)\n    await journal.load()\n\n    assert journal.next_expected_key() is None\n    assert not journal.is_replaying()\n\n\n@pytest.mark.asyncio\nasync def test_run_id_isolation(journal_db_path: str) -> None:\n    \"\"\"Journals with different run_ids are isolated.\"\"\"\n    journal1 = _make_journal(\"run-1\", journal_db_path)\n    await journal1.load()\n    await journal1.record(\"step_a:0\")\n\n    journal2 = _make_journal(\"run-2\", journal_db_path)\n    await journal2.load()\n    await journal2.record(\"step_b:0\")\n\n    # Each journal sees only its own entries\n    check1 = _make_journal(\"run-1\", journal_db_path)\n    await check1.load()\n    assert check1.next_expected_key() == \"step_a:0\"\n\n    check2 = _make_journal(\"run-2\", journal_db_path)\n    await check2.load()\n    assert check2.next_expected_key() == \"step_b:0\"\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/README.md",
    "content": "# LlamaIndex Integration Tests\n\nThis package contains integration tests to validate that llama-index-core workflow abstractions work correctly with the workflows package.\n\n## Purpose\n\nThe llama-index-core package uses several workflow features:\n- `ctx.store.get/set` for state management\n- `ctx.wait_for_event` for human-in-the-loop patterns\n- Event streaming via `stream_events()`\n- Context serialization for pause/resume\n\nThese tests ensure updates to the workflows package don't break these integration points.\n\n## Test Organization\n\nTests are organized by **workflow feature**, not agent type. Each test is parameterized to run against both `FunctionAgent` and `ReActAgent`.\n\n| File | Coverage |\n|------|----------|\n| `test_context_store.py` | `ctx.store.get/set`, state persistence, tool access to state |\n| `test_event_streaming.py` | `stream_events()`, event types, tool call events |\n| `test_human_in_the_loop.py` | `wait_for_event`, context serialization, pause/resume |\n| `test_error_handling.py` | Max iterations, early stopping, tool errors |\n\n## Running Tests\n\n```bash\nuv run --directory packages/llama-index-integration-tests pytest\n```\n\n## Note\n\nThis package is not published. It exists only for integration testing during development.\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.9.10,<0.10.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"basedpyright>=1.31.1\",\n  \"psycopg[binary]>=3.2.0\",\n  \"pytest>=8.4.2\",\n  \"pytest-asyncio>=1.0.0\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"sqlalchemy>=2.0.0\",\n  \"testcontainers[postgres]>=4.0.0\",\n  \"ty>=0.0.15\"\n]\n\n[project]\nname = \"llama-agents-integration-tests\"\nversion = \"0.1.0\"\ndescription = \"Integration tests for llama-index-core workflow abstractions against the workflows package\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n  \"llama-index-core>=0.14.13\",\n  \"llama-index-workflows\",\n  \"llama-agents-dbos\",\n  \"llama-agents-server\",\n  \"llama-agents-client\"\n]\n\n[tool.basedpyright]\ntypeCheckingMode = \"standard\"\npythonVersion = \"3.10\"\n\n[tool.coverage.run]\nsource = [\n  \"workflows\",\n  \"llama_agents\"\n]\nomit = [\"**/tests/*\"]\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"module\"\nasyncio_default_test_loop_scope = \"module\"\ntestpaths = [\"tests\"]\n# Skip docker tests by default - run with `pytest -m docker` to include them\naddopts = \"-nauto --timeout=120 -m 'not docker and not llamacloud'\"\nmarkers = [\n  \"docker: marks tests as requiring Docker (testcontainers/PostgreSQL)\",\n  \"llamacloud: marks tests as requiring LlamaCloud API credentials\"\n]\nfilterwarnings = [\n  # Ignore internal testcontainers deprecation warning (fixed in their code, just noisy)\n  \"ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning\"\n]\n\n[tool.uv.sources]\nllama-index-workflows = {workspace = true}\nllama-agents-dbos = {workspace = true}\nllama-agents-server = {workspace = true}\nllama-agents-client = {workspace = true}\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/src/llama_agents_integration_tests/__init__.py",
    "content": "\"\"\"Integration tests for llama-index-core workflow abstractions.\"\"\"\n\n__version__ = \"0.1.0\"\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/src/llama_agents_integration_tests/fake_agent_data.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"Fake Agent Data API backend for testing AgentDataStore.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport uuid\nfrom typing import Any\n\nimport httpx\nimport pytest\nfrom llama_agents.server import AgentDataStore\nfrom llama_agents.server._store.agent_data_client import AgentDataClient\nfrom llama_agents.server._store.agent_data_state_store import AgentDataStateStore\n\n\nclass FakeAgentDataBackend:\n    \"\"\"In-memory backend that simulates the Agent Data API HTTP endpoints.\n\n    Stores items keyed by (deployment_name, collection) and provides\n    search/create/update/delete semantics matching the real API.\n    \"\"\"\n\n    def __init__(self) -> None:\n        # (deployment_name, collection) → list[{id, deployment_name, collection, data, created_at}]\n        self._items: dict[tuple[str, str], list[dict[str, Any]]] = {}\n        # Monotonic counter used to synthesize row-level created_at, matching\n        # how the real backend auto-assigns a created_at column per row.\n        self._create_counter: int = 0\n\n    def _key(self, deployment_name: str, collection: str) -> tuple[str, str]:\n        return (deployment_name, collection)\n\n    def _get_items(self, deployment_name: str, collection: str) -> list[dict[str, Any]]:\n        return self._items.setdefault(self._key(deployment_name, collection), [])\n\n    def search(\n        self,\n        deployment_name: str,\n        collection: str,\n        filters: dict[str, Any] | None = None,\n        page_size: int = 100,\n        order_by: str | None = None,\n    ) -> list[dict[str, Any]]:\n        items = self._get_items(deployment_name, collection)\n        if filters:\n            matched = [item for item in items if self._matches(item[\"data\"], filters)]\n        else:\n            matched = list(items)\n\n        if order_by:\n            parts = order_by.split()\n            field = parts[0]\n            reverse = len(parts) > 1 and parts[1].lower() == \"desc\"\n            # Row-level fields (e.g. created_at) live on the item itself, not\n            # in data. Fall back to data for user fields.\n            matched.sort(\n                key=lambda item: item.get(field, item[\"data\"].get(field, 0)),\n                reverse=reverse,\n            )\n\n        return matched[:page_size]\n\n    @staticmethod\n    def _matches(data: dict[str, Any], filters: dict[str, Any]) -> bool:\n        for field, ops in filters.items():\n            value = data.get(field)\n            for op, expected in ops.items():\n                if op == \"eq\" and value != expected:\n                    return False\n                if op == \"includes\" and value not in expected:\n                    return False\n                if op == \"ne\" and value == expected:\n                    return False\n                if op == \"gt\" and (value is None or value <= expected):\n                    return False\n                if op == \"gte\" and (value is None or value < expected):\n                    return False\n        return True\n\n    def create(\n        self, deployment_name: str, collection: str, data: dict[str, Any]\n    ) -> dict[str, Any]:\n        items = self._get_items(deployment_name, collection)\n        self._create_counter += 1\n        item = {\n            \"id\": str(uuid.uuid4()),\n            \"deployment_name\": deployment_name,\n            \"collection\": collection,\n            \"data\": data,\n            \"created_at\": self._create_counter,\n        }\n        items.append(item)\n        return item\n\n    def update_item(self, item_id: str, data: dict[str, Any]) -> dict[str, Any]:\n        for items_list in self._items.values():\n            for item in items_list:\n                if item[\"id\"] == item_id:\n                    item[\"data\"] = data\n                    return item\n        raise ValueError(f\"Item {item_id} not found\")\n\n    def delete_item(self, item_id: str) -> None:\n        for items_list in self._items.values():\n            for i, item in enumerate(items_list):\n                if item[\"id\"] == item_id:\n                    items_list.pop(i)\n                    return\n        raise ValueError(f\"Item {item_id} not found\")\n\n    def delete_many(\n        self,\n        deployment_name: str,\n        collection: str,\n        filters: dict[str, Any],\n    ) -> int:\n        items = self._get_items(deployment_name, collection)\n        matched = [item for item in items if self._matches(item[\"data\"], filters)]\n        for item in matched:\n            items.remove(item)\n        return len(matched)\n\n    def handle_request(self, request: httpx.Request) -> httpx.Response:\n        \"\"\"Route an httpx.Request to the appropriate handler.\"\"\"\n        path = request.url.path\n        method = request.method\n\n        if method == \"POST\" and path == \"/api/v1/beta/agent-data/:search\":\n            body = json.loads(request.content)\n            items = self.search(\n                body[\"deployment_name\"],\n                body[\"collection\"],\n                body.get(\"filter\"),\n                body.get(\"page_size\", 100),\n                body.get(\"order_by\"),\n            )\n            return httpx.Response(200, json={\"items\": items})\n\n        if method == \"POST\" and path == \"/api/v1/beta/agent-data/:delete\":\n            body = json.loads(request.content)\n            count = self.delete_many(\n                body[\"deployment_name\"],\n                body[\"collection\"],\n                body.get(\"filter\", {}),\n            )\n            return httpx.Response(200, json={\"deleted_count\": count})\n\n        if method == \"POST\" and path == \"/api/v1/beta/agent-data\":\n            body = json.loads(request.content)\n            item = self.create(\n                body[\"deployment_name\"], body[\"collection\"], body[\"data\"]\n            )\n            return httpx.Response(200, json=item)\n\n        if method == \"PUT\" and path.startswith(\"/api/v1/beta/agent-data/\"):\n            item_id = path.split(\"/\")[-1]\n            body = json.loads(request.content)\n            item = self.update_item(item_id, body[\"data\"])\n            return httpx.Response(200, json=item)\n\n        if method == \"DELETE\" and path.startswith(\"/api/v1/beta/agent-data/\"):\n            item_id = path.split(\"/\")[-1]\n            self.delete_item(item_id)\n            return httpx.Response(200, json={})\n\n        return httpx.Response(404, json={\"error\": \"not found\"})\n\n\ndef _patch_client(\n    client: AgentDataClient,\n    backend: FakeAgentDataBackend,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Patch an AgentDataClient's http_client to use the fake backend.\n\n    Creates a single mock-transport client and makes http_client() return it,\n    matching the shared-client pattern in AgentDataClient.\n    \"\"\"\n    mock_http = httpx.AsyncClient(\n        base_url=client._base_url,\n        headers=client._headers(),\n        params={\"project_id\": client._project_id},\n        transport=httpx.MockTransport(backend.handle_request),\n    )\n    monkeypatch.setattr(client, \"_shared_client\", mock_http)\n\n\ndef create_agent_data_store(\n    backend: FakeAgentDataBackend,\n    monkeypatch: pytest.MonkeyPatch,\n) -> AgentDataStore:\n    \"\"\"Create an AgentDataStore with httpx patched to use the fake backend.\"\"\"\n    store = AgentDataStore(\n        base_url=\"https://fake-api.example.com\",\n        api_key=\"test-key\",\n        project_id=\"test-project\",\n        deployment_name=\"test-deploy\",\n        collection=\"handlers\",\n    )\n    _patch_client(store._client, backend, monkeypatch)\n    return store\n\n\ndef create_agent_data_state_store(\n    backend: FakeAgentDataBackend,\n    monkeypatch: pytest.MonkeyPatch,\n    run_id: str,\n    state_type: type[Any] | None = None,\n) -> AgentDataStateStore[Any]:\n    \"\"\"Create an AgentDataStateStore with httpx patched to use the fake backend.\"\"\"\n    client = AgentDataClient(\n        base_url=\"https://fake-api.example.com\",\n        api_key=\"test-key\",\n        project_id=\"test-project\",\n        deployment_name=\"test-deploy\",\n    )\n    _patch_client(client, backend, monkeypatch)\n    store = AgentDataStateStore(\n        client=client,\n        run_id=run_id,\n        state_type=state_type,\n    )\n    return store\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/src/llama_agents_integration_tests/helpers.py",
    "content": "\"\"\"Test helpers for llama-index workflow integration tests.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Callable\n\nfrom llama_index.core.base.llms.types import (\n    ChatMessage,\n    MessageRole,\n    TextBlock,\n    ToolCallBlock,\n)\n\n\ndef response_generator_from_list(responses: list[ChatMessage]) -> Callable:\n    \"\"\"Create a response generator that cycles through a list of responses.\"\"\"\n    index = 0\n\n    def generator(messages: list[ChatMessage]) -> ChatMessage:\n        nonlocal index\n        if not responses:\n            return ChatMessage(role=MessageRole.ASSISTANT, content=None)\n        msg = responses[index]\n        index = (index + 1) % len(responses)\n        return msg\n\n    return generator\n\n\ndef make_tool_call_response(\n    tool_name: str,\n    tool_kwargs: dict | None = None,\n    content: str = \"\",\n    tool_id: str = \"call_1\",\n) -> ChatMessage:\n    \"\"\"Create a ChatMessage with a tool call.\"\"\"\n    return ChatMessage(\n        role=MessageRole.ASSISTANT,\n        content=[\n            TextBlock(text=content),\n            ToolCallBlock(\n                tool_call_id=tool_id, tool_name=tool_name, tool_kwargs=tool_kwargs or {}\n            ),\n        ],\n    )\n\n\ndef make_text_response(content: str) -> ChatMessage:\n    \"\"\"Create a simple text response.\"\"\"\n    return ChatMessage(role=MessageRole.ASSISTANT, content=content)\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/src/llama_agents_integration_tests/postgres.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Reusable PostgreSQL testcontainers utilities for integration tests.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\n\nfrom testcontainers.postgres import PostgresContainer\n\n\n@contextmanager\ndef postgres_container(\n    image: str = \"postgres:16\",\n) -> Generator[PostgresContainer, None, None]:\n    \"\"\"Start a disposable Postgres container. Yields the container object.\n\n    Use ``get_connection_url()`` on the result to get a connection string.\n    The ``driver=None`` argument ensures the raw ``postgresql://`` scheme\n    is used (no psycopg2/psycopg suffix).\n    \"\"\"\n    with PostgresContainer(image, driver=None) as pg:\n        yield pg\n\n\ndef get_asyncpg_dsn(container: PostgresContainer) -> str:\n    \"\"\"Return a plain ``postgresql://`` DSN suitable for asyncpg.\"\"\"\n    url = container.get_connection_url()\n    # testcontainers may return psycopg2-style URLs\n    return url.replace(\"postgresql+psycopg2://\", \"postgresql://\")\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/src/llama_agents_integration_tests/server_test_utils.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"Shared test utilities for integration tests that run live HTTP servers.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport socket\nimport time\nfrom contextlib import asynccontextmanager\nfrom typing import AsyncGenerator, Awaitable, Callable, Literal, TypeVar\n\nimport httpx\nimport uvicorn\nfrom llama_agents.client.client import WorkflowClient\nfrom llama_agents.server import WorkflowServer\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\n\nT = TypeVar(\"T\")\n\n\nasync def wait_for_passing(\n    func: Callable[[], Awaitable[T]],\n    max_duration: float = 5.0,\n    interval: float = 0.05,\n) -> T:\n    \"\"\"Retry an async callable until it stops raising, or time runs out.\"\"\"\n    start_time = time.monotonic()\n    last_exception: Exception | None = None\n    while time.monotonic() - start_time < max_duration:\n        remaining_duration = max_duration - (time.monotonic() - start_time)\n        try:\n            return await asyncio.wait_for(func(), timeout=remaining_duration)\n        except Exception as e:\n            last_exception = e\n            await asyncio.sleep(interval)\n    if last_exception:\n        raise last_exception\n    raise TimeoutError(f\"Timed out after {max_duration}s\")\n\n\nasync def wait_for_requested_external_event_stream(\n    client: WorkflowClient,\n    handler_id: str,\n    *,\n    label: str,\n    max_duration: float = 5.0,\n) -> int | Literal[\"now\"]:\n    async def wait_for_prompt() -> int | Literal[\"now\"]:\n        stream = client.get_workflow_events(handler_id)\n        try:\n            async for ev in stream:\n                event = ev.load_event([RequestedExternalEvent])\n                if isinstance(event, RequestedExternalEvent):\n                    return stream.last_sequence\n        finally:\n            await stream.aclose()\n\n        raise AssertionError(\n            f\"{label}: event stream ended before RequestedExternalEvent \"\n            f\"for handler {handler_id}\"\n        )\n\n    try:\n        return await asyncio.wait_for(wait_for_prompt(), timeout=max_duration)\n    except TimeoutError as exc:\n        raise AssertionError(\n            f\"{label}: timed out waiting for RequestedExternalEvent \"\n            f\"for handler {handler_id}\"\n        ) from exc\n\n\n# -- Shared workflow definitions --\n\n\nclass SimpleTestWorkflow(Workflow):\n    @step\n    async def process(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        message = await ctx.store.get(\"test_param\", None)\n        if message is None:\n            message = getattr(ev, \"message\", \"default\")\n        return StopEvent(result=f\"processed: {message}\")\n\n\nclass StreamEvent(Event):\n    message: str\n    sequence: int\n\n\nclass StreamingWorkflow(Workflow):\n    @step\n    async def stream_data(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        count = getattr(ev, \"count\", 3)\n        for i in range(count):\n            ctx.write_event_to_stream(StreamEvent(message=f\"event_{i}\", sequence=i))\n            await asyncio.sleep(0.01)\n        return StopEvent(result=f\"completed_{count}_events\")\n\n\nclass RequestedExternalEvent(InputRequiredEvent):\n    message: str\n\n\nclass ExternalEvent(HumanResponseEvent):\n    response: str\n\n\nclass InteractiveWorkflow(Workflow):\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> RequestedExternalEvent:\n        return RequestedExternalEvent(message=\"ping\")\n\n    @step\n    async def end(self, ctx: Context, ev: ExternalEvent) -> StopEvent:\n        if ev.response == \"error\":\n            raise RuntimeError(\"Error response received\")\n        return StopEvent(result=f\"received: {ev.response}\")\n\n\n# -- Live server utility --\n\n\n@asynccontextmanager\nasync def live_server(\n    server_factory: Callable[[], WorkflowServer],\n) -> AsyncGenerator[tuple[str, WorkflowServer], None]:\n    \"\"\"Start a live HTTP server for testing with atomic port acquisition.\"\"\"\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    try:\n        sock.bind((\"127.0.0.1\", 0))\n        sock.listen(128)\n        port = sock.getsockname()[1]\n\n        server = server_factory()\n        await server.start()\n\n        config = uvicorn.Config(\n            server.app,\n            host=\"127.0.0.1\",\n            port=port,\n            log_level=\"error\",\n            loop=\"asyncio\",\n        )\n        uv_server = uvicorn.Server(config)\n\n        task = asyncio.create_task(uv_server.serve(sockets=[sock]))\n\n        base_url = f\"http://127.0.0.1:{port}\"\n        async with httpx.AsyncClient(base_url=base_url, timeout=1.0) as client:\n            for _ in range(50):\n                try:\n                    resp = await client.get(\"/health\")\n                    if resp.status_code == 200:\n                        break\n                except Exception:\n                    pass\n                await asyncio.sleep(0.01)\n            else:\n                uv_server.should_exit = True\n                await task\n                raise RuntimeError(\"Live server did not start in time\")\n\n        try:\n            yield base_url, server\n        finally:\n            uv_server.should_exit = True\n            try:\n                await task\n            finally:\n                await server.stop()\n    finally:\n        try:\n            sock.close()\n        except Exception:\n            pass\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/conftest.py",
    "content": "\"\"\"Shared fixtures for llama-index workflow integration tests.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any, Callable, Generator, Protocol\n\nimport pytest\nfrom llama_agents_integration_tests.helpers import (\n    make_text_response,\n    response_generator_from_list,\n)\nfrom llama_agents_integration_tests.postgres import (\n    postgres_container as _postgres_container,\n)\nfrom llama_index.core.agent.workflow import (\n    AgentWorkflow,\n    FunctionAgent,\n    ReActAgent,\n)\nfrom llama_index.core.base.llms.types import ChatMessage\nfrom llama_index.core.llms.mock import MockFunctionCallingLLM\nfrom llama_index.core.tools import BaseTool\nfrom sqlalchemy.engine import Engine\nfrom testcontainers.postgres import PostgresContainer\n\n\nclass WorkflowFactory(Protocol):\n    \"\"\"Protocol for workflow factory fixtures.\"\"\"\n\n    def __call__(\n        self,\n        name: str = ...,\n        tools: list[BaseTool | Callable[..., Any]] | None = ...,\n        responses: list[ChatMessage] | None = ...,\n        initial_state: dict[str, Any] | None = ...,\n        **kwargs: Any,\n    ) -> AgentWorkflow: ...\n\n\nclass SimpleWorkflowFactory(Protocol):\n    \"\"\"Protocol for simple workflow factory fixtures.\"\"\"\n\n    def __call__(\n        self,\n        name: str = ...,\n        responses: list[ChatMessage] | None = ...,\n        **kwargs: Any,\n    ) -> AgentWorkflow: ...\n\n\n@pytest.fixture\ndef create_workflow() -> WorkflowFactory:\n    \"\"\"Factory fixture to create AgentWorkflow with FunctionAgent.\n\n    FunctionAgent is used because it supports function calling via\n    tool_calls in additional_kwargs, which our mock responses use.\n    \"\"\"\n\n    def _create(\n        name: str = \"test_agent\",\n        tools: list[BaseTool | Callable[..., Any]] | None = None,\n        responses: list[ChatMessage] | None = None,\n        initial_state: dict[str, Any] | None = None,\n        **kwargs: Any,\n    ) -> AgentWorkflow:\n        if responses is None:\n            responses = [make_text_response(\"Done\")]\n\n        llm = MockFunctionCallingLLM(\n            response_generator=response_generator_from_list(responses)\n        )\n\n        agent = FunctionAgent(\n            name=name,\n            description=f\"Test {name}\",\n            tools=tools or [],\n            llm=llm,\n        )\n\n        return AgentWorkflow(\n            agents=[agent],\n            root_agent=name,\n            initial_state=initial_state,\n            **kwargs,\n        )\n\n    return _create  # type: ignore[return-value]\n\n\n@pytest.fixture(params=[\"function\", \"react\"])\ndef agent_type(request: pytest.FixtureRequest) -> str:\n    \"\"\"Parameterized fixture for agent type.\n\n    Use this only for tests that don't involve tool invocations,\n    since ReActAgent uses text parsing instead of function calling.\n    \"\"\"\n    return request.param  # type: ignore[return-value]\n\n\n@pytest.fixture\ndef create_simple_workflow(agent_type: str) -> SimpleWorkflowFactory:\n    \"\"\"Factory for simple workflows (no tools) parameterized by agent type.\n\n    Use this for tests that want to verify behavior across both\n    FunctionAgent and ReActAgent without tool invocations.\n    \"\"\"\n\n    def _create(\n        name: str = \"test_agent\",\n        responses: list[ChatMessage] | None = None,\n        **kwargs: Any,\n    ) -> AgentWorkflow:\n        if responses is None:\n            responses = [make_text_response(\"Done\")]\n\n        llm = MockFunctionCallingLLM(\n            response_generator=response_generator_from_list(responses)\n        )\n\n        if agent_type == \"function\":\n            agent = FunctionAgent(\n                name=name,\n                description=f\"Test {name}\",\n                llm=llm,\n            )\n        else:\n            agent = ReActAgent(\n                name=name,\n                description=f\"Test {name}\",\n                llm=llm,\n            )\n\n        return AgentWorkflow(\n            agents=[agent],\n            root_agent=name,\n            **kwargs,\n        )\n\n    return _create  # type: ignore[return-value]\n\n\n# -- Docker/PostgreSQL Fixtures --\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_container() -> Generator[PostgresContainer, None, None]:\n    \"\"\"Module-scoped PostgreSQL container for integration tests.\n\n    Requires Docker to be running. Used by tests marked with @pytest.mark.docker.\n    \"\"\"\n    with _postgres_container() as pg:\n        yield pg\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_engine(\n    postgres_container: PostgresContainer,\n) -> Generator[Engine, None, None]:\n    \"\"\"Module-scoped PostgreSQL engine for integration tests.\"\"\"\n    from sqlalchemy import create_engine\n\n    # Get connection URL and convert to use psycopg (psycopg3) driver\n    connection_url = postgres_container.get_connection_url()\n    # Replace postgresql:// or postgresql+psycopg2:// with postgresql+psycopg://\n    if \"postgresql+psycopg2://\" in connection_url:\n        connection_url = connection_url.replace(\n            \"postgresql+psycopg2://\", \"postgresql+psycopg://\"\n        )\n    elif connection_url.startswith(\"postgresql://\"):\n        connection_url = connection_url.replace(\n            \"postgresql://\", \"postgresql+psycopg://\", 1\n        )\n    engine = create_engine(connection_url)\n    yield engine\n    engine.dispose()\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_agent_data_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for AgentDataStore against both a fake backend and the real LlamaCloud API.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport uuid\nfrom datetime import datetime, timezone\nfrom typing import AsyncGenerator, Callable\n\nimport pytest\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom llama_agents.server import HandlerQuery, PersistentHandler\nfrom llama_agents.server._store.abstract_workflow_store import Status\nfrom llama_agents.server._store.agent_data_state_store import AgentDataStateStore\nfrom llama_agents.server._store.agent_data_store import AgentDataStore\nfrom llama_agents_integration_tests.fake_agent_data import (\n    FakeAgentDataBackend,\n    create_agent_data_store,\n)\nfrom workflows.context.state_store import DictState\nfrom workflows.events import Event\n\n_API_KEY = os.environ.get(\"LLAMA_CLOUD_API_KEY\", \"\")\n_BASE_URL = os.environ.get(\"LLAMA_CLOUD_BASE_URL\", \"https://api.cloud.llamaindex.ai\")\n_PROJECT_ID = os.environ.get(\"LLAMA_DEPLOY_PROJECT_ID\", \"\")\n\n\ndef _unique_collection() -> str:\n    return f\"test_{uuid.uuid4().hex[:12]}\"\n\n\ndef _make_store(collection: str | None = None) -> AgentDataStore:\n    return AgentDataStore(\n        base_url=_BASE_URL,\n        api_key=_API_KEY,\n        project_id=_PROJECT_ID,\n        deployment_name=\"_public\",\n        collection=collection or _unique_collection(),\n    )\n\n\nasync def _cleanup_store(store: AgentDataStore) -> None:\n    \"\"\"Delete all items from the store's handler, event, and tick collections.\"\"\"\n    for collection in [\n        store._collection,\n        store._events_collection,\n        store._ticks_collection,\n        f\"{store._collection}_state\",\n    ]:\n        items = await store._client.search(collection, page_size=1000)\n        for item in items:\n            await store._client.delete_item(item[\"id\"])\n\n\ndef _handler(\n    handler_id: str = \"h1\",\n    workflow_name: str = \"wf\",\n    status: Status = \"running\",\n    run_id: str | None = None,\n    idle_since: datetime | None = None,\n) -> PersistentHandler:\n    return PersistentHandler(\n        handler_id=handler_id,\n        workflow_name=workflow_name,\n        status=status,\n        run_id=run_id,\n        idle_since=idle_since,\n    )\n\n\ndef _event_envelope(event: Event) -> EventEnvelopeWithMetadata:\n    return EventEnvelopeWithMetadata.from_event(event)\n\n\n@pytest.fixture(\n    params=[\n        \"fake\",\n        pytest.param(\"real\", marks=pytest.mark.llamacloud),\n    ]\n)\nasync def store(\n    request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch\n) -> AsyncGenerator[AgentDataStore, None]:\n    if request.param == \"fake\":\n        s = create_agent_data_store(FakeAgentDataBackend(), monkeypatch)\n    else:\n        s = _make_store()\n    yield s\n    if request.param == \"real\":\n        await _cleanup_store(s)\n\n\n@pytest.fixture(\n    params=[\n        \"fake\",\n        pytest.param(\"real\", marks=pytest.mark.llamacloud),\n    ]\n)\ndef store_factory(\n    request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch\n) -> Callable[[], AgentDataStore]:\n    if request.param == \"fake\":\n        backend = FakeAgentDataBackend()\n\n        def make_fake() -> AgentDataStore:\n            return create_agent_data_store(backend, monkeypatch)\n\n        return make_fake\n    else:\n        collection = _unique_collection()\n\n        def make_real() -> AgentDataStore:\n            return _make_store(collection)\n\n        return make_real\n\n\n# ---------------------------------------------------------------------------\n# Tests: idle_since filter (the gt \"\" workaround)\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_idle_filter(store: AgentDataStore) -> None:\n    now = datetime.now(timezone.utc)\n\n    await store.update(_handler(\"idle-h\", idle_since=now, run_id=\"r1\"))\n    await store.update(_handler(\"active-h\", idle_since=None, run_id=\"r2\"))\n\n    idle = await store.query(HandlerQuery(is_idle=True))\n    idle_ids = {h.handler_id for h in idle}\n    assert \"idle-h\" in idle_ids\n    assert \"active-h\" not in idle_ids\n\n    active = await store.query(HandlerQuery(is_idle=False))\n    active_ids = {h.handler_id for h in active}\n    assert \"active-h\" in active_ids\n    assert \"idle-h\" not in active_ids\n\n\n@pytest.mark.asyncio\nasync def test_idle_filter_after_clearing_idle_since(store: AgentDataStore) -> None:\n    \"\"\"Handler transitions from idle to non-idle correctly in queries.\"\"\"\n    now = datetime.now(timezone.utc)\n\n    await store.update(_handler(\"h1\", idle_since=now, run_id=\"r1\"))\n\n    idle = await store.query(HandlerQuery(is_idle=True))\n    assert any(h.handler_id == \"h1\" for h in idle)\n\n    # Clear idle_since (simulate wake-up)\n    await store.update(_handler(\"h1\", idle_since=None, run_id=\"r1\"))\n\n    idle = await store.query(HandlerQuery(is_idle=True))\n    assert not any(h.handler_id == \"h1\" for h in idle)\n\n    active = await store.query(HandlerQuery(is_idle=False))\n    assert any(h.handler_id == \"h1\" for h in active)\n\n\n# ---------------------------------------------------------------------------\n# Tests: handler CRUD round-trip\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_handler_create_query_update_delete(store: AgentDataStore) -> None:\n    \"\"\"Full handler lifecycle.\"\"\"\n    h = _handler(\"crud-h\", run_id=\"r1\", status=\"running\")\n    await store.update(h)\n\n    results = await store.query(HandlerQuery(handler_id_in=[\"crud-h\"]))\n    assert len(results) == 1\n    assert results[0].status == \"running\"\n\n    h.status = \"completed\"\n    await store.update(h)\n    results = await store.query(HandlerQuery(handler_id_in=[\"crud-h\"]))\n    assert len(results) == 1\n    assert results[0].status == \"completed\"\n\n    deleted = await store.delete(HandlerQuery(handler_id_in=[\"crud-h\"]))\n    assert deleted == 1\n    results = await store.query(HandlerQuery(handler_id_in=[\"crud-h\"]))\n    assert len(results) == 0\n\n\n# ---------------------------------------------------------------------------\n# Tests: event journal\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_event_append_and_query_ordering(store: AgentDataStore) -> None:\n    \"\"\"Events are returned in sequence order.\"\"\"\n    run_id = f\"run-{uuid.uuid4().hex[:8]}\"\n\n    class Msg(Event):\n        text: str\n\n    for i in range(5):\n        await store.append_event(run_id, _event_envelope(Msg(text=f\"msg-{i}\")))\n\n    events = await store.query_events(run_id)\n    assert len(events) == 5\n    sequences = [e.sequence for e in events]\n    assert sequences == sorted(sequences)\n\n\n@pytest.mark.asyncio\nasync def test_event_query_after_sequence(store: AgentDataStore) -> None:\n    \"\"\"after_sequence filter works correctly.\"\"\"\n    run_id = f\"run-{uuid.uuid4().hex[:8]}\"\n\n    class Ping(Event):\n        n: int\n\n    for i in range(5):\n        await store.append_event(run_id, _event_envelope(Ping(n=i)))\n\n    events = await store.query_events(run_id, after_sequence=2)\n    assert len(events) == 2\n    assert events[0].sequence == 3\n    assert events[1].sequence == 4\n\n\n# ---------------------------------------------------------------------------\n# Tests: tick journal\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_tick_append_and_ordering(store: AgentDataStore) -> None:\n    \"\"\"Ticks round-trip and come back in sequence order.\"\"\"\n    run_id = f\"run-{uuid.uuid4().hex[:8]}\"\n\n    for i in range(3):\n        await store.append_tick(run_id, {\"step\": i, \"data\": f\"tick-{i}\"})\n\n    ticks = await store.get_ticks(run_id)\n    assert len(ticks) == 3\n    assert [t.sequence for t in ticks] == [0, 1, 2]\n    assert ticks[0].tick_data[\"step\"] == 0\n    assert ticks[2].tick_data[\"data\"] == \"tick-2\"\n\n\n# ---------------------------------------------------------------------------\n# Tests: sequence continuity across store instances\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_event_sequence_new_store_instance_no_collision(\n    store_factory: Callable[[], AgentDataStore],\n) -> None:\n    \"\"\"A new AgentDataStore instance seeds sequence counters from existing data.\n\n    The second store instance queries the max existing sequence before appending,\n    so sequences are unique across instances.\n    \"\"\"\n    run_id = f\"run-{uuid.uuid4().hex[:8]}\"\n\n    class Note(Event):\n        text: str\n\n    store1 = store_factory()\n    try:\n        await store1.append_event(run_id, _event_envelope(Note(text=\"from-store1-0\")))\n        await store1.append_event(run_id, _event_envelope(Note(text=\"from-store1-1\")))\n\n        # Flush store1's buffer so events are persisted before store2 reads\n        await store1.query_events(run_id)\n\n        store2 = store_factory()\n        await store2.append_event(run_id, _event_envelope(Note(text=\"from-store2-0\")))\n\n        events = await store2.query_events(run_id)\n        sequences = [e.sequence for e in events]\n        assert len(sequences) == 3\n        assert len(set(sequences)) == 3, (\n            f\"Expected all unique sequences, got {sequences}\"\n        )\n        assert sorted(sequences) == [0, 1, 2]\n    finally:\n        await _cleanup_store(store1)\n\n\n# ---------------------------------------------------------------------------\n# Tests: state store round-trip\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_state_store_set_get_round_trip(store: AgentDataStore) -> None:\n    \"\"\"State store persists and loads state.\"\"\"\n    run_id = f\"run-{uuid.uuid4().hex[:8]}\"\n    state_store = store.create_state_store(run_id)\n\n    state = await state_store.get_state()\n    assert isinstance(state, DictState)\n\n    await state_store.set(path=\"count\", value=42)\n    await state_store.set(path=\"name\", value=\"test\")\n\n    assert await state_store.get(\"count\") == 42\n    assert await state_store.get(\"name\") == \"test\"\n\n\n@pytest.mark.asyncio\nasync def test_state_store_new_instance_reads_persisted(store: AgentDataStore) -> None:\n    \"\"\"A new AgentDataStateStore with the same collection reads persisted state.\"\"\"\n    run_id = f\"run-{uuid.uuid4().hex[:8]}\"\n    state_store = store.create_state_store(run_id)\n\n    await state_store.set(path=\"key\", value=\"value\")\n\n    # Create a fresh instance pointing at the same collection\n    restored = AgentDataStateStore(\n        client=store._client,\n        run_id=run_id,\n        collection=f\"{store._collection}_state\",\n    )\n    val = await restored.get(\"key\")\n    assert val == \"value\"\n\n\n@pytest.mark.asyncio\nasync def test_state_store_edit_state_context_manager(store: AgentDataStore) -> None:\n    \"\"\"edit_state atomically loads, mutates, and saves.\"\"\"\n    run_id = f\"run-{uuid.uuid4().hex[:8]}\"\n    state_store = store.create_state_store(run_id)\n\n    await state_store.set(path=\"x\", value=1)\n    assert await state_store.get(\"x\") == 1\n\n    async with state_store.edit_state() as state:\n        current = state[\"x\"]\n        state[\"x\"] = current + 10\n\n    assert await state_store.get(\"x\") == 11\n\n\n# ---------------------------------------------------------------------------\n# Tests: compound queries\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_query_by_status_and_workflow_name(store: AgentDataStore) -> None:\n    \"\"\"Multi-field queries work correctly.\"\"\"\n    await store.update(\n        _handler(\"h1\", workflow_name=\"wf-a\", status=\"running\", run_id=\"r1\")\n    )\n    await store.update(\n        _handler(\"h2\", workflow_name=\"wf-a\", status=\"completed\", run_id=\"r2\")\n    )\n    await store.update(\n        _handler(\"h3\", workflow_name=\"wf-b\", status=\"running\", run_id=\"r3\")\n    )\n\n    results = await store.query(\n        HandlerQuery(\n            workflow_name_in=[\"wf-a\"],\n            status_in=[\"running\"],\n        )\n    )\n    assert len(results) == 1\n    assert results[0].handler_id == \"h1\"\n\n\n@pytest.mark.asyncio\nasync def test_query_by_multiple_run_ids(store: AgentDataStore) -> None:\n    \"\"\"includes filter works for multi-value queries.\"\"\"\n    await store.update(_handler(\"h1\", run_id=\"r1\"))\n    await store.update(_handler(\"h2\", run_id=\"r2\"))\n    await store.update(_handler(\"h3\", run_id=\"r3\"))\n\n    results = await store.query(HandlerQuery(run_id_in=[\"r1\", \"r3\"]))\n    ids = {h.handler_id for h in results}\n    assert ids == {\"h1\", \"h3\"}\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_context_store.py",
    "content": "\"\"\"Tests for workflow Context store operations.\n\nThese tests verify that ctx.store.get/set work correctly when used\nby llama-index agents, including state persistence across steps and\naccess from within tool functions.\n\"\"\"\n\nfrom conftest import WorkflowFactory\nfrom llama_agents_integration_tests.helpers import (\n    make_text_response,\n    make_tool_call_response,\n)\nfrom workflows import Context\n\n\nasync def test_initial_state_accessible_in_tool(\n    create_workflow: WorkflowFactory,\n) -> None:\n    \"\"\"Test that initial_state is accessible via ctx.store in tools.\"\"\"\n    received_value = None\n\n    async def check_state(ctx: Context) -> str:\n        nonlocal received_value\n        state = await ctx.store.get(\"state\")\n        received_value = state.get(\"initial_key\")\n        return f\"Got: {received_value}\"\n\n    workflow = create_workflow(\n        tools=[check_state],\n        responses=[\n            make_tool_call_response(\"check_state\"),\n            make_text_response(\"Done\"),\n        ],\n        initial_state={\"initial_key\": \"initial_value\"},\n    )\n\n    handler = workflow.run(user_msg=\"Check the state\")\n    async for _ in handler.stream_events():\n        pass\n    await handler\n\n    assert received_value == \"initial_value\"\n\n\nasync def test_state_modification_persists(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that state modifications in tools persist across calls.\"\"\"\n    call_count = 0\n    final_counter_value = None\n\n    async def increment_counter(ctx: Context) -> str:\n        nonlocal call_count, final_counter_value\n        call_count += 1\n        state = await ctx.store.get(\"state\")\n        state[\"counter\"] = state.get(\"counter\", 0) + 1\n        await ctx.store.set(\"state\", state)\n        final_counter_value = state[\"counter\"]\n        return f\"Counter: {state['counter']}\"\n\n    workflow = create_workflow(\n        tools=[increment_counter],\n        responses=[\n            make_tool_call_response(\"increment_counter\"),\n            make_tool_call_response(\"increment_counter\"),\n            make_text_response(\"Done\"),\n        ],\n        initial_state={\"counter\": 0},\n    )\n\n    handler = workflow.run(user_msg=\"Increment twice\")\n    async for _ in handler.stream_events():\n        pass\n    await handler\n\n    # Verify tool was called twice\n    assert call_count == 2\n\n    # Verify final state via the last captured value\n    assert final_counter_value == 2\n\n\nasync def test_state_survives_handler_access(\n    create_workflow: WorkflowFactory,\n) -> None:\n    \"\"\"Test that state can be read from handler.ctx after workflow completes.\"\"\"\n    captured_result = None\n\n    async def set_result(ctx: Context) -> str:\n        nonlocal captured_result\n        state = await ctx.store.get(\"state\")\n        state[\"result\"] = \"computation_complete\"\n        await ctx.store.set(\"state\", state)\n        captured_result = state[\"result\"]\n        return \"Done\"\n\n    workflow = create_workflow(\n        tools=[set_result],\n        responses=[\n            make_tool_call_response(\"set_result\"),\n            make_text_response(\"Finished\"),\n        ],\n        initial_state={},\n    )\n\n    handler = workflow.run(user_msg=\"Compute something\")\n    async for _ in handler.stream_events():\n        pass\n    await handler\n\n    # Verify state was set correctly via captured value\n    assert captured_result == \"computation_complete\"\n\n\nasync def test_complex_state_types(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that complex state types (nested dicts, lists) work correctly.\"\"\"\n    captured_state: dict | None = None\n\n    async def modify_complex_state(ctx: Context) -> str:\n        nonlocal captured_state\n        state = await ctx.store.get(\"state\")\n        state[\"items\"].append(\"new_item\")\n        state[\"nested\"][\"count\"] += 1\n        await ctx.store.set(\"state\", state)\n        # Capture a copy of the state for verification\n        captured_state = {\n            \"items\": list(state[\"items\"]),\n            \"nested\": dict(state[\"nested\"]),\n        }\n        return \"Modified\"\n\n    workflow = create_workflow(\n        tools=[modify_complex_state],\n        responses=[\n            make_tool_call_response(\"modify_complex_state\"),\n            make_text_response(\"Done\"),\n        ],\n        initial_state={\n            \"items\": [\"initial\"],\n            \"nested\": {\"count\": 0, \"name\": \"test\"},\n        },\n    )\n\n    handler = workflow.run(user_msg=\"Modify state\")\n    async for _ in handler.stream_events():\n        pass\n    await handler\n\n    assert captured_state is not None\n    assert captured_state[\"items\"] == [\"initial\", \"new_item\"]\n    assert captured_state[\"nested\"][\"count\"] == 1\n    assert captured_state[\"nested\"][\"name\"] == \"test\"  # Unchanged\n\n\nasync def test_multiple_tools_share_state(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that multiple different tools can share state.\"\"\"\n    final_state: dict | None = None\n\n    async def tool_a(ctx: Context) -> str:\n        state = await ctx.store.get(\"state\")\n        state[\"from_a\"] = True\n        await ctx.store.set(\"state\", state)\n        return \"A done\"\n\n    async def tool_b(ctx: Context) -> str:\n        nonlocal final_state\n        state = await ctx.store.get(\"state\")\n        state[\"from_b\"] = True\n        # Verify tool_a's change is visible\n        assert state.get(\"from_a\") is True\n        await ctx.store.set(\"state\", state)\n        final_state = dict(state)\n        return \"B done\"\n\n    workflow = create_workflow(\n        tools=[tool_a, tool_b],\n        responses=[\n            make_tool_call_response(\"tool_a\"),\n            make_tool_call_response(\"tool_b\"),\n            make_text_response(\"Done\"),\n        ],\n        initial_state={},\n    )\n\n    handler = workflow.run(user_msg=\"Run both tools\")\n    async for _ in handler.stream_events():\n        pass\n    await handler\n\n    assert final_state is not None\n    assert final_state[\"from_a\"] is True\n    assert final_state[\"from_b\"] is True\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_error_handling.py",
    "content": "\"\"\"Tests for workflow error handling.\n\nThese tests verify that error conditions like max iterations are\nhandled correctly, and that early stopping methods work as expected.\n\"\"\"\n\nimport pytest\nfrom conftest import WorkflowFactory\nfrom llama_agents_integration_tests.helpers import (\n    make_text_response,\n    make_tool_call_response,\n)\nfrom workflows.errors import WorkflowRuntimeError\n\n\nasync def test_max_iterations_raises_error(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that exceeding max_iterations raises WorkflowRuntimeError.\"\"\"\n\n    def infinite_tool() -> str:\n        \"\"\"A tool that will be called repeatedly.\"\"\"\n        return \"called\"\n\n    workflow = create_workflow(\n        tools=[infinite_tool],\n        # Response generator that always calls the tool\n        responses=[make_tool_call_response(\"infinite_tool\")] * 100,\n    )\n\n    with pytest.raises(WorkflowRuntimeError, match=\"Max iterations\"):\n        await workflow.run(user_msg=\"Loop forever\", max_iterations=5)\n\n\nasync def test_early_stopping_generate(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test early_stopping_method='generate' produces a final response instead of error.\"\"\"\n\n    def looping_tool() -> str:\n        return \"still going\"\n\n    workflow = create_workflow(\n        tools=[looping_tool],\n        responses=[\n            make_tool_call_response(\"looping_tool\"),\n            make_tool_call_response(\"looping_tool\"),\n            make_tool_call_response(\"looping_tool\"),\n            make_tool_call_response(\"looping_tool\"),\n            make_tool_call_response(\"looping_tool\"),\n            # After max iterations, LLM should give final response\n            make_text_response(\"Final summary after hitting limit\"),\n        ],\n        early_stopping_method=\"generate\",\n    )\n\n    # Should NOT raise, should return a response\n    handler = workflow.run(user_msg=\"Test\", max_iterations=5)\n    async for _ in handler.stream_events():\n        pass\n\n    result = await handler\n    assert result is not None\n\n\nasync def test_tool_error_captured_in_result(\n    create_workflow: WorkflowFactory,\n) -> None:\n    \"\"\"Test that tool errors are captured rather than crashing the workflow.\"\"\"\n\n    def failing_tool() -> str:\n        raise ValueError(\"Tool failed!\")\n\n    workflow = create_workflow(\n        tools=[failing_tool],\n        responses=[\n            make_tool_call_response(\"failing_tool\"),\n            make_text_response(\"Handled the error\"),\n        ],\n    )\n\n    handler = workflow.run(user_msg=\"Use the failing tool\")\n    async for _ in handler.stream_events():\n        pass\n\n    # Workflow should complete (error is passed back to LLM)\n    result = await handler\n    assert result is not None\n\n\nasync def test_max_iterations_configurable_per_run(\n    create_workflow: WorkflowFactory,\n) -> None:\n    \"\"\"Test that max_iterations can be set per run() call.\"\"\"\n    call_count = 0\n\n    def counting_tool() -> str:\n        nonlocal call_count\n        call_count += 1\n        return f\"Call {call_count}\"\n\n    workflow = create_workflow(\n        tools=[counting_tool],\n        responses=[make_tool_call_response(\"counting_tool\")] * 50\n        + [make_text_response(\"Done\")],\n    )\n\n    # With low max_iterations, should fail\n    with pytest.raises(WorkflowRuntimeError):\n        await workflow.run(user_msg=\"Test\", max_iterations=3)\n\n    # Reset\n    call_count = 0\n\n    # With higher max_iterations, more calls happen before error\n    with pytest.raises(WorkflowRuntimeError):\n        await workflow.run(user_msg=\"Test\", max_iterations=10)\n\n    # Should have made more calls with higher limit\n    assert call_count > 3\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_event_streaming.py",
    "content": "\"\"\"Tests for workflow event streaming.\n\nThese tests verify that event streaming works correctly with llama-index\nagents, including write_event_to_stream, collect_events, and the\nstream_events() API.\n\"\"\"\n\nfrom conftest import SimpleWorkflowFactory, WorkflowFactory\nfrom llama_agents_integration_tests.helpers import (\n    make_text_response,\n    make_tool_call_response,\n)\nfrom llama_index.core.agent.workflow import AgentOutput, ToolCall, ToolCallResult\n\n\nasync def test_stream_events_yields_events(\n    create_simple_workflow: SimpleWorkflowFactory,\n) -> None:\n    \"\"\"Test that stream_events() yields events during workflow execution.\"\"\"\n    workflow = create_simple_workflow(\n        responses=[make_text_response(\"Hello!\")],\n    )\n\n    handler = workflow.run(user_msg=\"Hi\")\n\n    events = []\n    async for event in handler.stream_events():\n        events.append(event)\n\n    await handler\n\n    # Should have received some events\n    assert len(events) > 0\n\n\nasync def test_agent_output_streamed(\n    create_simple_workflow: SimpleWorkflowFactory,\n) -> None:\n    \"\"\"Test that AgentOutput events are streamed.\"\"\"\n    workflow = create_simple_workflow(\n        responses=[make_text_response(\"Test response\")],\n    )\n\n    handler = workflow.run(user_msg=\"Test\")\n\n    agent_outputs = []\n    async for event in handler.stream_events():\n        if isinstance(event, AgentOutput):\n            agent_outputs.append(event)\n\n    await handler\n\n    # Should have at least one AgentOutput\n    assert len(agent_outputs) >= 1\n\n\nasync def test_tool_call_events_streamed(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that ToolCall and ToolCallResult events are streamed.\"\"\"\n\n    def dummy_tool() -> str:\n        \"\"\"A dummy tool.\"\"\"\n        return \"tool result\"\n\n    workflow = create_workflow(\n        tools=[dummy_tool],\n        responses=[\n            make_tool_call_response(\"dummy_tool\"),\n            make_text_response(\"Done\"),\n        ],\n    )\n\n    handler = workflow.run(user_msg=\"Use the tool\")\n\n    tool_calls = []\n    tool_results = []\n    async for event in handler.stream_events():\n        if isinstance(event, ToolCall):\n            tool_calls.append(event)\n        elif isinstance(event, ToolCallResult):\n            tool_results.append(event)\n\n    await handler\n\n    # Should have tool call and result events\n    assert len(tool_calls) >= 1\n    assert len(tool_results) >= 1\n\n\nasync def test_multiple_tool_calls_streamed(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that multiple sequential tool calls each produce events.\"\"\"\n    call_count = 0\n\n    def counting_tool() -> str:\n        nonlocal call_count\n        call_count += 1\n        return f\"Call {call_count}\"\n\n    workflow = create_workflow(\n        tools=[counting_tool],\n        responses=[\n            make_tool_call_response(\"counting_tool\", tool_id=\"call_1\"),\n            make_tool_call_response(\"counting_tool\", tool_id=\"call_2\"),\n            make_text_response(\"Done\"),\n        ],\n    )\n\n    handler = workflow.run(user_msg=\"Call tool twice\")\n\n    tool_results = []\n    async for event in handler.stream_events():\n        if isinstance(event, ToolCallResult):\n            tool_results.append(event)\n\n    await handler\n\n    # Should have two tool results\n    assert len(tool_results) == 2\n    assert call_count == 2\n\n\nasync def test_handler_result_after_streaming(\n    create_simple_workflow: SimpleWorkflowFactory,\n) -> None:\n    \"\"\"Test that awaiting handler after streaming returns the final result.\"\"\"\n    workflow = create_simple_workflow(\n        responses=[make_text_response(\"Final answer\")],\n    )\n\n    handler = workflow.run(user_msg=\"Question\")\n\n    # Stream all events\n    async for _ in handler.stream_events():\n        pass\n\n    # Await should return the result\n    result = await handler\n\n    assert result is not None\n    assert isinstance(result, AgentOutput)\n\n\nasync def test_events_contain_agent_name(\n    create_simple_workflow: SimpleWorkflowFactory,\n) -> None:\n    \"\"\"Test that streamed events contain the current agent name.\"\"\"\n    workflow = create_simple_workflow(\n        name=\"named_agent\",\n        responses=[make_text_response(\"Response\")],\n    )\n\n    handler = workflow.run(user_msg=\"Test\")\n\n    events_with_name = []\n    async for event in handler.stream_events():\n        if hasattr(event, \"current_agent_name\"):\n            events_with_name.append(event)\n\n    await handler\n\n    # Events should have the agent name\n    assert len(events_with_name) > 0\n    for event in events_with_name:\n        assert event.current_agent_name == \"named_agent\"\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_human_in_the_loop.py",
    "content": "# ty: ignore[unknown-argument]\n\"\"\"Tests for human-in-the-loop (HITL) workflow patterns.\n\nThese tests verify that wait_for_event works correctly inside tool\nfunctions, and that workflows can be paused, serialized, and resumed.\n\"\"\"\n\nfrom conftest import WorkflowFactory\nfrom llama_agents_integration_tests.helpers import (\n    make_text_response,\n    make_tool_call_response,\n)\nfrom workflows import Context\nfrom workflows.context import PickleSerializer\nfrom workflows.events import HumanResponseEvent, InputRequiredEvent\n\n\nasync def test_wait_for_event_in_tool(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that wait_for_event works inside a tool function.\"\"\"\n    received_response = None\n\n    async def ask_human(ctx: Context) -> str:\n        nonlocal received_response\n        resp = await ctx.wait_for_event(\n            HumanResponseEvent,\n            waiter_event=InputRequiredEvent(prefix=\"What is your name?\"),  # type: ignore[call-arg]\n        )\n        received_response = resp.response\n        return f\"Hello, {resp.response}!\"\n\n    workflow = create_workflow(\n        tools=[ask_human],\n        responses=[\n            make_tool_call_response(\"ask_human\"),\n            make_text_response(\"Greeted the user\"),\n        ],\n    )\n\n    handler = workflow.run(user_msg=\"Ask for my name\")\n\n    # Wait for the InputRequiredEvent\n    paused = False\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            assert event.prefix == \"What is your name?\"\n            paused = True\n            await handler.cancel_run()\n            break\n\n    assert paused, \"Should have paused for human input\"\n\n    # Resume with human response\n    new_ctx = Context.from_dict(workflow, handler.ctx.to_dict())\n    handler = workflow.run(user_msg=\"Ask for my name\", ctx=new_ctx)\n    handler.ctx.send_event(HumanResponseEvent(response=\"Alice\"))  # type: ignore[call-arg]\n\n    await handler\n\n    assert received_response == \"Alice\"\n\n\nasync def test_context_dict_serialization(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that context can be serialized to dict and restored.\"\"\"\n\n    async def pausable_tool(ctx: Context) -> str:\n        await ctx.wait_for_event(\n            HumanResponseEvent,\n            waiter_event=InputRequiredEvent(prefix=\"Continue?\"),  # type: ignore[call-arg]\n        )\n        return \"Continued\"\n\n    workflow = create_workflow(\n        tools=[pausable_tool],\n        responses=[\n            make_tool_call_response(\"pausable_tool\"),\n            make_text_response(\"Done\"),\n        ],\n    )\n\n    handler = workflow.run(user_msg=\"Test\")\n\n    # Pause and serialize\n    ctx_dict = None\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            ctx_dict = handler.ctx.to_dict()\n            await handler.cancel_run()\n            break\n\n    assert ctx_dict is not None\n\n    # Restore and resume\n    restored_ctx = Context.from_dict(workflow, ctx_dict)\n    handler = workflow.run(user_msg=\"Test\", ctx=restored_ctx)\n    handler.ctx.send_event(HumanResponseEvent(response=\"yes\"))  # type: ignore[call-arg]\n\n    result = await handler\n    assert result is not None\n\n\nasync def test_pickle_serialization(create_workflow: WorkflowFactory) -> None:\n    \"\"\"Test that context can be pickle-serialized for cross-process persistence.\"\"\"\n\n    async def pausable_tool(ctx: Context) -> str:\n        await ctx.wait_for_event(\n            HumanResponseEvent,\n            waiter_event=InputRequiredEvent(prefix=\"Confirm?\"),  # type: ignore[call-arg]\n        )\n        return \"Confirmed\"\n\n    workflow = create_workflow(\n        tools=[pausable_tool],\n        responses=[\n            make_tool_call_response(\"pausable_tool\"),\n            make_text_response(\"Done\"),\n        ],\n    )\n\n    handler = workflow.run(user_msg=\"Test\")\n\n    # Pause and pickle-serialize\n    ctx_dict = None\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            serializer = PickleSerializer()\n            ctx_dict = handler.ctx.to_dict(serializer=serializer)\n            await handler.cancel_run()\n            break\n\n    assert ctx_dict is not None\n\n    # Restore with pickle deserializer\n    serializer = PickleSerializer()\n    restored_ctx = Context.from_dict(workflow, ctx_dict, serializer=serializer)\n    handler = workflow.run(user_msg=\"Test\", ctx=restored_ctx)\n    handler.ctx.send_event(HumanResponseEvent(response=\"confirmed\"))  # type: ignore[call-arg]\n\n    result = await handler\n    assert result is not None\n\n\nasync def test_state_preserved_across_pause_resume(\n    create_workflow: WorkflowFactory,\n) -> None:\n    \"\"\"Test that workflow state is preserved when pausing and resuming.\"\"\"\n    final_state: dict | None = None\n\n    async def stateful_pause(ctx: Context) -> str:\n        nonlocal final_state\n        # Modify state before pausing\n        state = await ctx.store.get(\"state\")\n        state[\"before_pause\"] = True\n        await ctx.store.set(\"state\", state)\n\n        await ctx.wait_for_event(\n            HumanResponseEvent,\n            waiter_event=InputRequiredEvent(prefix=\"Ready?\"),  # type: ignore[call-arg]\n        )\n\n        # State should still be there after resume\n        state = await ctx.store.get(\"state\")\n        state[\"after_resume\"] = True\n        await ctx.store.set(\"state\", state)\n        final_state = dict(state)\n\n        return \"Done\"\n\n    workflow = create_workflow(\n        tools=[stateful_pause],\n        responses=[\n            make_tool_call_response(\"stateful_pause\"),\n            make_text_response(\"Complete\"),\n        ],\n        initial_state={\"initial\": True},\n    )\n\n    handler = workflow.run(user_msg=\"Test\")\n\n    # Pause\n    ctx_dict = None\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            ctx_dict = handler.ctx.to_dict()\n            await handler.cancel_run()\n            break\n\n    assert ctx_dict is not None\n\n    # Resume\n    restored_ctx = Context.from_dict(workflow, ctx_dict)\n    handler = workflow.run(user_msg=\"Test\", ctx=restored_ctx)\n    handler.ctx.send_event(HumanResponseEvent(response=\"yes\"))  # type: ignore[call-arg]\n\n    await handler\n\n    # Check all state was preserved via captured final state\n    assert final_state is not None\n    assert final_state[\"initial\"] is True\n    assert final_state[\"before_pause\"] is True\n    assert final_state[\"after_resume\"] is True\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_runtime_matrix.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"Runtime matrix tests - testing workflows against BasicRuntime and DBOSRuntime.\n\nAll workflow classes are defined at module level so they can be registered with\nDBOS once at module initialization time, avoiding repeated init/destroy cycles.\n\nNote: The dbos-postgres variant requires Docker to be available and is marked\nwith the 'docker' pytest marker. Run with `pytest -m docker` to include it.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nfrom pathlib import Path\nfrom typing import Any, AsyncGenerator, Generator\n\nimport pytest\nfrom dbos import DBOS, DBOSConfig\nfrom llama_agents.dbos import DBOSRuntime\nfrom pydantic import BaseModel, Field\nfrom testcontainers.postgres import PostgresContainer\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.errors import WorkflowTimeoutError\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.plugins.basic import BasicRuntime\nfrom workflows.runtime.types.plugin import Runtime\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.workflow import Workflow\n\n# -- Fixtures --\n\n\ndef _get_runtime_params() -> list[Any]:\n    \"\"\"Get runtime parameters for the test matrix.\n\n    Includes:\n    - basic: BasicRuntime (fast, no dependencies)\n    - dbos: DBOSRuntime with SQLite backend (fast, no Docker)\n    - dbos-postgres: DBOSRuntime with PostgreSQL backend (requires Docker)\n\n    Note: The dbos-postgres variant is marked with the 'docker' marker and\n    requires Docker to be running. It only runs when explicitly requested\n    via `pytest -m docker`.\n    \"\"\"\n    return [\n        pytest.param(\"basic\", id=\"basic\"),\n        pytest.param(\"dbos\", id=\"dbos\"),\n        pytest.param(\"dbos-postgres\", marks=pytest.mark.docker, id=\"dbos-postgres\"),\n    ]\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_container() -> Generator[PostgresContainer, None, None]:\n    \"\"\"Module-scoped PostgreSQL container for DBOS tests.\n\n    This fixture is only used when dbos-postgres runtime is requested.\n    Requires Docker to be running.\n    \"\"\"\n    with PostgresContainer(\"postgres:16\", driver=None) as postgres:\n        yield postgres\n\n\n@pytest.fixture\ndef dbos_runtime_sqlite(\n    tmp_path_factory: pytest.TempPathFactory,\n) -> Generator[DBOSRuntime, None, None]:\n    \"\"\"Function-scoped DBOS runtime with SQLite backend (fresh DB per test).\n\n    DBOS.destroy() shuts down its executor which it also installs as the event\n    loop's default executor. Since the event loop is module-scoped, we must\n    restore a fresh default executor after destroy to avoid poisoning subsequent\n    tests that use run_in_executor(None, ...).\n    \"\"\"\n    db_file: Path = tmp_path_factory.mktemp(\"dbos\") / \"dbos_test.sqlite3\"\n    system_db_url: str = f\"sqlite+pysqlite:///{db_file}?check_same_thread=false\"\n    config: DBOSConfig = {\n        \"name\": \"workflows-py-dbostest\",\n        \"system_database_url\": system_db_url,\n        \"run_admin_server\": False,\n        \"notification_listener_polling_interval_sec\": 0.01,\n    }\n    DBOS(config=config)\n    runtime = DBOSRuntime(polling_interval_sec=0.01)\n    # Capture the event loop before destroy_sync() can close it (asyncio.run()\n    # creates and closes a temporary loop, which removes the thread-local loop).\n    loop = asyncio.get_event_loop()\n    try:\n        yield runtime\n    finally:\n        runtime.destroy_sync()\n        # DBOS sets itself as the default executor and destroy() shuts it down.\n        # Restore a fresh executor so the module-scoped event loop remains usable.\n        # Re-set the event loop since asyncio.run() cleared it.\n        asyncio.set_event_loop(loop)\n        loop.set_default_executor(ThreadPoolExecutor())\n\n\n@pytest.fixture(scope=\"module\")\ndef dbos_runtime_postgres(\n    postgres_container: PostgresContainer,\n) -> Generator[DBOSRuntime, None, None]:\n    \"\"\"Module-scoped DBOS runtime with PostgreSQL backend.\"\"\"\n    connection_url = postgres_container.get_connection_url()\n    config: DBOSConfig = {\n        \"name\": \"wf-dbos-pg-test\",  # Must be <= 30 chars\n        \"system_database_url\": connection_url,\n        \"run_admin_server\": False,\n        \"notification_listener_polling_interval_sec\": 0.01,\n    }\n    DBOS(config=config)\n    runtime = DBOSRuntime(polling_interval_sec=0.01)\n    try:\n        yield runtime\n    finally:\n        runtime.destroy_sync()\n\n\n@pytest.fixture(params=_get_runtime_params())\nasync def runtime(\n    request: pytest.FixtureRequest,\n) -> AsyncGenerator[Runtime, None]:\n    \"\"\"Yield an unlaunched runtime.\n\n    For DBOS variants, returns the module-scoped runtime (already created, not yet\n    launched). Each test must call runtime.launch() after creating workflows.\n\n    Note: Only one DBOS variant can be used per test run since DBOS is a singleton.\n    Use TEST_DBOS_POSTGRES=1 to run with PostgreSQL instead of the default SQLite.\n    \"\"\"\n    if request.param == \"basic\":\n        rt = BasicRuntime()\n        try:\n            yield rt\n        finally:\n            await rt.destroy()\n    elif request.param == \"dbos\":\n        dbos_rt: DBOSRuntime = request.getfixturevalue(\"dbos_runtime_sqlite\")\n        yield dbos_rt\n    elif request.param == \"dbos-postgres\":\n        dbos_rt = request.getfixturevalue(\"dbos_runtime_postgres\")\n        yield dbos_rt\n\n\n# -- Shared event types --\n\n\nclass OneTestEvent(Event):\n    test_param: str = Field(default=\"test\")\n\n\nclass AnotherTestEvent(Event):\n    another_test_param: str = Field(default=\"another_test\")\n\n\nclass LastEvent(Event):\n    pass\n\n\nclass MyStart(StartEvent):\n    query: str\n\n\nclass MyStop(StopEvent):\n    outcome: str\n\n\n# -- Workflow definitions (module level for DBOS registration) --\n\n\nclass SimpleWorkflow(Workflow):\n    @step\n    async def start_step(self, ev: StartEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    @step\n    async def middle_step(self, ev: OneTestEvent) -> LastEvent:\n        return LastEvent()\n\n    @step\n    async def end_step(self, ev: LastEvent) -> StopEvent:\n        return StopEvent(result=\"Workflow completed\")\n\n\nclass SlowWorkflow(Workflow):\n    @step\n    async def slow_step(self, ev: StartEvent) -> StopEvent:\n        await asyncio.sleep(2.0)\n        return StopEvent(result=\"Done\")\n\n\nclass EventTrackingWorkflow(Workflow):\n    \"\"\"Workflow that tracks events in an external list.\"\"\"\n\n    tracked_events: list[str] = []\n\n    @step\n    async def step1(self, ev: StartEvent) -> OneTestEvent:\n        self.tracked_events.append(\"step1\")\n        return OneTestEvent()\n\n    @step\n    async def step2(self, ev: OneTestEvent) -> StopEvent:\n        self.tracked_events.append(\"step2\")\n        return StopEvent(result=\"Done\")\n\n\nclass SyncAsyncWorkflow(Workflow):\n    @step\n    async def async_step(self, ev: StartEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    @step\n    def sync_step(self, ev: OneTestEvent) -> StopEvent:\n        return StopEvent(result=\"Done\")\n\n\nclass SyncWorkflow(Workflow):\n    @step\n    def step_one(self, ctx: Context, ev: StartEvent) -> OneTestEvent:\n        ctx.collect_events(ev, [StartEvent])\n        return OneTestEvent()\n\n    @step\n    def step_two(self, ctx: Context, ev: OneTestEvent) -> StopEvent:\n        return StopEvent()\n\n\nclass MultiRunWorkflow(Workflow):\n    @step\n    async def step(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=ev.number * 2)\n\n\nclass ErrorWorkflow(Workflow):\n    @step\n    async def step(self, ev: StartEvent) -> StopEvent:\n        raise ValueError(\"The step raised an error!\")\n\n\nclass CounterWorkflow(Workflow):\n    @step\n    async def step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        cur_number = await ctx.store.get(\"number\", default=0)\n        new_number = cur_number + 1\n        await ctx.store.set(\"number\", new_number)\n        return StopEvent(result=new_number)\n\n\nclass StepSendEventWorkflow(Workflow):\n    @step\n    async def step1(self, ctx: Context, ev: StartEvent) -> OneTestEvent:\n        ctx.send_event(OneTestEvent(), step=\"step2\")\n        return None  # type: ignore\n\n    @step\n    async def step2(self, ev: OneTestEvent) -> StopEvent:\n        return StopEvent(result=\"step2\")\n\n    @step\n    async def step3(self, ev: OneTestEvent) -> StopEvent:\n        return StopEvent(result=\"step3\")\n\n\nclass NumWorkersWorkflow(Workflow):\n    @step\n    async def original_step(\n        self, ctx: Context, ev: StartEvent\n    ) -> OneTestEvent | LastEvent:\n        await ctx.store.set(\"num_to_collect\", 3)\n        ctx.send_event(OneTestEvent(test_param=\"test1\"))\n        ctx.send_event(OneTestEvent(test_param=\"test2\"))\n        ctx.send_event(OneTestEvent(test_param=\"test3\"))\n        ctx.send_event(AnotherTestEvent(another_test_param=\"test4\"))\n        return LastEvent()\n\n    @step(num_workers=3)\n    async def test_step(self, ev: OneTestEvent) -> AnotherTestEvent:\n        # Note: await_count logic needs to be injected per-test\n        return AnotherTestEvent(another_test_param=ev.test_param)\n\n    @step\n    async def final_step(\n        self, ctx: Context, ev: AnotherTestEvent | LastEvent\n    ) -> StopEvent:\n        n = await ctx.store.get(\"num_to_collect\")\n        events = ctx.collect_events(ev, [AnotherTestEvent] * n)\n        if events is None:\n            return None  # type: ignore\n        return StopEvent(result=[ev.another_test_param for ev in events])\n\n\nclass CustomEventsWorkflow(Workflow):\n    @step\n    async def start_step(self, ev: MyStart) -> OneTestEvent:\n        # Small delay to avoid DBOS read_stream_async race condition where\n        # the workflow completes before the stream reader starts polling.\n        # See thoughts/shared/bugs/dbos-read-stream-race.md\n        await asyncio.sleep(0.05)\n        return OneTestEvent()\n\n    @step\n    async def middle_step(self, ev: OneTestEvent) -> LastEvent:\n        return LastEvent()\n\n    @step\n    async def end_step(self, ev: LastEvent) -> MyStop:\n        return MyStop(outcome=\"Workflow completed\")\n\n\nclass HITLWorkflow(Workflow):\n    @step\n    async def step1(self, ctx: Context, ev: StartEvent) -> InputRequiredEvent:\n        cur_runs = await ctx.store.get(\"step1_runs\", default=0)\n        await ctx.store.set(\"step1_runs\", cur_runs + 1)\n        return InputRequiredEvent(prefix=\"Enter a number: \")  # type:ignore\n\n    @step\n    async def step2(self, ctx: Context, ev: HumanResponseEvent) -> StopEvent:\n        cur_runs = await ctx.store.get(\"step2_runs\", default=0)\n        await ctx.store.set(\"step2_runs\", cur_runs + 1)\n        return StopEvent(result=ev.response)\n\n\nclass StreamWorkflow(Workflow):\n    @step\n    async def chat(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        async def stream_messages() -> AsyncGenerator[str, None]:\n            resp = \"Paul Graham is a British-American computer scientist, entrepreneur, vc, and writer.\"\n            for word in resp.split():\n                yield word\n\n        async for w in stream_messages():\n            ctx.write_event_to_stream(Event(msg=w))\n\n        return StopEvent(result=None)\n\n\nclass ErrorStreamingWorkflow(Workflow):\n    @step\n    async def step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        ctx.write_event_to_stream(OneTestEvent(test_param=\"foo\"))\n        raise ValueError(\"The step raised an error!\")\n\n\nclass TimeoutStreamingWorkflow(Workflow):\n    @step\n    async def step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        ctx.write_event_to_stream(OneTestEvent(test_param=\"foo\"))\n        await asyncio.sleep(2)\n        return StopEvent()\n\n\n# -- Tests --\n\n\n@pytest.mark.asyncio\nasync def test_workflow_run(runtime: Runtime) -> None:\n    workflow = SimpleWorkflow(runtime=runtime)\n    await runtime.launch()\n    r = await WorkflowTestRunner(workflow).run()\n    assert r.result == \"Workflow completed\"\n\n\n@pytest.mark.asyncio\nasync def test_workflow_timeout(runtime: Runtime) -> None:\n    wf = SlowWorkflow(timeout=0.1, runtime=runtime)\n    await runtime.launch()\n    with pytest.raises(WorkflowTimeoutError):\n        await WorkflowTestRunner(wf).run()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_event_propagation(runtime: Runtime) -> None:\n    events: list[str] = []\n\n    class LocalEventTrackingWorkflow(Workflow):\n        @step\n        async def step1(self, ev: StartEvent) -> OneTestEvent:\n            events.append(\"step1\")\n            return OneTestEvent()\n\n        @step\n        async def step2(self, ev: OneTestEvent) -> StopEvent:\n            events.append(\"step2\")\n            return StopEvent(result=\"Done\")\n\n    wf = LocalEventTrackingWorkflow(runtime=runtime)\n    await runtime.launch()\n    await WorkflowTestRunner(wf).run()\n    assert events == [\"step1\", \"step2\"]\n\n\n@pytest.mark.asyncio\nasync def test_workflow_sync_async_steps(runtime: Runtime) -> None:\n    wf = SyncAsyncWorkflow(runtime=runtime)\n    await runtime.launch()\n    await WorkflowTestRunner(wf).run()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_sync_steps_only(runtime: Runtime) -> None:\n    wf = SyncWorkflow(runtime=runtime)\n    await runtime.launch()\n    await WorkflowTestRunner(wf).run()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_multiple_runs(runtime: Runtime) -> None:\n    wf = MultiRunWorkflow(runtime=runtime)\n    await runtime.launch()\n    runner = WorkflowTestRunner(wf)\n    results = await asyncio.gather(\n        runner.run(StartEvent(number=3)),  # type: ignore\n        runner.run(StartEvent(number=42)),  # type: ignore\n        runner.run(StartEvent(number=-99)),  # type: ignore\n    )\n    assert set([r.result for r in results]) == {6, 84, -198}\n\n\n@pytest.mark.asyncio\nasync def test_workflow_task_raises(runtime: Runtime) -> None:\n    wf = ErrorWorkflow(runtime=runtime)\n    await runtime.launch()\n    with pytest.raises(ValueError, match=\"The step raised an error!\"):\n        await WorkflowTestRunner(wf).run()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_step_send_event(runtime: Runtime) -> None:\n    workflow = StepSendEventWorkflow(runtime=runtime)\n    await runtime.launch()\n    r = await WorkflowTestRunner(workflow).run()\n    assert r.result == \"step2\"\n\n\n@pytest.mark.asyncio\nasync def test_workflow_num_workers(runtime: Runtime) -> None:\n    \"\"\"Test that num_workers limits concurrent step executions.\n\n    This test verifies that:\n    1. A step with num_workers=5 can process up to 5 events concurrently\n    2. All 5 workers can run simultaneously (they synchronize to prove concurrency)\n    3. The workflow completes successfully with all events processed\n    \"\"\"\n    num_workers = 5\n    num_events = 10\n    # Track max concurrent executions\n    lock = asyncio.Lock()\n    current_workers = 0\n    max_concurrent = 0\n    # Barrier to ensure all workers reach this point before any proceed\n    barrier_count = 0\n    barrier_event = asyncio.Event()\n\n    class NumWorkersWorkflow(Workflow):\n        @step\n        async def fan_out(self, ctx: Context, ev: StartEvent) -> OneTestEvent:\n            # Send more events than num_workers to test queuing\n            for i in range(num_events):\n                ctx.send_event(OneTestEvent(test_param=str(i)))\n            return None  # type: ignore\n\n        @step(num_workers=num_workers)\n        async def worker_step(self, ev: OneTestEvent) -> AnotherTestEvent:\n            nonlocal current_workers, max_concurrent, barrier_count\n\n            async with lock:\n                current_workers += 1\n                max_concurrent = max(max_concurrent, current_workers)\n                barrier_count += 1\n                if barrier_count == num_workers:\n                    # All workers have arrived, release them\n                    barrier_event.set()\n\n            # Wait for all workers to arrive (proves concurrency)\n            await barrier_event.wait()\n\n            async with lock:\n                current_workers -= 1\n\n            return AnotherTestEvent(another_test_param=ev.test_param)\n\n        @step\n        async def collect_step(\n            self, ctx: Context, ev: AnotherTestEvent\n        ) -> StopEvent | None:\n            events = ctx.collect_events(ev, [AnotherTestEvent] * num_events)\n            if events is None:\n                return None\n            return StopEvent(result=[e.another_test_param for e in events])\n\n    workflow = NumWorkersWorkflow(timeout=10, runtime=runtime)\n    await runtime.launch()\n    r = await WorkflowTestRunner(workflow).run()\n\n    # Verify all events were processed\n    assert len(r.result) == num_events\n    assert set(r.result) == {str(i) for i in range(num_events)}\n    # Verify we achieved the expected concurrency (all 5 workers ran together)\n    assert max_concurrent == num_workers\n\n\n@pytest.mark.asyncio\nasync def test_custom_stop_event(runtime: Runtime) -> None:\n    wf = CustomEventsWorkflow(runtime=runtime)\n    await runtime.launch()\n\n    assert wf._start_event_class == MyStart\n    assert wf.start_event_class == wf._start_event_class\n    assert wf._stop_event_class == MyStop\n    assert wf.stop_event_class == wf._stop_event_class\n    result: MyStop = await wf.run(query=\"foo\")\n    assert result.outcome == \"Workflow completed\"\n\n    # Run again with the same workflow instance\n    assert wf._start_event_class == MyStart\n    assert wf._stop_event_class == MyStop\n    result = await wf.run(query=\"foo\")\n    assert result.outcome == \"Workflow completed\"\n\n    # ensure that streaming exits\n    r = await WorkflowTestRunner(wf).run(MyStart(query=\"foo\"))\n    assert len(r.collected) > 0\n\n\n@pytest.mark.asyncio\nasync def test_human_in_the_loop(runtime: Runtime) -> None:\n    # Create both workflow instances before launch\n    timeout_wf = HITLWorkflow(timeout=0.01, runtime=runtime)\n    workflow = HITLWorkflow(runtime=runtime)\n    await runtime.launch()\n\n    # workflow should raise a timeout error because hitl only works with streaming\n    with pytest.raises(WorkflowTimeoutError):\n        await WorkflowTestRunner(timeout_wf).run()\n\n    # workflow should work with streaming\n    handler = workflow.run()\n    assert handler.ctx\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            assert event.prefix == \"Enter a number: \"\n            handler.ctx.send_event(HumanResponseEvent(response=\"42\"))  # type:ignore\n\n    final_result = await handler\n    assert final_result == \"42\"\n\n\n@pytest.mark.asyncio\nasync def test_workflow_stream_events_exits(runtime: Runtime) -> None:\n    wf = CustomEventsWorkflow(runtime=runtime)\n    await runtime.launch()\n    handler = wf.run(query=\"foo\")\n\n    async def _stream_events() -> MyStop:\n        async for event in handler.stream_events():\n            continue\n        return await handler\n\n    stream_task = asyncio.create_task(_stream_events())\n    result = await asyncio.wait_for(stream_task, timeout=10)\n    assert result.outcome == \"Workflow completed\"\n\n\n# -- Streaming tests --\n\n\n@pytest.mark.asyncio\nasync def test_streaming_e2e(runtime: Runtime) -> None:\n    wf = StreamWorkflow(runtime=runtime)\n    await runtime.launch()\n    test_runner = WorkflowTestRunner(wf)\n    r = await test_runner.run(expose_internal=False, exclude_events=[StopEvent])\n    assert all(\"msg\" in ev for ev in r.collected)\n\n\n@pytest.mark.asyncio\nasync def test_streaming_task_raised(runtime: Runtime) -> None:\n    wf = ErrorStreamingWorkflow(runtime=runtime)\n    await runtime.launch()\n    r = wf.run()\n\n    async for ev in r.stream_events():\n        if not isinstance(ev, StopEvent):\n            assert ev.test_param == \"foo\"\n\n    with pytest.raises(ValueError, match=\"The step raised an error!\"):\n        await r\n\n\n@pytest.mark.asyncio\nasync def test_streaming_task_timeout(runtime: Runtime) -> None:\n    wf = TimeoutStreamingWorkflow(timeout=0.1, runtime=runtime)\n    await runtime.launch()\n    r = wf.run()\n\n    async for ev in r.stream_events():\n        if not isinstance(ev, StopEvent):\n            assert ev.test_param == \"foo\"\n\n    with pytest.raises(WorkflowTimeoutError, match=\"Operation timed out\"):\n        await r\n\n\n# -- Workflow State Tests --\n\n\nclass StatefulWorkflow(Workflow):\n    \"\"\"Workflow that accumulates state across steps.\"\"\"\n\n    @step\n    async def step1(self, ctx: Context, ev: StartEvent) -> OneTestEvent:\n        await ctx.store.set(\"step1_ran\", True)\n        await ctx.store.set(\"counter\", 1)\n        return OneTestEvent()\n\n    @step\n    async def step2(self, ctx: Context, ev: OneTestEvent) -> StopEvent:\n        await ctx.store.set(\"step2_ran\", True)\n        counter = await ctx.store.get(\"counter\")\n        await ctx.store.set(\"counter\", counter + 1)\n        final_counter = await ctx.store.get(\"counter\")\n        return StopEvent(result={\"counter\": final_counter})\n\n\nclass NestedStateWorkflow(Workflow):\n    \"\"\"Workflow that uses nested state paths.\"\"\"\n\n    @step\n    async def process(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        await ctx.store.set(\"user\", {\"name\": \"Alice\", \"profile\": {\"level\": 1}})\n        await ctx.store.set(\"user.profile.level\", 2)\n        level = await ctx.store.get(\"user.profile.level\")\n        name = await ctx.store.get(\"user.name\")\n        return StopEvent(result={\"name\": name, \"level\": level})\n\n\n@pytest.mark.asyncio\nasync def test_workflow_state_basic(runtime: Runtime) -> None:\n    \"\"\"Test basic state operations within a workflow.\"\"\"\n    wf = CounterWorkflow(runtime=runtime)\n    await runtime.launch()\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == 1\n\n\n@pytest.mark.asyncio\nasync def test_workflow_state_across_steps(runtime: Runtime) -> None:\n    \"\"\"Test state persistence across multiple workflow steps.\"\"\"\n    wf = StatefulWorkflow(runtime=runtime)\n    await runtime.launch()\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == {\"counter\": 2}\n\n\n@pytest.mark.asyncio\nasync def test_workflow_nested_state(runtime: Runtime) -> None:\n    \"\"\"Test nested state path access within workflows.\"\"\"\n    wf = NestedStateWorkflow(runtime=runtime)\n    await runtime.launch()\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == {\"name\": \"Alice\", \"level\": 2}\n\n\n@pytest.mark.asyncio\nasync def test_workflow_state_multiple_runs(runtime: Runtime) -> None:\n    \"\"\"Test that each workflow run has isolated state.\"\"\"\n    wf = CounterWorkflow(runtime=runtime)\n    await runtime.launch()\n    runner = WorkflowTestRunner(wf)\n\n    # Run multiple times - each should start fresh\n    results = await asyncio.gather(\n        runner.run(),\n        runner.run(),\n        runner.run(),\n    )\n\n    # Each run should have counter=1 (not accumulating)\n    for r in results:\n        assert r.result == 1\n\n\n# -- Typed State Tests --\n\n\nclass TypedState(BaseModel):\n    \"\"\"Custom typed state for workflow testing.\"\"\"\n\n    counter: int = 0\n    name: str = \"default\"\n    items: list[str] = Field(default_factory=list)\n\n\nclass TypeStateStopEvent(StopEvent):\n    state_type: str\n    initial_counter: int\n    final_counter: int\n    final_name: str\n\n\nclass TypedStateWorkflow(Workflow):\n    \"\"\"Workflow that uses typed state via Context[TypedState].\"\"\"\n\n    @step\n    async def process(self, ctx: Context[TypedState], ev: StartEvent) -> StopEvent:\n        # Access typed state\n        state = await ctx.store.get_state()\n\n        # Verify we got the right type\n        state_type_name = type(state).__name__\n\n        # Modify state using typed fields\n        await ctx.store.set(\"counter\", state.counter + 1)\n        await ctx.store.set(\"name\", \"modified\")\n\n        final_state = await ctx.store.get_state()\n        return TypeStateStopEvent(\n            state_type=state_type_name,\n            initial_counter=state.counter,\n            final_counter=final_state.counter,\n            final_name=final_state.name,\n        )\n\n\n@pytest.mark.asyncio\nasync def test_typed_state_workflow(runtime: Runtime) -> None:\n    \"\"\"Test workflow with typed state Context[TypedState].\n\n    This verifies that:\n    1. The state type is correctly inferred from Context[T] annotation\n    2. The state is created with the correct type\n    3. Typed field access works correctly\n    \"\"\"\n    wf = TypedStateWorkflow(runtime=runtime)\n    await runtime.launch()\n\n    result = await WorkflowTestRunner(wf).run()\n\n    # The state should be TypedState, not DictState\n    assert result.result.state_type == \"TypedState\", (\n        f\"Expected TypedState but got {result.result.state_type}. \"\n        \"State type inference may not be working.\"\n    )\n    assert result.result.initial_counter == 0\n    assert result.result.final_counter == 1\n    assert result.result.final_name == \"modified\"\n\n\nclass TypedStateWithDefaultsWorkflow(Workflow):\n    \"\"\"Workflow that verifies typed state has correct defaults.\"\"\"\n\n    @step\n    async def check_defaults(\n        self, ctx: Context[TypedState], ev: StartEvent\n    ) -> StopEvent:\n        state = await ctx.store.get_state()\n        return StopEvent(\n            result={\n                \"counter\": state.counter,\n                \"name\": state.name,\n                \"items\": state.items,\n            }\n        )\n\n\n@pytest.mark.asyncio\nasync def test_typed_state_defaults(runtime: Runtime) -> None:\n    \"\"\"Test that typed state is initialized with correct defaults.\"\"\"\n    wf = TypedStateWithDefaultsWorkflow(runtime=runtime)\n    await runtime.launch()\n\n    result = await WorkflowTestRunner(wf).run()\n\n    assert result.result[\"counter\"] == 0\n    assert result.result[\"name\"] == \"default\"\n    assert result.result[\"items\"] == []\n\n\n@pytest.mark.asyncio\nasync def test_typed_state_with_initial_values(runtime: Runtime) -> None:\n    \"\"\"Test that initial state values are passed through to the workflow.\n\n    This verifies that:\n    1. State can be set before running the workflow\n    2. The initial values are correctly used (not replaced with defaults)\n    3. Modifications build on the initial values\n    \"\"\"\n    wf = TypedStateWorkflow(runtime=runtime)\n    await runtime.launch()\n\n    # Create a context and set initial state with counter=1 (default is 0)\n    ctx = Context(wf)\n    await ctx.store.set(\"counter\", 1)\n\n    result = await WorkflowTestRunner(wf).run(ctx=ctx)\n\n    # If initial state wasn't passed through, initial_counter would be 0\n    # and final_counter would be 1 (from default 0 + 1)\n    assert result.result.initial_counter == 1, (\n        f\"Expected initial_counter=1 but got {result.result.initial_counter}. \"\n        \"Initial state was not passed through to the workflow.\"\n    )\n    assert result.result.final_counter == 2, (\n        f\"Expected final_counter=2 but got {result.result.final_counter}. \"\n        \"State modification did not build on initial value.\"\n    )\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_server_http_matrix.py",
    "content": "# ty: ignore[unknown-argument]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Parameterized live HTTP server integration tests across storage backends.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport socket\nfrom pathlib import Path\nfrom typing import Any, AsyncGenerator\n\nimport httpx\nimport pytest\nimport uvicorn\nfrom dbos import DBOS, DBOSConfig\nfrom llama_agents.client.client import WorkflowClient\nfrom llama_agents.dbos import DBOSRuntime\nfrom llama_agents.server import MemoryWorkflowStore, SqliteWorkflowStore, WorkflowServer\nfrom llama_agents_integration_tests.server_test_utils import (\n    ExternalEvent,\n    InteractiveWorkflow,\n    SimpleTestWorkflow,\n    StreamingWorkflow,\n    live_server,\n    wait_for_passing,\n    wait_for_requested_external_event_stream,\n)\nfrom testcontainers.postgres import PostgresContainer\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n)\n\n\n# Workflow definitions (test-specific)\nclass CumulativeWorkflow(Workflow):\n    @step\n    async def accumulate(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        current_count = await ctx.store.get(\"count\", 0)\n        increment = getattr(ev, \"increment\", 1)\n        new_count = current_count + increment\n        await ctx.store.set(\"count\", new_count)\n        run_history = await ctx.store.get(\"run_history\", [])\n        run_history.append(f\"run_{len(run_history) + 1}\")\n        await ctx.store.set(\"run_history\", run_history)\n        return StopEvent(result=f\"count: {new_count}, runs: {len(run_history)}\")\n\n\nclass WaitableExternalEvent(Event):\n    response: str\n\n\nclass WaitingWorkflow(Workflow):\n    @step\n    async def start_and_wait(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        external = await ctx.wait_for_event(WaitableExternalEvent)\n        return StopEvent(result=f\"received: {external.response}\")\n\n\ndef _get_backend_params() -> list[Any]:\n    return [\n        pytest.param(\"memory\", id=\"memory\"),\n        pytest.param(\"sqlite\", id=\"sqlite\"),\n        pytest.param(\"postgres\", marks=pytest.mark.docker, id=\"postgres\"),\n    ]\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_container(request: Any) -> Any:\n    # Only start the container when docker-marked tests will actually run\n    if not any(\n        item.get_closest_marker(\"docker\")\n        for item in request.session.items\n        if item.module is request.module\n    ):\n        yield None\n        return\n    with PostgresContainer(\"postgres:16\", driver=None) as container:\n        yield container\n\n\ndef _add_all_workflows(server: WorkflowServer) -> None:\n    server.add_workflow(\"SimpleTestWorkflow\", SimpleTestWorkflow())\n    server.add_workflow(\"StreamingWorkflow\", StreamingWorkflow())\n    server.add_workflow(\"InteractiveWorkflow\", InteractiveWorkflow())\n    server.add_workflow(\"CumulativeWorkflow\", CumulativeWorkflow())\n    server.add_workflow(\"WaitingWorkflow\", WaitingWorkflow())\n\n\n_pg_server_state: dict[str, Any] = {}\n\n\nasync def _start_postgres_server(\n    postgres_container: Any,\n) -> tuple[str, WorkflowServer]:\n    \"\"\"Start a persistent postgres-backed server (called once per module).\"\"\"\n    connection_url = postgres_container.get_connection_url()\n    dbos_config: DBOSConfig = {\n        \"name\": \"wf-server-http-pg\",\n        \"system_database_url\": connection_url,\n        \"run_admin_server\": False,\n        \"notification_listener_polling_interval_sec\": 0.01,\n    }\n    DBOS(config=dbos_config)\n    runtime = DBOSRuntime(polling_interval_sec=0.01)\n    await runtime.launch()\n    store = runtime.create_workflow_store()\n    server_runtime = runtime.build_server_runtime()\n\n    server = WorkflowServer(workflow_store=store, runtime=server_runtime)\n    _add_all_workflows(server)\n    await server.start()\n\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    sock.bind((\"127.0.0.1\", 0))\n    sock.listen(128)\n    port = sock.getsockname()[1]\n\n    config = uvicorn.Config(\n        server.app, host=\"127.0.0.1\", port=port, log_level=\"error\", loop=\"asyncio\"\n    )\n    uv_server = uvicorn.Server(config)\n    serve_task = asyncio.create_task(uv_server.serve(sockets=[sock]))\n\n    base_url = f\"http://127.0.0.1:{port}\"\n    async with httpx.AsyncClient(base_url=base_url, timeout=1.0) as client:\n        for _ in range(50):\n            try:\n                resp = await client.get(\"/health\")\n                if resp.status_code == 200:\n                    break\n            except Exception:\n                pass\n            await asyncio.sleep(0.01)\n        else:\n            uv_server.should_exit = True\n            await serve_task\n            raise RuntimeError(\"Postgres live server did not start in time\")\n\n    _pg_server_state[\"runtime\"] = runtime\n    _pg_server_state[\"uv_server\"] = uv_server\n    _pg_server_state[\"serve_task\"] = serve_task\n    _pg_server_state[\"sock\"] = sock\n    _pg_server_state[\"server\"] = server\n    _pg_server_state[\"base_url\"] = base_url\n    return base_url, server\n\n\nasync def _stop_postgres_server() -> None:\n    if \"uv_server\" in _pg_server_state:\n        _pg_server_state[\"uv_server\"].should_exit = True\n        try:\n            await _pg_server_state[\"serve_task\"]\n        except Exception:\n            pass\n        _pg_server_state[\"sock\"].close()\n        await _pg_server_state[\"server\"].stop()\n        _pg_server_state.clear()\n\n\n@pytest.fixture(scope=\"module\")\nasync def postgres_server(\n    postgres_container: Any,\n) -> AsyncGenerator[tuple[str, WorkflowServer] | None, None]:\n    \"\"\"Module-scoped postgres server — DBOS is created and destroyed once.\"\"\"\n    if postgres_container is None:\n        yield None\n        return\n    result = await _start_postgres_server(postgres_container)\n    yield result\n    await _stop_postgres_server()\n\n\n@pytest.fixture\nasync def backend_server(\n    request: Any,\n    tmp_path: Path,\n    postgres_server: tuple[str, WorkflowServer] | None,\n) -> AsyncGenerator[tuple[str, WorkflowServer], None]:\n    backend = request.param\n\n    if backend == \"memory\":\n\n        def factory() -> WorkflowServer:\n            store = MemoryWorkflowStore()\n            server = WorkflowServer(workflow_store=store)\n            _add_all_workflows(server)\n            return server\n\n        async with live_server(factory) as (base_url, server):\n            yield base_url, server\n\n    elif backend == \"sqlite\":\n        db_path = tmp_path / \"test.db\"\n\n        def factory() -> WorkflowServer:\n            store = SqliteWorkflowStore(db_path=str(db_path))\n            server = WorkflowServer(workflow_store=store)\n            _add_all_workflows(server)\n            return server\n\n        async with live_server(factory) as (base_url, server):\n            yield base_url, server\n\n    elif backend == \"postgres\":\n        assert postgres_server is not None\n        yield postgres_server\n\n    else:\n        raise ValueError(f\"Unknown backend: {backend}\")\n\n\n# Tests\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"backend_server\", _get_backend_params(), indirect=True)\nasync def test_basic_run_and_result(\n    backend_server: tuple[str, WorkflowServer],\n) -> None:\n    base_url, server = backend_server\n    client = WorkflowClient(base_url=base_url)\n\n    start_event = StartEvent(message=\"test_message\")  # type: ignore[call-arg]\n    result = await client.run_workflow(\"SimpleTestWorkflow\", start_event=start_event)\n    assert result.result is not None\n    assert result.result.value[\"result\"] == \"processed: test_message\"\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"backend_server\", _get_backend_params(), indirect=True)\nasync def test_streaming_and_interactive(\n    backend_server: tuple[str, WorkflowServer],\n) -> None:\n    base_url, server = backend_server\n    client = WorkflowClient(base_url=base_url)\n\n    started = await client.run_workflow_nowait(\"InteractiveWorkflow\")\n    handler_id = started.handler_id\n\n    await wait_for_requested_external_event_stream(\n        client, handler_id, label=type(server._workflow_store).__name__\n    )\n    sent = await client.send_event(handler_id, ExternalEvent(response=\"pong\"))\n    assert sent.status == \"sent\"\n\n    async def check_completed() -> str:\n        handler = await client.get_handler(handler_id)\n        assert handler.status == \"completed\"\n        assert handler.result is not None\n        return handler.result.value[\"result\"]\n\n    result = await wait_for_passing(check_completed, max_duration=5.0)\n    assert result == \"received: pong\"\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"backend_server\", _get_backend_params(), indirect=True)\nasync def test_reconnect_stream(\n    backend_server: tuple[str, WorkflowServer],\n) -> None:\n    base_url, server = backend_server\n    client = WorkflowClient(base_url=base_url)\n\n    started = await client.run_workflow_nowait(\"InteractiveWorkflow\")\n    handler_id = started.handler_id\n\n    prompt_cursor = await wait_for_requested_external_event_stream(\n        client, handler_id, label=type(server._workflow_store).__name__\n    )\n\n    stop_seen = asyncio.Event()\n\n    async def consume_again() -> None:\n        async for ev in client.get_workflow_events(\n            handler_id, after_sequence=prompt_cursor\n        ):\n            event = ev.load_event()\n            if isinstance(event, StopEvent):\n                stop_seen.set()\n                break\n\n    consume_task = asyncio.create_task(consume_again())\n\n    await asyncio.sleep(0.05)\n\n    sent = await client.send_event(handler_id, ExternalEvent(response=\"reconnect_test\"))\n    assert sent.status == \"sent\"\n\n    await asyncio.wait_for(stop_seen.wait(), timeout=5.0)\n    consume_task.cancel()\n\n    async def check_completed() -> str:\n        handler = await client.get_handler(handler_id)\n        assert handler.status == \"completed\"\n        assert handler.result is not None\n        return handler.result.value[\"result\"]\n\n    result = await wait_for_passing(check_completed, max_duration=5.0)\n    assert result == \"received: reconnect_test\"\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"backend_server\", _get_backend_params(), indirect=True)\nasync def test_cumulative_rerun(\n    backend_server: tuple[str, WorkflowServer],\n) -> None:\n    base_url, server = backend_server\n    client = WorkflowClient(base_url=base_url)\n\n    start_event1 = StartEvent(increment=5)  # type: ignore[call-arg]\n    result1 = await client.run_workflow(\"CumulativeWorkflow\", start_event=start_event1)\n    assert result1.result is not None\n    result_str1 = result1.result.value[\"result\"]\n    assert \"count: 5\" in result_str1\n    assert \"runs: 1\" in result_str1\n\n    handler_id = result1.handler_id\n\n    start_event2 = StartEvent(increment=3)  # type: ignore[call-arg]\n    result2 = await client.run_workflow(\n        \"CumulativeWorkflow\", handler_id=handler_id, start_event=start_event2\n    )\n    assert result2.result is not None\n    result_str2 = result2.result.value[\"result\"]\n    assert \"count: 8\" in result_str2\n    assert \"runs: 2\" in result_str2\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_server_store_matrix.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"End-to-end HTTP tests matrix-tested across workflow stores (memory, sqlite).\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import Any, AsyncGenerator, Callable\n\nimport httpx\nimport pytest\nfrom llama_agents.client.client import WorkflowClient\nfrom llama_agents.server import (\n    MemoryWorkflowStore,\n    SqliteWorkflowStore,\n    WorkflowServer,\n)\nfrom llama_agents.server._store.postgres_workflow_store import PostgresWorkflowStore\nfrom llama_agents_integration_tests.fake_agent_data import (\n    FakeAgentDataBackend,\n    create_agent_data_store,\n)\nfrom llama_agents_integration_tests.postgres import get_asyncpg_dsn\nfrom llama_agents_integration_tests.server_test_utils import (\n    ExternalEvent,\n    InteractiveWorkflow,\n    SimpleTestWorkflow,\n    StreamEvent,\n    StreamingWorkflow,\n    live_server,\n    wait_for_passing,\n    wait_for_requested_external_event_stream,\n)\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\n# -- Utilities --\n\n\nasync def _get_handler_raw(base_url: str, handler_id: str) -> dict[str, Any]:\n    \"\"\"Get handler data without raising on non-200 status (server returns 500 for failed/cancelled).\"\"\"\n    async with httpx.AsyncClient(base_url=base_url, timeout=5.0) as client:\n        resp = await client.get(f\"/handlers/{handler_id}\")\n        return resp.json()  # type: ignore[no-any-return]\n\n\n# -- Workflow definitions (test-specific) --\n\n\nclass ErrorWorkflow(Workflow):\n    @step\n    async def error_step(self, ev: StartEvent) -> StopEvent:\n        raise ValueError(\"Test error\")\n\n\n# -- Fixtures --\n\n\n@pytest.fixture(\n    params=[\n        \"memory\",\n        \"sqlite\",\n        \"agent_data\",\n        pytest.param(\"postgres\", marks=pytest.mark.docker),\n    ]\n)\nasync def server_with_store(\n    request: pytest.FixtureRequest,\n    tmp_path_factory: pytest.TempPathFactory,\n    monkeypatch: pytest.MonkeyPatch,\n) -> AsyncGenerator[tuple[str, WorkflowServer, str], None]:\n    store_type: str = request.param\n\n    pg_store: PostgresWorkflowStore | None = None\n\n    if store_type == \"postgres\":\n        postgres_container = request.getfixturevalue(\"postgres_container\")\n        dsn = get_asyncpg_dsn(postgres_container)\n        pg_store = PostgresWorkflowStore(dsn=dsn)\n        await pg_store.start()\n\n    def make_server() -> WorkflowServer:\n        if store_type == \"memory\":\n            store = MemoryWorkflowStore()\n        elif store_type == \"sqlite\":\n            db_path = tmp_path_factory.mktemp(\"sqlite\") / \"test.db\"\n            store = SqliteWorkflowStore(str(db_path))\n        elif store_type == \"postgres\":\n            assert pg_store is not None\n            store = pg_store\n        else:\n            store = create_agent_data_store(FakeAgentDataBackend(), monkeypatch)\n        server = WorkflowServer(workflow_store=store)\n        server.add_workflow(\"test\", SimpleTestWorkflow())\n        server.add_workflow(\"streaming\", StreamingWorkflow())\n        server.add_workflow(\n            \"interactive\", InteractiveWorkflow(), additional_events=[ExternalEvent]\n        )\n        server.add_workflow(\"error\", ErrorWorkflow())\n        return server\n\n    async with live_server(make_server) as (base_url, server):\n        try:\n            yield base_url, server, store_type\n        finally:\n            if pg_store is not None:\n                await pg_store.close()\n\n\n# -- Tests --\n\n\n@pytest.mark.asyncio\nasync def test_sync_run(server_with_store: tuple[str, WorkflowServer, str]) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n    result = await client.run_workflow(\"test\", start_event=StartEvent())\n    assert result.status == \"completed\"\n    assert result.result is not None\n    assert result.result.value.get(\"result\") == \"processed: default\"\n\n\n@pytest.mark.asyncio\nasync def test_async_run_and_poll(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n    started = await client.run_workflow_nowait(\"test\")\n    handler_id = started.handler_id\n\n    async def check_completed() -> None:\n        data = await client.get_handler(handler_id)\n        assert data.status == \"completed\"\n        assert data.result is not None\n        assert data.result.value.get(\"result\") == \"processed: default\"\n\n    await wait_for_passing(check_completed)\n\n\n@pytest.mark.asyncio\nasync def test_sse_event_streaming(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n    started = await client.run_workflow_nowait(\"streaming\")\n    handler_id = started.handler_id\n\n    events_seen: list[StreamEvent] = []\n    async for ev in client.get_workflow_events(handler_id):\n        event = ev.load_event([StreamEvent])\n        if isinstance(event, StreamEvent):\n            events_seen.append(event)\n\n    assert len(events_seen) == 3\n    assert [e.sequence for e in events_seen] == [0, 1, 2]\n\n\n@pytest.mark.asyncio\nasync def test_send_external_event(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n    started = await client.run_workflow_nowait(\"interactive\")\n    handler_id = started.handler_id\n\n    await wait_for_requested_external_event_stream(client, handler_id, label=store_type)\n    sent = await client.send_event(handler_id, ExternalEvent(response=\"pong\"))\n    assert sent.status == \"sent\"\n\n    async def check_completed() -> None:\n        data = await client.get_handler(handler_id)\n        assert data.status == \"completed\", (\n            f\"{store_type}: handler {handler_id} did not complete after external event\"\n        )\n        assert data.result is not None\n        assert data.result.value.get(\"result\") == \"received: pong\"\n\n    await wait_for_passing(check_completed)\n\n\n@pytest.mark.asyncio\nasync def test_cancel_handler(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n    started = await client.run_workflow_nowait(\"interactive\")\n    handler_id = started.handler_id\n\n    # Wait for the workflow to reach the waiting state by polling\n    async def is_running() -> None:\n        data = await client.get_handler(handler_id)\n        assert data.status == \"running\"\n\n    await wait_for_passing(is_running)\n\n    result = await client.cancel_handler(handler_id)\n    assert result.status == \"cancelled\"\n\n    async def check_cancelled() -> None:\n        data = await _get_handler_raw(base_url, handler_id)\n        assert data[\"status\"] == \"cancelled\"\n\n    await wait_for_passing(check_cancelled)\n\n\n@pytest.mark.asyncio\nasync def test_list_handlers(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n\n    # Run two workflows\n    await client.run_workflow(\"test\", start_event=StartEvent())\n    await client.run_workflow(\"test\", start_event=StartEvent())\n\n    handlers = await client.get_handlers()\n    assert len(handlers.handlers) >= 2\n\n\n@pytest.mark.asyncio\nasync def test_error_workflow_status(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n    started = await client.run_workflow_nowait(\"error\")\n    handler_id = started.handler_id\n\n    async def check_failed() -> None:\n        data = await _get_handler_raw(base_url, handler_id)\n        assert data[\"status\"] == \"failed\"\n        assert data[\"error\"] is not None\n        assert \"Test error\" in data[\"error\"]\n\n    await wait_for_passing(check_failed)\n\n\n@pytest.mark.asyncio\nasync def test_streaming_workflow_events(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n    started = await client.run_workflow_nowait(\"streaming\")\n    handler_id = started.handler_id\n\n    stream_events: list[StreamEvent] = []\n    saw_stop = False\n    async for ev in client.get_workflow_events(handler_id):\n        event = ev.load_event([StreamEvent])\n        if isinstance(event, StreamEvent):\n            stream_events.append(event)\n        elif isinstance(event, StopEvent):\n            saw_stop = True\n\n    assert len(stream_events) == 3\n    assert saw_stop\n\n\n@pytest.mark.asyncio\nasync def test_cursor_resume(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n\n    # Run a streaming workflow synchronously first so all events are stored\n    result = await client.run_workflow(\"streaming\", start_event=StartEvent())\n    assert result.status == \"completed\"\n    handler_id = result.handler_id\n\n    # Fetch events after sequence 1 using client API\n    events = []\n    async for ev in client.get_workflow_events(handler_id, after_sequence=1):\n        events.append(ev)\n\n    # Should have events after sequence 1 (i.e. sequence 2+ and StopEvent)\n    assert len(events) >= 1\n\n\n@pytest.mark.asyncio\nasync def test_concurrent_workflows(\n    server_with_store: tuple[str, WorkflowServer, str],\n) -> None:\n    base_url, _server, _store_type = server_with_store\n    client = WorkflowClient(base_url=base_url)\n\n    # Start multiple workflows concurrently\n    tasks = [client.run_workflow_nowait(\"test\") for _ in range(5)]\n    started = await asyncio.gather(*tasks)\n    handler_ids = [s.handler_id for s in started]\n\n    async def all_completed() -> None:\n        for hid in handler_ids:\n            data = await client.get_handler(hid)\n            assert data.status == \"completed\"\n\n    await wait_for_passing(all_completed)\n\n\n# -- Durability tests (parameterized across durable stores) --\n\n\n@pytest.fixture(\n    params=[\n        \"sqlite\",\n        \"agent_data\",\n        pytest.param(\"postgres\", marks=pytest.mark.docker),\n    ]\n)\ndef durable_server_factory(\n    request: pytest.FixtureRequest,\n    tmp_path_factory: pytest.TempPathFactory,\n    monkeypatch: pytest.MonkeyPatch,\n) -> Callable[[], WorkflowServer]:\n    \"\"\"Factory that creates a WorkflowServer with a durable store.\n\n    Calling the factory multiple times reuses the same underlying storage,\n    which is essential for restart/reload tests.\n    \"\"\"\n    store_type: str = request.param\n\n    if store_type == \"sqlite\":\n        db_path = str(tmp_path_factory.mktemp(\"sqlite_durable\") / \"test.db\")\n\n        def make_server() -> WorkflowServer:\n            store = SqliteWorkflowStore(db_path)\n            server = WorkflowServer(workflow_store=store)\n            server.add_workflow(\n                \"interactive\", InteractiveWorkflow(), additional_events=[ExternalEvent]\n            )\n            return server\n\n    elif store_type == \"agent_data\":\n        backend = FakeAgentDataBackend()\n\n        def make_server() -> WorkflowServer:\n            store = create_agent_data_store(backend, monkeypatch)\n            server = WorkflowServer(workflow_store=store)\n            server.add_workflow(\n                \"interactive\", InteractiveWorkflow(), additional_events=[ExternalEvent]\n            )\n            return server\n\n    else:\n        postgres_container = request.getfixturevalue(\"postgres_container\")\n        dsn = get_asyncpg_dsn(postgres_container)\n\n        def make_server() -> WorkflowServer:\n            pg_store = PostgresWorkflowStore(dsn=dsn)\n            server = WorkflowServer(workflow_store=pg_store)\n            server.add_workflow(\n                \"interactive\", InteractiveWorkflow(), additional_events=[ExternalEvent]\n            )\n            return server\n\n    return make_server\n\n\n@pytest.mark.asyncio\nasync def test_server_restart_resumes_workflow(\n    durable_server_factory: Callable[[], WorkflowServer],\n) -> None:\n    # Start workflow, then stop server\n    async with live_server(durable_server_factory) as (base_url, _server):\n        client = WorkflowClient(base_url=base_url)\n        started = await client.run_workflow_nowait(\"interactive\")\n        handler_id = started.handler_id\n\n        await wait_for_requested_external_event_stream(\n            client, handler_id, label=\"durable-restart\"\n        )\n\n    # Restart with same store - workflow should resume\n    async with live_server(durable_server_factory) as (base_url2, _server2):\n        client2 = WorkflowClient(base_url=base_url2)\n\n        # Send the event to complete the workflow\n        sent = await client2.send_event(\n            handler_id, ExternalEvent(response=\"after_restart\")\n        )\n        assert sent.status == \"sent\"\n\n        async def check_completed() -> None:\n            data = await client2.get_handler(handler_id)\n            assert data.status == \"completed\"\n            assert data.result is not None\n            assert data.result.value.get(\"result\") == \"received: after_restart\"\n\n        await wait_for_passing(check_completed)\n\n\n@pytest.mark.asyncio\nasync def test_idle_release_and_reload(\n    durable_server_factory: Callable[[], WorkflowServer],\n) -> None:\n    async with live_server(durable_server_factory) as (base_url, _server):\n        client = WorkflowClient(base_url=base_url)\n        started = await client.run_workflow_nowait(\"interactive\")\n        handler_id = started.handler_id\n\n        # Wait for handler to be running (idle internally)\n        async def is_running() -> None:\n            data = await client.get_handler(handler_id)\n            assert data.status == \"running\"\n\n        await wait_for_passing(is_running, max_duration=3.0)\n\n        # Give time for idle release to happen\n        await asyncio.sleep(0.5)\n\n        # Send event via HTTP to trigger reload from idle state\n        sent = await client.send_event(handler_id, ExternalEvent(response=\"after_idle\"))\n        assert sent.status == \"sent\"\n\n        async def check_completed() -> None:\n            data = await client.get_handler(handler_id)\n            assert data.status == \"completed\"\n            assert data.result is not None\n            assert data.result.value.get(\"result\") == \"received: after_idle\"\n\n        await wait_for_passing(check_completed)\n"
  },
  {
    "path": "packages/llama-agents-integration-tests/tests/test_state_store_matrix.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"State store matrix tests - testing StateStore implementations.\n\nTests the StateStore protocol across InMemoryStateStore and SqlStateStore\n(with both SQLite and PostgreSQL engines) to ensure consistent behavior.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport sqlite3\nfrom pathlib import Path\nfrom typing import Any, AsyncGenerator, Generator\n\nimport asyncpg\nimport pytest\nfrom llama_agents.server._store.postgres_state_store import PostgresStateStore\nfrom llama_agents.server._store.sqlite.migrate import run_migrations\nfrom llama_agents.server._store.sqlite.sqlite_state_store import SqliteStateStore\nfrom llama_agents_integration_tests.fake_agent_data import (\n    FakeAgentDataBackend,\n    create_agent_data_state_store,\n)\nfrom pydantic import (\n    BaseModel,\n    ConfigDict,\n    ValidationError,\n    field_serializer,\n    field_validator,\n)\nfrom testcontainers.postgres import PostgresContainer\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import DictState, InMemoryStateStore, StateStore\n\n# -- Custom state types for testing --\n\n\nclass MyRandomObject:\n    \"\"\"Non-Pydantic object that requires custom serialization.\"\"\"\n\n    def __init__(self, name: str) -> None:\n        self.name = name\n\n\nclass PydanticObject(BaseModel):\n    \"\"\"Simple Pydantic model for nested state testing.\"\"\"\n\n    name: str\n\n\nclass MyState(BaseModel):\n    \"\"\"Custom typed state with serialization logic.\"\"\"\n\n    model_config = ConfigDict(\n        arbitrary_types_allowed=True,\n        validate_assignment=True,\n        strict=True,\n    )\n\n    my_obj: MyRandomObject\n    pydantic_obj: PydanticObject\n    name: str\n    age: int\n\n    @field_serializer(\"my_obj\", when_used=\"always\")\n    def serialize_my_obj(self, my_obj: MyRandomObject) -> str:\n        return my_obj.name\n\n    @field_validator(\"my_obj\", mode=\"before\")\n    @classmethod\n    def deserialize_my_obj(cls, v: str | MyRandomObject) -> MyRandomObject:\n        if isinstance(v, MyRandomObject):\n            return v\n        if isinstance(v, str):\n            return MyRandomObject(v)\n        raise ValueError(f\"Invalid type for my_obj: {type(v)}\")\n\n\n# -- Fixtures --\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_container() -> Generator[PostgresContainer, None, None]:\n    \"\"\"Module-scoped PostgreSQL container for state store tests.\n\n    Requires Docker to be running.\n    \"\"\"\n    with PostgresContainer(\"postgres:16\", driver=None) as postgres:\n        yield postgres\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_dsn(\n    postgres_container: PostgresContainer,\n) -> str:\n    \"\"\"Module-scoped PostgreSQL DSN for state store tests.\"\"\"\n    connection_url = postgres_container.get_connection_url()\n    if \"postgresql+psycopg2://\" in connection_url:\n        connection_url = connection_url.replace(\n            \"postgresql+psycopg2://\", \"postgresql://\"\n        )\n    elif \"postgresql+psycopg://\" in connection_url:\n        connection_url = connection_url.replace(\n            \"postgresql+psycopg://\", \"postgresql://\"\n        )\n    return connection_url\n\n\nasync def _create_postgres_pool(dsn: str) -> asyncpg.Pool:\n    \"\"\"Create a pool and ensure schema/table exist.\"\"\"\n    pool = await asyncpg.create_pool(dsn=dsn)\n    assert pool is not None\n    async with pool.acquire() as conn:\n        await conn.execute(\"CREATE SCHEMA IF NOT EXISTS dbos\")\n        await conn.execute(\"\"\"\n            CREATE TABLE IF NOT EXISTS dbos.workflow_state (\n                run_id VARCHAR(255) PRIMARY KEY,\n                state_json TEXT NOT NULL,\n                state_type VARCHAR(255),\n                state_module VARCHAR(255),\n                created_at TIMESTAMPTZ,\n                updated_at TIMESTAMPTZ\n            )\n        \"\"\")\n    return pool\n\n\n@pytest.fixture(scope=\"module\")\ndef sqlite_db_path(\n    tmp_path_factory: pytest.TempPathFactory,\n) -> str:\n    \"\"\"Module-scoped SQLite database path for state store tests.\"\"\"\n    db_file: Path = tmp_path_factory.mktemp(\"state_store\") / \"test.sqlite3\"\n    db_path = str(db_file)\n\n    # Run migrations to create tables\n    conn = sqlite3.connect(db_path)\n    try:\n        run_migrations(conn)\n        conn.commit()\n    finally:\n        conn.close()\n\n    return db_path\n\n\ndef _get_store_params() -> list[Any]:\n    \"\"\"Get store type parameters for the test matrix.\"\"\"\n    return [\n        pytest.param(\"in_memory\", id=\"in_memory\"),\n        pytest.param(\"sqlite\", id=\"sqlite\"),\n        pytest.param(\"postgres\", marks=pytest.mark.docker, id=\"postgres\"),\n        pytest.param(\"agent_data\", id=\"agent_data\"),\n    ]\n\n\ndef _get_sql_params() -> list[Any]:\n    \"\"\"Get SQL-only backend parameters for persistence/isolation tests.\"\"\"\n    return [\n        pytest.param(\"sqlite\", id=\"sqlite\"),\n        pytest.param(\"postgres\", marks=pytest.mark.docker, id=\"postgres\"),\n    ]\n\n\n@pytest.fixture(params=_get_store_params())\nasync def state_store(\n    request: pytest.FixtureRequest,\n    sqlite_db_path: str,\n    monkeypatch: pytest.MonkeyPatch,\n) -> AsyncGenerator[StateStore[DictState], None]:\n    \"\"\"Parametrized fixture yielding a fresh StateStore for each test.\"\"\"\n    # Use unique run_id per test to avoid state bleeding\n    run_id = f\"test-{id(request)}\"\n\n    if request.param == \"in_memory\":\n        yield InMemoryStateStore(DictState())\n    elif request.param == \"sqlite\":\n        store = SqliteStateStore(db_path=sqlite_db_path, run_id=run_id)\n        yield store\n    elif request.param == \"postgres\":\n        dsn: str = request.getfixturevalue(\"postgres_dsn\")\n        pool = await _create_postgres_pool(dsn)\n        store = PostgresStateStore(pool=pool, run_id=run_id, schema=\"dbos\")\n        yield store\n        await pool.close()\n    elif request.param == \"agent_data\":\n        yield create_agent_data_state_store(\n            FakeAgentDataBackend(), monkeypatch, run_id=run_id\n        )\n\n\n@pytest.fixture(params=_get_sql_params())\nasync def sql_store_factory(\n    request: pytest.FixtureRequest,\n    sqlite_db_path: str,\n) -> AsyncGenerator[tuple[str, str | None, asyncpg.Pool | None], None]:\n    \"\"\"Parametrized fixture yielding (backend, db_path_or_schema, pool) for SQL backend tests.\"\"\"\n    if request.param == \"sqlite\":\n        yield \"sqlite\", sqlite_db_path, None\n    elif request.param == \"postgres\":\n        dsn: str = request.getfixturevalue(\"postgres_dsn\")\n        pool = await _create_postgres_pool(dsn)\n        yield \"postgres\", \"dbos\", pool\n        await pool.close()\n\n\n@pytest.fixture(params=_get_store_params())\nasync def custom_state_store(\n    request: pytest.FixtureRequest,\n    sqlite_db_path: str,\n    monkeypatch: pytest.MonkeyPatch,\n) -> AsyncGenerator[StateStore[MyState], None]:\n    \"\"\"Parametrized fixture yielding a StateStore with custom typed state.\"\"\"\n    run_id = f\"test-custom-{id(request)}\"\n    initial_state = MyState(\n        my_obj=MyRandomObject(\"llama-index\"),\n        pydantic_obj=PydanticObject(name=\"llama-index\"),\n        name=\"John\",\n        age=30,\n    )\n\n    if request.param == \"in_memory\":\n        yield InMemoryStateStore(initial_state)\n    elif request.param == \"sqlite\":\n        store = SqliteStateStore(\n            db_path=sqlite_db_path,\n            run_id=run_id,\n            state_type=MyState,\n        )\n        await store.set_state(initial_state)\n        yield store\n    elif request.param == \"postgres\":\n        dsn: str = request.getfixturevalue(\"postgres_dsn\")\n        pool = await _create_postgres_pool(dsn)\n        store = PostgresStateStore(\n            pool=pool,\n            run_id=run_id,\n            state_type=MyState,\n            schema=\"dbos\",\n        )\n        await store.set_state(initial_state)\n        yield store\n        await pool.close()\n    elif request.param == \"agent_data\":\n        store = create_agent_data_state_store(\n            FakeAgentDataBackend(), monkeypatch, run_id=run_id, state_type=MyState\n        )\n        await store.set_state(initial_state)\n        yield store\n\n\n# -- Basic Operations Tests --\n\n\n@pytest.mark.asyncio\nasync def test_get_set_basic_values(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test basic get/set operations with simple values.\"\"\"\n    await state_store.set(\"name\", \"John\")\n    await state_store.set(\"age\", 30)\n\n    assert await state_store.get(\"name\") == \"John\"\n    assert await state_store.get(\"age\") == 30\n\n\n@pytest.mark.asyncio\nasync def test_get_with_default(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test get with default value for missing keys.\"\"\"\n    result = await state_store.get(\"nonexistent\", default=None)\n    assert result is None\n\n    result = await state_store.get(\"missing\", default=\"fallback\")\n    assert result == \"fallback\"\n\n\n@pytest.mark.asyncio\nasync def test_get_missing_raises(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test that get raises ValueError for missing key without default.\"\"\"\n    with pytest.raises(ValueError, match=\"not found\"):\n        await state_store.get(\"nonexistent\")\n\n\n@pytest.mark.asyncio\nasync def test_nested_get_set(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test nested path access with dot notation.\"\"\"\n    await state_store.set(\"nested\", {\"a\": \"b\"})\n    assert await state_store.get(\"nested.a\") == \"b\"\n\n    await state_store.set(\"nested.a\", \"c\")\n    assert await state_store.get(\"nested.a\") == \"c\"\n\n\n@pytest.mark.asyncio\nasync def test_get_state_returns_copy(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test that get_state returns a copy, not the original.\"\"\"\n    await state_store.set(\"value\", 1)\n\n    state1 = await state_store.get_state()\n    state2 = await state_store.get_state()\n\n    # Should be equal but not the same object\n    assert state1.model_dump() == state2.model_dump()\n\n\n@pytest.mark.asyncio\nasync def test_set_state_replaces(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test that set_state replaces the entire state.\"\"\"\n    await state_store.set(\"old_key\", \"old_value\")\n\n    new_state = DictState()\n    new_state[\"new_key\"] = \"new_value\"\n    await state_store.set_state(new_state)\n\n    assert await state_store.get(\"new_key\") == \"new_value\"\n    # Old key should be gone or inaccessible\n    result = await state_store.get(\"old_key\", default=None)\n    assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_clear_resets_state(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test that clear resets to default state.\"\"\"\n    await state_store.set(\"name\", \"Jane\")\n    await state_store.set(\"age\", 25)\n\n    await state_store.clear()\n\n    assert await state_store.get(\"name\", default=None) is None\n    assert await state_store.get(\"age\", default=None) is None\n\n\n# -- edit_state Context Manager Tests --\n\n\n@pytest.mark.asyncio\nasync def test_edit_state_basic(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test basic edit_state context manager usage.\"\"\"\n    await state_store.set(\"counter\", 0)\n\n    async with state_store.edit_state() as state:\n        current = state.get(\"counter\", 0)\n        state[\"counter\"] = current + 1\n\n    assert await state_store.get(\"counter\") == 1\n\n\n@pytest.mark.asyncio\nasync def test_edit_state_multiple_changes(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test multiple changes within a single edit_state.\"\"\"\n    async with state_store.edit_state() as state:\n        state[\"a\"] = 1\n        state[\"b\"] = 2\n        state[\"c\"] = {\"nested\": \"value\"}\n\n    assert await state_store.get(\"a\") == 1\n    assert await state_store.get(\"b\") == 2\n    assert await state_store.get(\"c.nested\") == \"value\"\n\n\n@pytest.mark.asyncio\nasync def test_edit_state_exception_handling(\n    state_store: StateStore[DictState],\n) -> None:\n    \"\"\"Test that exceptions in edit_state don't corrupt state.\"\"\"\n    await state_store.set(\"value\", \"original\")\n\n    with pytest.raises(ValueError, match=\"intentional\"):\n        async with state_store.edit_state() as state:\n            state[\"value\"] = \"modified\"\n            raise ValueError(\"intentional error\")\n\n    # State should remain unchanged after exception\n    # Note: behavior may vary - InMemory commits on context exit, SQL rolls back\n    # This test documents the expected behavior\n\n\n# -- Custom Typed State Tests --\n\n\n@pytest.mark.asyncio\nasync def test_custom_state_type(custom_state_store: StateStore[MyState]) -> None:\n    \"\"\"Test state store with custom Pydantic model.\"\"\"\n    state = await custom_state_store.get_state()\n    assert isinstance(state, MyState)\n    assert state.name == \"John\"\n    assert state.age == 30\n    assert state.my_obj.name == \"llama-index\"\n\n\n@pytest.mark.asyncio\nasync def test_custom_state_set_values(custom_state_store: StateStore[MyState]) -> None:\n    \"\"\"Test setting values on custom typed state.\"\"\"\n    await custom_state_store.set(\"name\", \"Jane\")\n    await custom_state_store.set(\"age\", 25)\n\n    assert await custom_state_store.get(\"name\") == \"Jane\"\n    assert await custom_state_store.get(\"age\") == 25\n\n    # Original custom fields should still be accessible\n    state = await custom_state_store.get_state()\n    assert state.my_obj.name == \"llama-index\"\n\n\n@pytest.mark.asyncio\nasync def test_custom_state_validation(custom_state_store: StateStore[MyState]) -> None:\n    \"\"\"Test that Pydantic validation is enforced on custom state.\"\"\"\n    # MyState has strict=True, so setting age to string should fail\n    with pytest.raises(ValidationError):\n        await custom_state_store.set(\"age\", \"not a number\")\n\n\n# -- Serialization Tests --\n\n\n@pytest.mark.asyncio\nasync def test_to_dict_from_dict_roundtrip(state_store: StateStore[DictState]) -> None:\n    \"\"\"Test serialization roundtrip with to_dict/from_dict.\"\"\"\n    await state_store.set(\"name\", \"John\")\n    await state_store.set(\"age\", 30)\n\n    serializer = JsonSerializer()\n    data = state_store.to_dict(serializer)\n\n    # For InMemoryStateStore, from_dict restores the full state\n    # For SqlStateStore, from_dict returns metadata (engine must be set separately)\n    if isinstance(state_store, InMemoryStateStore):\n        restored = InMemoryStateStore.from_dict(data, serializer)\n        assert await restored.get(\"name\") == \"John\"\n        assert await restored.get(\"age\") == 30\n\n\n# -- SQL Backend Tests (parameterized across SQLite and PostgreSQL) --\n\n\n@pytest.mark.asyncio\nasync def test_sql_persistence(\n    sql_store_factory: tuple[str, str, asyncpg.Pool | None],\n) -> None:\n    \"\"\"Test that state persists across store instances.\"\"\"\n    backend, db_path_or_schema, pool = sql_store_factory\n    run_id = \"persistence-test\"\n\n    if backend == \"sqlite\":\n        store1 = SqliteStateStore(db_path=db_path_or_schema, run_id=run_id)\n        await store1.set(\"persistent_key\", \"persistent_value\")\n        store2 = SqliteStateStore(db_path=db_path_or_schema, run_id=run_id)\n    else:\n        assert pool is not None\n        store1 = PostgresStateStore(pool=pool, run_id=run_id, schema=db_path_or_schema)\n        await store1.set(\"persistent_key\", \"persistent_value\")\n        store2 = PostgresStateStore(pool=pool, run_id=run_id, schema=db_path_or_schema)\n\n    result = await store2.get(\"persistent_key\")\n    assert result == \"persistent_value\"\n\n\n@pytest.mark.asyncio\nasync def test_sql_isolation(\n    sql_store_factory: tuple[str, str, asyncpg.Pool | None],\n) -> None:\n    \"\"\"Test that different run_ids have isolated state.\"\"\"\n    backend, db_path_or_schema, pool = sql_store_factory\n\n    if backend == \"sqlite\":\n        store1 = SqliteStateStore(db_path=db_path_or_schema, run_id=\"run-1\")\n        store2 = SqliteStateStore(db_path=db_path_or_schema, run_id=\"run-2\")\n    else:\n        assert pool is not None\n        store1 = PostgresStateStore(pool=pool, run_id=\"run-1\", schema=db_path_or_schema)\n        store2 = PostgresStateStore(pool=pool, run_id=\"run-2\", schema=db_path_or_schema)\n\n    await store1.set(\"key\", \"value1\")\n    await store2.set(\"key\", \"value2\")\n\n    assert await store1.get(\"key\") == \"value1\"\n    assert await store2.get(\"key\") == \"value2\"\n\n\n@pytest.mark.asyncio\nasync def test_sql_concurrent_edits(\n    sql_store_factory: tuple[str, str, asyncpg.Pool | None],\n) -> None:\n    \"\"\"Test concurrent edit_state calls are serialized correctly.\"\"\"\n    backend, db_path_or_schema, pool = sql_store_factory\n    run_id = \"concurrent-test\"\n\n    if backend == \"sqlite\":\n        store = SqliteStateStore(db_path=db_path_or_schema, run_id=run_id)\n    else:\n        assert pool is not None\n        store = PostgresStateStore(pool=pool, run_id=run_id, schema=db_path_or_schema)\n\n    await store.set(\"counter\", 0)\n\n    async def increment() -> None:\n        async with store.edit_state() as state:\n            current = state.get(\"counter\", 0)\n            await asyncio.sleep(0.01)  # Simulate some work\n            state[\"counter\"] = current + 1\n\n    await asyncio.gather(*[increment() for _ in range(5)])\n\n    result = await store.get(\"counter\")\n    assert result == 5\n\n\n@pytest.mark.asyncio\nasync def test_sql_custom_state_persistence(\n    sql_store_factory: tuple[str, str, asyncpg.Pool | None],\n) -> None:\n    \"\"\"Test that custom typed state persists correctly.\"\"\"\n    backend, db_path_or_schema, pool = sql_store_factory\n    run_id = \"custom-persistence-test\"\n\n    initial_state = MyState(\n        my_obj=MyRandomObject(\"persisted\"),\n        pydantic_obj=PydanticObject(name=\"persisted\"),\n        name=\"Original\",\n        age=100,\n    )\n\n    if backend == \"sqlite\":\n        store1 = SqliteStateStore(\n            db_path=db_path_or_schema,\n            run_id=run_id,\n            state_type=MyState,\n        )\n        await store1.set_state(initial_state)\n        await store1.set(\"name\", \"Modified\")\n        store2 = SqliteStateStore(\n            db_path=db_path_or_schema,\n            run_id=run_id,\n            state_type=MyState,\n        )\n    else:\n        assert pool is not None\n        store1 = PostgresStateStore(\n            pool=pool,\n            run_id=run_id,\n            state_type=MyState,\n            schema=db_path_or_schema,\n        )\n        await store1.set_state(initial_state)\n        await store1.set(\"name\", \"Modified\")\n        store2 = PostgresStateStore(\n            pool=pool,\n            run_id=run_id,\n            state_type=MyState,\n            schema=db_path_or_schema,\n        )\n\n    state = await store2.get_state()\n\n    assert state.name == \"Modified\"\n    assert state.my_obj.name == \"persisted\"\n\n\n# -- PostgreSQL-Specific Tests --\n\n\n@pytest.mark.docker\n@pytest.mark.asyncio\nasync def test_postgres_uses_dbos_schema(postgres_dsn: str) -> None:\n    \"\"\"Test that PostgresStateStore with schema='dbos' uses the table in the dbos schema.\"\"\"\n    pool = await _create_postgres_pool(postgres_dsn)\n    try:\n        run_id = \"pg-schema-test\"\n        store = PostgresStateStore(pool=pool, run_id=run_id, schema=\"dbos\")\n\n        await store.set(\"test\", \"value\")\n\n        async with pool.acquire() as conn:\n            exists = await conn.fetchval(\n                \"\"\"\n                SELECT EXISTS (\n                    SELECT FROM information_schema.tables\n                    WHERE table_schema = 'dbos'\n                    AND table_name = 'workflow_state'\n                )\n                \"\"\"\n            )\n            assert exists is True\n    finally:\n        await pool.close()\n"
  },
  {
    "path": "packages/llama-agents-server/CHANGELOG.md",
    "content": "# llama-agents-server\n\n## 0.5.0\n\n### Minor Changes\n\n- 95d8c2b: Share a single asyncpg pool across DBOSRuntime, PostgresWorkflowStore, and ExecutorLeaseManager instead of each opening their own. Pool size is configurable via `DBOSRuntimeConfig.pool_size`. Also adds LISTEN reconnect with backoff to PostgresWorkflowStore.\n\n## 0.4.7\n\n### Patch Changes\n\n- 9bf247a: Classify zombie handlers on resume and collapse duplicate handler rows on write.\n- Updated dependencies [9bf247a]\n- Updated dependencies [2cc9fae]\n  - llama-index-workflows@2.20.0\n  - llama-agents-client@0.3.7\n\n## 0.4.6\n\n### Patch Changes\n\n- f7e037e: Stream ticks during resume so peak memory is bounded by batch size rather than total tick history.\n- 60cd349: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.39/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.39/dist/app.css\n\n- Updated dependencies [f7e037e]\n  - llama-index-workflows@2.19.1\n  - llama-agents-client@0.3.6\n\n## 0.4.5\n\n### Patch Changes\n\n- Updated dependencies [2592c80]\n  - llama-index-workflows@2.19.0\n  - llama-agents-client@0.3.5\n\n## 0.4.4\n\n### Patch Changes\n\n- Updated dependencies [43ff242]\n  - llama-index-workflows@2.18.0\n  - llama-agents-client@0.3.4\n\n## 0.4.3\n\n### Patch Changes\n\n- 12bda18: Ensure single_connection=True prevents locking from happening\n\n## 0.4.2\n\n### Patch Changes\n\n- 3850844: Support single connection mode for sqlite\n\n## 0.4.1\n\n### Patch Changes\n\n- 9f52f40: Make the sqlite db more resliant to locking\n\n## 0.4.0\n\n### Minor Changes\n\n- 391f287: Make the context API opt-in via `accept_context_api=True` on `WorkflowServer`.\n\n### Patch Changes\n\n- 3432a83: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.36/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.36/dist/app.css\n\n- Updated dependencies [b8c7c7e]\n  - llama-index-workflows@2.17.3\n  - llama-agents-client@0.3.3\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies [7e06f87]\n  - llama-index-workflows@2.17.2\n  - llama-agents-client@0.3.2\n\n## 0.3.2\n\n### Patch Changes\n\n- e7da58b: Log workflow failures and timeouts\n\n## 0.3.1\n\n### Patch Changes\n\n- 5776bd1: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.35/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.35/dist/app.css\n\n- Updated dependencies [979d68b]\n- Updated dependencies [983f6f6]\n  - llama-index-workflows@2.17.1\n  - llama-agents-client@0.3.1\n\n## 0.3.0\n\n### Minor Changes\n\n- b32ec53: Drop python 3.9 support\n\n### Patch Changes\n\n- 7049e23: Add SSE heartbeat to prevent idle closed connections\n- Updated dependencies [7fc1aae]\n- Updated dependencies [b32ec53]\n  - llama-index-workflows@2.17.0\n  - llama-agents-client@0.3.0\n\n## 0.2.3\n\n### Patch Changes\n\n- 703ec92: Improve agent data store step and state performance\n- ccd0db6: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.34/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.34/dist/app.css\n\n- Updated dependencies [c7bbedb]\n- Updated dependencies [703ec92]\n  - llama-index-workflows@2.16.1\n  - llama-agents-client@0.2.3\n\n## 0.2.2\n\n### Patch Changes\n\n- 9c0b4a0: Fix performance issues during heavy streaming from excessive chatter\n- 5e7f9e5: Namespace handler_id instrument tag as `llamaindex.handler_id`.\n- Updated dependencies [5e7f9e5]\n- Updated dependencies [9f26314]\n  - llama-index-workflows@2.16.0\n  - llama-agents-client@0.2.2\n\n## 0.2.1\n\n### Patch Changes\n\n- 6605457: Bump dependency requirements\n- Updated dependencies [6605457]\n- Updated dependencies [6ec262c]\n  - llama-agents-client@0.2.1\n  - llama-index-workflows@2.15.1\n\n## 0.2.0\n\n### Minor Changes\n\n- d61646f: Add max_completed history cap to MemoryWorkflowStore in order to control memory consumption\n- 18a5d68: Refactor server internals from monolithic handler to composable runtime decorators (ServerRuntimeDecorator, PersistenceDecorator, IdleReleaseDecorator) enabling pluggable server runtimes\n- 6bccda7: Add AgentDataStore backed by LlamaCloud Agent Data API\n- 4ba29dc: Add tick storage, event storage with SSE subscription, per-run state stores, and centralized handler status transitions to AbstractWorkflowStore and SQLite/memory implementations\n\n### Patch Changes\n\n- 77a3f9c: Add workflow release for idle DBOS workflows (with replica support)\n- 96e437e: Move task execution into the runtime, for maximal control of specific runtime semantics around determinism\n- 23385c7: Add better 500 error logging and structured responses\n- Updated dependencies [4ba29dc]\n- Updated dependencies [77a3f9c]\n- Updated dependencies [62ffc15]\n- Updated dependencies [707a254]\n- Updated dependencies [05f5f4e]\n- Updated dependencies [3c22216]\n- Updated dependencies [96e437e]\n- Updated dependencies [23385c7]\n  - llama-agents-client@0.2.0\n  - llama-index-workflows@2.15.0\n\n## 0.2.0-rc.3\n\n### Minor Changes\n\n- c1fbb8f: Add max_completed history cap to MemoryWorkflowStore in order to control memory consumption\n- 281d441: Add AgentDataStore backed by LlamaCloud Agent Data API\n\n### Patch Changes\n\n- 3720c61: Add workflow release for idle DBOS workflows (with replica support)\n- a2aad32: Move task execution into the runtime, for maximal control of specific runtime semantics around determinism\n- Updated dependencies [3720c61]\n- Updated dependencies [8762129]\n- Updated dependencies [a2aad32]\n  - llama-index-workflows@2.15.0-rc.1\n  - llama-agents-client@0.2.0-rc.1\n\n## 0.2.0-rc.2\n\n### Minor Changes\n\n- 6ccdebd: Refactor server internals from monolithic handler to composable runtime decorators (ServerRuntimeDecorator, PersistenceDecorator, IdleReleaseDecorator) enabling pluggable server runtimes\n\n## 0.2.0-rc.1\n\n### Minor Changes\n\n- 528d562: Add tick storage, event storage with SSE subscription, per-run state stores, and centralized handler status transitions to AbstractWorkflowStore and SQLite/memory implementations\n\n### Patch Changes\n\n- Updated dependencies [528d562]\n- Updated dependencies [e981f73]\n- Updated dependencies [b515a46]\n- Updated dependencies [7433d4c]\n  - llama-agents-client@0.2.0-rc.0\n  - llama-index-workflows@2.15.0-rc.0\n\n## 0.2.0-rc.0\n\n### Minor Changes\n\n- 06cca76: Test pre-release functioning\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [3590913]\n- Updated dependencies [7433d4c]\n  - llama-index-workflows@2.14.2\n  - llama-agents-client@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- ef7f808: Fix OpenAPI schema version to use current server package, not workflows core\n- Updated dependencies [6ece797]\n  - llama-index-workflows@2.14.1\n  - llama-agents-client@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- db90f89: Separate server/client to their own packages under a llama_agents namespace\n- 45e7614: Refact: make control loop more deterministic\n\n  - Switches out the asyncio delay mechanism for a pull-with-timeout that is more deterministic friendly\n  - Adds a priority queue of delayed tasks\n  - Switches out the misc firing /spawning of async tasks to a more rigorous pattern where tasks are only created in the main loop, and gathered in one location. This makes the concurrency more straightforward to reason about\n\n- Updated dependencies [db90f89]\n- Updated dependencies [73c1254]\n- Updated dependencies [45e7614]\n- Updated dependencies [45e7614]\n- Updated dependencies [2900f58]\n- Updated dependencies [6fdc45c]\n  - llama-agents-client@0.1.1\n  - llama-index-workflows@2.14.0\n"
  },
  {
    "path": "packages/llama-agents-server/README.md",
    "content": "# LlamaAgents Server\n\nHTTP server for deploying [LlamaIndex Workflows](https://pypi.org/project/llama-index-workflows/) as web services. Built on Starlette and Uvicorn.\n\n## Installation\n\n```bash\npip install llama-agents-server\n```\n\n## Quick Start\n\nCreate a server file (e.g., `my_server.py`):\n\n```python\nimport asyncio\nfrom workflows import Workflow, step\nfrom workflows.context import Context\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_agents.server import WorkflowServer\n\nclass StreamEvent(Event):\n    sequence: int\n\nclass GreetingWorkflow(Workflow):\n    @step\n    async def greet(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        for i in range(3):\n            ctx.write_event_to_stream(StreamEvent(sequence=i))\n        name = ev.get(\"name\", \"World\")\n        return StopEvent(result=f\"Hello, {name}!\")\n\nserver = WorkflowServer()\nserver.add_workflow(\"greet\", GreetingWorkflow())\n\nif __name__ == \"__main__\":\n    asyncio.run(server.serve(\"0.0.0.0\", 8080))\n```\n\nOr run it with the CLI:\n\n```bash\nllama-agents-server my_server.py\n```\n\n## Features\n\n- REST API for running, streaming, and managing workflows\n- Debugger UI automatically mounted at `/` for visualizing and debugging workflows\n- Event streaming via newline-delimited JSON or Server-Sent Events\n- Human-in-the-loop support for interactive workflows\n- Persistence with built-in SQLite store (or bring your own via `AbstractWorkflowStore`)\n\n## Client\n\nUse [`llama-agents-client`](https://pypi.org/project/llama-agents-client/) to interact with deployed servers programmatically.\n\n## Documentation\n\nSee the full [deployment guide](https://developers.llamaindex.ai/python/llamaagents/workflows/deployment/) for API details, persistence configuration, and more.\n"
  },
  {
    "path": "packages/llama-agents-server/package.json",
    "content": "{\n  \"name\": \"llama-agents-server\",\n  \"version\": \"0.5.0\",\n  \"private\": false,\n  \"license\": \"MIT\",\n  \"scripts\": {},\n  \"dependencies\": {\n    \"llama-index-workflows\": \"workspace:*\",\n    \"llama-agents-client\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/llama-agents-server/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.9.6,<0.10.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"hatch>=1.14.1\",\n  \"pyyaml>=6.0.2\",\n  \"pytest>=8.4.2\",\n  \"pytest-asyncio>=1.0.0\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"time-machine>=2.19.0,<3.0.0\",\n  \"llama-agents-integration-tests\",\n  \"asyncpg>=0.29.0\",\n  \"testcontainers[postgres]>=4.0.0\"\n]\n\n[project]\nname = \"llama-agents-server\"\nversion = \"0.5.0\"\ndescription = \"HTTP server for deploying and serving LlamaIndex workflows as web services\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n  \"llama-index-workflows>=2.20.0,<3.0.0\",\n  \"llama-agents-client>=0.2.0\",\n  \"starlette>=0.39.0\",\n  \"uvicorn>=0.32.0\",\n  \"httpx>=0.27.0\"\n]\n\n[project.optional-dependencies]\nasyncpg = [\"asyncpg>=0.29.0\"]\n\n[project.scripts]\nllama-agents-server = \"llama_agents.server.__main__:run_server\"\n\n[tool.hatch.envs.default]\ndependencies = [\"pyyaml>=6.0.2\"]\n\n[tool.hatch.envs.default.scripts]\nopenapi = \"python -m llama_agents.server.server --output openapi.json\"\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"module\"\nasyncio_default_test_loop_scope = \"module\"\ntestpaths = [\"tests\"]\naddopts = \"-nauto --timeout=60 -m 'not docker'\"\nmarkers = [\n  \"docker: marks tests as requiring Docker (testcontainers/PostgreSQL)\"\n]\nfilterwarnings = [\n  \"ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning\"\n]\n\n[tool.uv.build-backend]\nmodule-name = \"llama_agents.server\"\n\n[tool.uv.sources]\nllama-index-workflows = {workspace = true}\nllama-agents-client = {workspace = true}\nllama-agents-integration-tests = {workspace = true}\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom ._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    PersistentHandler,\n)\nfrom ._store.agent_data_store import AgentDataStore\nfrom ._store.memory_workflow_store import MemoryWorkflowStore\nfrom ._store.sqlite.sqlite_workflow_store import SqliteWorkflowStore\nfrom .server import WorkflowServer\n\n__all__ = [\n    \"AgentDataStore\",\n    \"AbstractWorkflowStore\",\n    \"HandlerQuery\",\n    \"PersistentHandler\",\n    \"WorkflowServer\",\n    \"MemoryWorkflowStore\",\n    \"SqliteWorkflowStore\",\n]\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/__main__.py",
    "content": "import argparse\nimport importlib.util\nimport os\nimport sys\nfrom pathlib import Path\n\nimport uvicorn\n\nfrom .server import WorkflowServer\n\n\ndef run_server() -> None:\n    parser = argparse.ArgumentParser(description=\"Start the workflows server\")\n    parser.add_argument(\"file_path\", nargs=\"?\", help=\"Path to server application\")\n    args = parser.parse_args()\n\n    if not args.file_path:\n        usage = \"Usage: python -m workflows.server <path_to_server_script>\"\n        print(usage, file=sys.stderr)\n        sys.exit(1)\n\n    file_path = Path(args.file_path)\n    if not file_path.exists():\n        print(f\"Error: File '{file_path}' not found\", file=sys.stderr)\n        sys.exit(1)\n\n    if not file_path.is_file():\n        print(f\"Error: '{file_path}' is not a file\", file=sys.stderr)\n        sys.exit(1)\n\n    file_path = file_path.resolve()\n    module_name = file_path.stem\n\n    try:\n        # Load the module dynamically\n        spec = importlib.util.spec_from_file_location(module_name, file_path)\n        if spec is None or spec.loader is None:\n            raise ValueError(f\"Unable to get spec from module {module_name}\")\n\n        module = importlib.util.module_from_spec(spec)\n        spec.loader.exec_module(module)\n\n        # Find a variable of type WorkflowServer\n        server = None\n        for attr_name in dir(module):\n            attr_value = getattr(module, attr_name)\n            if isinstance(attr_value, WorkflowServer):\n                server = attr_value\n                break\n\n        if server is None:\n            print(\n                f\"Error: No WorkflowServer instance found in '{args.file_path}'\",\n                file=sys.stderr,\n            )\n            sys.exit(1)\n\n        # At this point, a WorkflowServer instance is guaranteed to exist\n        assert server is not None\n\n        host = os.environ.get(\"WORKFLOWS_PY_SERVER_HOST\", \"0.0.0.0\")\n        port = int(os.environ.get(\"WORKFLOWS_PY_SERVER_PORT\", 8080))\n        uvicorn.run(server.app, host=host, port=port)\n\n    except Exception as e:\n        print(f\"Error loading or running server: {e}\", file=sys.stderr)\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    run_server()\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_api.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom contextlib import asynccontextmanager\nfrom importlib.metadata import version\nfrom pathlib import Path\nfrom typing import Any, AsyncGenerator, cast\n\nfrom llama_agents.client.protocol import (\n    CancelHandlerResponse,\n    HandlerData,\n    HandlersListResponse,\n    HealthResponse,\n    SendEventResponse,\n    WorkflowEventsListResponse,\n    WorkflowGraphResponse,\n    WorkflowSchemaResponse,\n)\nfrom llama_agents.client.protocol.serializable_events import (\n    EventEnvelope,\n    EventEnvelopeWithMetadata,\n    EventValidationError,\n)\nfrom starlette.applications import Starlette\nfrom starlette.exceptions import HTTPException\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.cors import CORSMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse, StreamingResponse\nfrom starlette.routing import Route\nfrom starlette.schemas import SchemaGenerator\nfrom starlette.staticfiles import StaticFiles\nfrom workflows import Context, Workflow\nfrom workflows.events import Event, InternalDispatchEvent, StartEvent\nfrom workflows.representation import get_workflow_representation\nfrom workflows.utils import _nanoid as nanoid\n\nfrom ._service import (\n    EventSendError,\n    HandlerCompletedError,\n    HandlerNotFoundError,\n    _WorkflowService,\n)\nfrom ._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    Status,\n    is_terminal_status,\n)\n\nlogger = logging.getLogger(__name__)\n\n\nasync def _http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:\n    return JSONResponse({\"detail\": exc.detail}, status_code=exc.status_code)\n\n\nasync def _unhandled_exception_handler(\n    request: Request, exc: Exception\n) -> JSONResponse:\n    logger.error(\n        \"Unhandled exception on %s %s\", request.method, request.url.path, exc_info=exc\n    )\n    return JSONResponse({\"detail\": f\"Internal server error: {exc}\"}, status_code=500)\n\n\n_DEFAULT_ASSETS_PATH = Path(__file__).parent / \"static\"\n\n\nclass _WorkflowAPI:\n    def __init__(\n        self,\n        service: _WorkflowService,\n        *,\n        middleware: list[Middleware] | None = None,\n        exception_handlers: dict[Any, Any] | None = None,\n        assets_path: Path = _DEFAULT_ASSETS_PATH,\n        sse_heartbeat_interval: float | None = None,\n        accept_context_api: bool = False,\n    ) -> None:\n        self._service = service\n        self._additional_events: dict[str, list[type[Event]]] = {}\n        self._sse_heartbeat_interval = sse_heartbeat_interval\n        self._accept_context_api = accept_context_api\n\n        exception_handlers = exception_handlers or {\n            HTTPException: _http_exception_handler,\n            Exception: _unhandled_exception_handler,\n        }\n\n        middleware = middleware or [\n            Middleware(\n                CORSMiddleware,  # type: ignore[arg-type]\n                # regex echoes the origin header back, which some browsers require (rather than \"*\") when credentials are required\n                allow_origin_regex=\".*\",\n                allow_methods=[\"*\"],\n                allow_headers=[\"*\"],\n                allow_credentials=True,\n            )\n        ]\n\n        @asynccontextmanager\n        async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:\n            await self._service.start()\n            try:\n                yield\n            finally:\n                await self._service.stop()\n\n        self.app = Starlette(\n            routes=self._routes(),\n            middleware=middleware,\n            lifespan=lifespan,\n            exception_handlers=exception_handlers,  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]\n        )\n        self.app.mount(\n            \"/\", app=StaticFiles(directory=assets_path, html=True), name=\"ui\"\n        )\n\n    def register_additional_events(self, name: str, events: list[type[Event]]) -> None:\n        self._additional_events[name] = events\n\n    def get_workflow_events(self, workflow_name: str) -> list[type[Event]]:\n        workflow = self._service.get_workflow(workflow_name)\n        if workflow is None:\n            return []\n        return workflow.events + (self._additional_events.get(workflow_name) or [])\n\n    def event_registry(self, workflow_name: str) -> dict[str, type[Event]]:\n        \"\"\"Return a name→type mapping of events for the given workflow.\"\"\"\n        return {e.__name__: e for e in self.get_workflow_events(workflow_name)}\n\n    def _routes(self) -> list[Route]:\n        return [\n            Route(\"/workflows\", self._list_workflows, methods=[\"GET\"]),\n            Route(\"/workflows/{name}/run\", self._run_workflow, methods=[\"POST\"]),\n            Route(\n                \"/workflows/{name}/run-nowait\",\n                self._run_workflow_nowait,\n                methods=[\"POST\"],\n            ),\n            Route(\n                \"/workflows/{name}/schema\",\n                self._get_events_schema,\n                methods=[\"GET\"],\n            ),\n            Route(\n                \"/results/{handler_id}\",\n                self._get_workflow_result,\n                methods=[\"GET\"],\n            ),\n            Route(\"/events/{handler_id}\", self._stream_events, methods=[\"GET\"]),\n            Route(\"/events/{handler_id}\", self._post_event, methods=[\"POST\"]),\n            Route(\"/health\", self._health_check, methods=[\"GET\"]),\n            Route(\"/handlers\", self._get_handlers, methods=[\"GET\"]),\n            Route(\n                \"/handlers/{handler_id}\",\n                self._get_workflow_handler,\n                methods=[\"GET\"],\n            ),\n            Route(\n                \"/handlers/{handler_id}/cancel\",\n                self._cancel_handler,\n                methods=[\"POST\"],\n            ),\n            Route(\n                \"/workflows/{name}/representation\",\n                self._get_workflow_representation,\n                methods=[\"GET\"],\n            ),\n            Route(\n                \"/workflows/{name}/events\",\n                self._list_workflow_events,\n                methods=[\"GET\"],\n            ),\n        ]\n\n    def openapi_schema(self) -> dict:\n        gen = SchemaGenerator(\n            {\n                \"openapi\": \"3.0.0\",\n                \"info\": {\n                    \"title\": \"Workflows API\",\n                    \"version\": version(\"llama-agents-server\"),\n                },\n                \"components\": {\n                    \"schemas\": {\n                        \"EventEnvelopeWithMetadata\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"value\": {\"type\": \"object\"},\n                                \"types\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}},\n                                \"type\": {\"type\": \"string\"},\n                                \"qualified_name\": {\"type\": \"string\"},\n                            },\n                            \"required\": [\"value\", \"type\"],\n                        },\n                        \"Handler\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"handler_id\": {\"type\": \"string\"},\n                                \"workflow_name\": {\"type\": \"string\"},\n                                \"run_id\": {\"type\": \"string\", \"nullable\": True},\n                                \"status\": {\n                                    \"type\": \"string\",\n                                    \"enum\": [\n                                        \"running\",\n                                        \"completed\",\n                                        \"failed\",\n                                        \"cancelled\",\n                                    ],\n                                },\n                                \"started_at\": {\"type\": \"string\", \"format\": \"date-time\"},\n                                \"updated_at\": {\n                                    \"type\": \"string\",\n                                    \"format\": \"date-time\",\n                                    \"nullable\": True,\n                                },\n                                \"completed_at\": {\n                                    \"type\": \"string\",\n                                    \"format\": \"date-time\",\n                                    \"nullable\": True,\n                                },\n                                \"error\": {\"type\": \"string\", \"nullable\": True},\n                                \"result\": {\n                                    \"description\": \"Workflow result value\",\n                                    \"oneOf\": [\n                                        {\n                                            \"$ref\": \"#/components/schemas/EventEnvelopeWithMetadata\"\n                                        },\n                                        {\"type\": \"null\"},\n                                    ],\n                                },\n                            },\n                            \"required\": [\n                                \"handler_id\",\n                                \"workflow_name\",\n                                \"status\",\n                                \"started_at\",\n                            ],\n                        },\n                        \"HandlersList\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"handlers\": {\n                                    \"type\": \"array\",\n                                    \"items\": {\"$ref\": \"#/components/schemas/Handler\"},\n                                }\n                            },\n                            \"required\": [\"handlers\"],\n                        },\n                    }\n                },\n            }\n        )\n\n        return gen.get_schema(self.app.routes)\n\n    #\n    # HTTP endpoints\n    #\n\n    async def _health_check(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Health check\n        description: Returns the server health status and workflow counts.\n        responses:\n          200:\n            description: Successful health check\n            content:\n              application/json:\n                schema:\n                  type: object\n                  properties:\n                    status:\n                      type: string\n                      example: healthy\n                  required: [status]\n        \"\"\"\n        if not self._service._runtime.is_launched:\n            return JSONResponse(\n                HealthResponse(status=\"unhealthy\").model_dump(),\n                status_code=503,\n            )\n        return JSONResponse(HealthResponse(status=\"healthy\").model_dump())\n\n    async def _list_workflows(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: List workflows\n        description: Returns the list of registered workflow names.\n        responses:\n          200:\n            description: List of workflows\n            content:\n              application/json:\n                schema:\n                  type: object\n                  properties:\n                    workflows:\n                      type: array\n                      items:\n                        type: string\n                  required: [workflows]\n        \"\"\"\n        workflow_names = self._service.get_workflow_names()\n        return JSONResponse({\"workflows\": workflow_names})\n\n    async def _list_workflow_events(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: List workflow events\n        description: Returns the list of registered workflow event schemas.\n        parameters:\n          - in: path\n            name: name\n            required: true\n            schema:\n              type: string\n            description: Registered workflow name.\n        responses:\n          200:\n            description: List of workflow event schemas\n            content:\n              application/json:\n                schema:\n                  type: object\n                  properties:\n                    events:\n                      type: array\n                      description: List of workflow event JSON schemas\n                      items:\n                        type: object\n                  required: [events]\n        \"\"\"\n        if \"name\" not in request.path_params:\n            raise HTTPException(status_code=400, detail=\"name param is required\")\n\n        name = request.path_params[\"name\"]\n        if self._service.get_workflow(name) is None:\n            raise HTTPException(status_code=404, detail=f\"Workflow '{name}' not found\")\n\n        events = self.get_workflow_events(name)\n\n        return JSONResponse(\n            WorkflowEventsListResponse(\n                events=[event.model_json_schema() for event in events]\n            ).model_dump()\n        )\n\n    async def _run_workflow(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Run workflow (wait)\n        description: |\n          Runs the specified workflow synchronously and returns the final result.\n          The request body may include an optional serialized start event and optional\n          keyword arguments passed to the workflow run.\n        parameters:\n          - in: path\n            name: name\n            required: true\n            schema:\n              type: string\n            description: Registered workflow name.\n        requestBody:\n          required: false\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  start_event:\n                    type: object\n                    description: 'Plain JSON object representing the start event (e.g., {\"message\": \"...\"}).'\n                  handler_id:\n                    type: string\n                    description: Workflow handler identifier to continue from a previous completed run.\n                  kwargs:\n                    type: object\n                    description: Additional keyword arguments for the workflow.\n        responses:\n          200:\n            description: Workflow completed successfully\n            content:\n              application/json:\n                schema:\n                  $ref: '#/components/schemas/Handler'\n          400:\n            description: Invalid start_event payload\n          404:\n            description: Workflow or handler identifier not found\n          500:\n            description: Error running workflow or invalid request body\n        \"\"\"\n        workflow = self._extract_workflow(request)\n        context, start_event, handler_id = await self._extract_run_params(\n            request, workflow, workflow.workflow_name\n        )\n\n        if start_event is not None:\n            input_ev = workflow.start_event_class.model_validate(start_event)\n        else:\n            input_ev = None\n\n        try:\n            started = await self._service.start_workflow(\n                workflow=workflow,\n                handler_id=handler_id,\n                context=context,\n                start_event=input_ev,\n            )\n        except Exception as e:\n            logger.error(f\"Error running workflow: {e}\", exc_info=True)\n            raise HTTPException(detail=f\"Error running workflow: {e}\", status_code=500)\n\n        try:\n            handler_data = await self._service.await_workflow(started)\n            if handler_data.status == \"completed\":\n                status = 200\n            else:\n                logger.error(\n                    \"Workflow %s finished with status=%s error=%s (handler=%s, run=%s)\",\n                    handler_data.workflow_name,\n                    handler_data.status,\n                    handler_data.error,\n                    handler_data.handler_id,\n                    handler_data.run_id,\n                )\n                status = 500\n        except Exception as e:\n            logger.error(f\"Error running workflow: {e}\", exc_info=True)\n            handler_data = await self._service.load_handler(handler_id)\n            status = 500\n\n        return JSONResponse(\n            handler_data.model_dump() if handler_data else {}, status_code=status\n        )\n\n    async def _get_events_schema(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Get JSON schema for start event\n        description: |\n          Gets the JSON schema of the start and stop events from the specified workflow and returns it under \"start\" (start event) and \"stop\" (stop event)\n        parameters:\n          - in: path\n            name: name\n            required: true\n            schema:\n              type: string\n            description: Registered workflow name.\n        requestBody:\n          required: false\n        responses:\n          200:\n            description: JSON schema successfully retrieved for start event\n            content:\n              application/json:\n                schema:\n                  type: object\n                  properties:\n                    start:\n                      description: JSON schema for the start event\n                    stop:\n                      description: JSON schema for the stop event\n                  required: [start, stop]\n          404:\n            description: Workflow not found\n          500:\n            description: Error while getting the JSON schema for the start or stop event\n        \"\"\"\n        workflow = self._extract_workflow(request)\n        try:\n            start_event_schema = workflow.start_event_class.model_json_schema()\n        except Exception as e:\n            raise HTTPException(\n                detail=f\"Error getting schema of start event for workflow: {e}\",\n                status_code=500,\n            )\n        try:\n            stop_event_schema = workflow.stop_event_class.model_json_schema()\n        except Exception as e:\n            raise HTTPException(\n                detail=f\"Error getting schema of stop event for workflow: {e}\",\n                status_code=500,\n            )\n\n        return JSONResponse(\n            WorkflowSchemaResponse(\n                start=start_event_schema, stop=stop_event_schema\n            ).model_dump()\n        )\n\n    async def _get_workflow_representation(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Get the representation of the workflow\n        description: |\n          Get the representation of the workflow as a directed graph in JSON format\n        parameters:\n          - in: path\n            name: name\n            required: true\n            schema:\n              type: string\n            description: Registered workflow name.\n        requestBody:\n          required: false\n        responses:\n          200:\n            description: JSON representation successfully retrieved\n            content:\n              application/json:\n                schema:\n                  type: object\n                  properties:\n                    graph:\n                      description: the elements of the JSON representation of the workflow\n                  required: [graph]\n          404:\n            description: Workflow not found\n          500:\n            description: Error while getting JSON workflow representation\n        \"\"\"\n        workflow = self._extract_workflow(request)\n        try:\n            workflow_graph = get_workflow_representation(workflow)\n        except Exception as e:\n            raise HTTPException(\n                detail=f\"Error while getting JSON workflow representation: {e}\",\n                status_code=500,\n            )\n        return JSONResponse(WorkflowGraphResponse(graph=workflow_graph).model_dump())\n\n    async def _run_workflow_nowait(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Run workflow (no-wait)\n        description: |\n          Starts the specified workflow asynchronously and returns a handler identifier\n          which can be used to query results or stream events.\n        parameters:\n          - in: path\n            name: name\n            required: true\n            schema:\n              type: string\n            description: Registered workflow name.\n        requestBody:\n          required: false\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  start_event:\n                    type: object\n                    description: 'Plain JSON object representing the start event (e.g., {\"message\": \"...\"}).'\n                  handler_id:\n                    type: string\n                    description: Workflow handler identifier to continue from a previous completed run.\n                  kwargs:\n                    type: object\n                    description: Additional keyword arguments for the workflow.\n        responses:\n          200:\n            description: Workflow started\n            content:\n              application/json:\n                schema:\n                  $ref: '#/components/schemas/Handler'\n          400:\n            description: Invalid start_event payload\n          404:\n            description: Workflow or handler identifier not found\n        \"\"\"\n        workflow = self._extract_workflow(request)\n        context, start_event, handler_id = await self._extract_run_params(\n            request, workflow, workflow.workflow_name\n        )\n\n        if start_event is not None:\n            input_ev = workflow.start_event_class.model_validate(start_event)\n        else:\n            input_ev = None\n\n        try:\n            handler_data = await self._service.start_workflow(\n                workflow=workflow,\n                handler_id=handler_id,\n                context=context,\n                start_event=input_ev,\n            )\n        except Exception as e:\n            raise HTTPException(\n                detail=f\"Initial persistence failed: {e}\", status_code=500\n            )\n        return JSONResponse(handler_data.model_dump())\n\n    async def _load_handler(self, handler_id: str) -> HandlerData:\n        handler_data = await self._service.load_handler(handler_id)\n        if handler_data is None:\n            raise HTTPException(detail=\"Handler not found\", status_code=404)\n        return handler_data\n\n    async def _resolve_event_stream(\n        self,\n        handler_id: str,\n        *,\n        after_sequence: int | None,\n        include_internal: bool,\n        include_qualified_name: bool,\n    ) -> AsyncGenerator[tuple[int, EventEnvelopeWithMetadata], None] | None:\n        \"\"\"Resolve a handler to an event stream.\n\n        Args:\n            handler_id: The handler to stream events for.\n            after_sequence: Resume after this sequence number. None means \"now\"\n                (skip historical events).\n            include_internal: Whether to include internal dispatch events.\n            include_qualified_name: Whether to include qualified_name in envelopes.\n\n        Returns:\n            An async generator of (sequence, envelope) tuples, or None if the\n            handler is completed and all events have been consumed.\n\n        Raises:\n            HTTPException: 404 if handler not found or has no run.\n        \"\"\"\n        store = self._service.store\n\n        # Resolve handler_id → run_id via persistence\n        found = await store.query(HandlerQuery(handler_id_in=[handler_id]))\n        if not found:\n            raise HTTPException(detail=\"Handler not found\", status_code=404)\n\n        persistent = found[0]\n        run_id = persistent.run_id\n        if run_id is None:\n            raise HTTPException(detail=\"Handler has no associated run\", status_code=404)\n\n        # Resolve \"now\" cursor to current max sequence\n        if after_sequence is None:\n            all_current = await store.query_events(run_id)\n            after_sequence = all_current[-1].sequence if all_current else -1\n\n        # Check if already fully consumed\n        remaining_events = await store.query_events(\n            run_id, after_sequence=after_sequence\n        )\n        if not remaining_events:\n            all_events = await store.query_events(run_id)\n            run_is_complete = is_terminal_status(persistent.status) or (\n                bool(all_events)\n                and AbstractWorkflowStore._is_terminal_event(all_events[-1])\n            )\n            if run_is_complete:\n                return None\n\n        _INTERNAL_EVENT_TYPE = InternalDispatchEvent.__name__\n\n        async def event_gen() -> AsyncGenerator[\n            tuple[int, EventEnvelopeWithMetadata], None\n        ]:\n            async for stored_event in store.subscribe_events(\n                run_id,\n                after_sequence=after_sequence,  # type: ignore[arg-type]\n            ):\n                envelope = stored_event.event\n                types = (envelope.types or []) + [envelope.type]\n                if not include_internal and _INTERNAL_EVENT_TYPE in types:\n                    continue\n                if not include_qualified_name:\n                    envelope = envelope.model_copy(update={\"qualified_name\": None})\n                yield stored_event.sequence, envelope\n\n        return event_gen()\n\n    async def _get_workflow_result(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Get workflow result (deprecated)\n        description: |\n          Deprecated. Use GET /handlers/{handler_id} instead. Returns the final result of an asynchronously started workflow, if available.\n        parameters:\n          - in: path\n            name: handler_id\n            required: true\n            schema:\n              type: string\n            description: Workflow run identifier returned from the no-wait run endpoint.\n        deprecated: true\n        responses:\n          200:\n            description: Result is available\n            content:\n              application/json:\n                schema:\n                  type: object\n          202:\n            description: Result not ready yet\n            content:\n              application/json:\n                schema:\n                  type: object\n          404:\n            description: Handler not found\n          500:\n            description: Error computing result\n            content:\n              text/plain:\n                schema:\n                  type: string\n        \"\"\"\n        handler_id = request.path_params[\"handler_id\"]\n        if not handler_id:\n            raise HTTPException(detail=\"Handler ID is required\", status_code=400)\n\n        handler_data = await self._load_handler(handler_id)\n        status = (\n            202\n            if handler_data.status == \"running\"\n            else 200\n            if handler_data.status == \"completed\"\n            else 500\n        )\n        response_model = handler_data.model_dump()\n\n        # compatibility. Use handler api instead\n        if not handler_data.result:\n            response_model[\"result\"] = None\n        else:\n            type = handler_data.result.qualified_name\n            response_model[\"result\"] = (\n                handler_data.result.value.get(\"result\")\n                if type == \"workflows.events.StopEvent\"\n                else handler_data.result.value\n            )\n        return JSONResponse(response_model, status_code=status)\n\n    async def _get_workflow_handler(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Get workflow handler\n        description: Returns the final result of an asynchronously started workflow, if available\n        parameters:\n          - in: path\n            name: handler_id\n            required: true\n            schema:\n              type: string\n            description: Workflow run identifier returned from the no-wait run endpoint.\n        responses:\n          200:\n            description: Result is available\n            content:\n              application/json:\n                schema:\n                  $ref: '#/components/schemas/Handler'\n          202:\n            description: Result not ready yet\n            content:\n              application/json:\n                schema:\n                  $ref: '#/components/schemas/Handler'\n          404:\n            description: Handler not found\n          500:\n            description: Error computing result\n            content:\n              text/plain:\n                schema:\n                  type: string\n        \"\"\"\n        handler_id = request.path_params[\"handler_id\"]\n        if not handler_id:\n            raise HTTPException(detail=\"Handler ID is required\", status_code=400)\n\n        handler_data = await self._load_handler(handler_id)\n        status = (\n            202\n            if handler_data.status == \"running\"\n            else 200\n            if handler_data.status == \"completed\"\n            else 500\n        )\n        return JSONResponse(handler_data.model_dump(), status_code=status)\n\n    async def _stream_events(self, request: Request) -> StreamingResponse:\n        \"\"\"\n        ---\n        summary: Stream workflow events\n        description: |\n          Streams events produced by a workflow execution. Events are emitted as\n          newline-delimited JSON by default, or as Server-Sent Events when `sse=true`.\n          Multiple clients can stream the same handler concurrently. Disconnected\n          clients can resume from their last-seen position via `after_sequence`.\n\n          Event data is returned as an envelope:\n          {\n            \"value\": <pydantic serialized value>,\n            \"types\": [<class names from MRO excluding the event class and base Event>],\n            \"type\": <class name>,\n            \"qualified_name\": <python module path + class name>,\n          }\n\n        parameters:\n          - in: path\n            name: handler_id\n            required: true\n            schema:\n              type: string\n            description: Identifier returned from the no-wait run endpoint.\n          - in: query\n            name: sse\n            required: false\n            schema:\n              type: boolean\n              default: true\n            description: If false, as NDJSON instead of Server-Sent Events.\n          - in: query\n            name: include_internal\n            required: false\n            schema:\n              type: boolean\n              default: false\n            description: If true, include internal workflow events (e.g., step state changes).\n          - in: query\n            name: after_sequence\n            required: false\n            schema:\n              oneOf:\n                - type: integer\n                - type: string\n                  enum: [now]\n              default: now\n            description: >\n              Resume streaming after this event sequence number. Use -1\n              to start from the beginning, or \"now\" (default) to skip historical events and\n              only receive events appended after the request is made.\n              In SSE mode, the Last-Event-ID request header takes priority over\n              this parameter.\n          - in: query\n            name: include_qualified_name\n            required: false\n            schema:\n              type: boolean\n              default: true\n            description: If true, include the qualified name of the event in the response body.\n        responses:\n          200:\n            description: Streaming started\n            content:\n              text/event-stream:\n                schema:\n                  type: object\n                  description: Server-Sent Events stream of event data.\n                  properties:\n                    value:\n                      type: object\n                      description: The event value.\n                    type:\n                      type: string\n                      description: The class name of the event.\n                    types:\n                      type: array\n                      description: Superclass names from MRO (excluding the event class and base Event).\n                      items:\n                        type: string\n                    qualified_name:\n                      type: string\n                      description: The qualified name of the event.\n                  required: [value, type]\n          204:\n            description: Handler completed and all events already consumed\n          404:\n            description: Handler not found\n        \"\"\"\n        handler_id = request.path_params[\"handler_id\"]\n        include_internal = (\n            request.query_params.get(\"include_internal\", \"false\").lower() == \"true\"\n        )\n        include_qualified_name = (\n            request.query_params.get(\"include_qualified_name\", \"true\").lower() == \"true\"\n        )\n        sse = request.query_params.get(\"sse\", \"true\").lower() == \"true\"\n        after_sequence_str = request.query_params.get(\"after_sequence\", \"now\")\n        after_sequence_is_now = after_sequence_str.lower() == \"now\"\n        if after_sequence_is_now:\n            after_sequence: int | None = None  # resolved by helper\n        else:\n            try:\n                after_sequence = int(after_sequence_str)\n            except ValueError:\n                raise HTTPException(\n                    detail=f\"Invalid after_sequence: '{after_sequence_str}'\",\n                    status_code=400,\n                )\n\n        # SSE Last-Event-ID header overrides after_sequence\n        if sse:\n            last_event_id = request.headers.get(\"last-event-id\")\n            if last_event_id is not None:\n                try:\n                    after_sequence = int(last_event_id)\n                except ValueError:\n                    pass  # Ignore non-integer Last-Event-ID\n\n        gen = await self._resolve_event_stream(\n            handler_id,\n            after_sequence=after_sequence,\n            include_internal=include_internal,\n            include_qualified_name=include_qualified_name,\n        )\n        if gen is None:\n            raise HTTPException(detail=\"Handler is completed\", status_code=204)\n\n        media_type = \"text/event-stream\" if sse else \"application/x-ndjson\"\n        heartbeat_interval = self._sse_heartbeat_interval if sse else None\n\n        async def format_stream() -> AsyncGenerator[str, None]:\n            # We use a queue + feeder task so we can apply wait_for on\n            # queue.get() without corrupting async-generator state (which\n            # happens when asyncio.wait_for cancels __anext__).\n            _SENTINEL = object()\n            queue: asyncio.Queue[tuple[int, EventEnvelopeWithMetadata] | object] = (\n                asyncio.Queue()\n            )\n\n            async def _feed() -> None:\n                async for item in gen:\n                    await queue.put(item)\n                await queue.put(_SENTINEL)\n\n            feeder = asyncio.ensure_future(_feed())\n            try:\n                while True:\n                    try:\n                        item = await asyncio.wait_for(\n                            queue.get(), timeout=heartbeat_interval\n                        )\n                    except asyncio.TimeoutError:\n                        yield \": heartbeat\\n\\n\"\n                        await asyncio.sleep(0)\n                        continue\n                    if item is _SENTINEL:\n                        break\n                    sequence, envelope = cast(\n                        tuple[int, EventEnvelopeWithMetadata], item\n                    )\n                    payload = envelope.model_dump_json()\n                    if sse:\n                        yield f\"id: {sequence}\\ndata: {payload}\\n\\n\"\n                    else:\n                        yield f\"{payload}\\n\"\n                    await asyncio.sleep(0)\n            finally:\n                feeder.cancel()\n\n        return StreamingResponse(format_stream(), media_type=media_type)\n\n    async def _get_handlers(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Get handlers\n        description: Returns workflow handlers, optionally filtered by query parameters.\n        parameters:\n          - in: query\n            name: status\n            required: false\n            schema:\n              type: array\n              items:\n                type: string\n                enum: [running, completed, failed, cancelled]\n            style: form\n            explode: true\n            description: |\n              Filter by handler status. Can be provided multiple times (e.g., status=running&status=failed)\n          - in: query\n            name: workflow_name\n            required: false\n            schema:\n              type: array\n              items:\n                type: string\n            style: form\n            explode: true\n            description: |\n              Filter by workflow name. Can be provided multiple times (e.g., workflow_name=test&workflow_name=other)\n        responses:\n          200:\n            description: List of handlers\n            content:\n              application/json:\n                schema:\n                  $ref: '#/components/schemas/HandlersList'\n        \"\"\"\n\n        def _parse_list_param(param_name: str) -> list[str] | None:\n            # parse repeated params\n            values = list(request.query_params.getlist(param_name))\n            if not values:\n                single = request.query_params.get(param_name) or \"\"\n                values = [single]\n            values = [value.strip() for value in values if value.strip()]\n            if not values:\n                return None\n            return values\n\n        # Parse filters\n        status_values = _parse_list_param(\"status\")\n        workflow_name_in = _parse_list_param(\"workflow_name\")\n\n        # Narrow types for status to match HandlerQuery expectations\n        allowed_status_values: set[Status] = {\n            \"running\",\n            \"completed\",\n            \"failed\",\n            \"cancelled\",\n        }\n\n        status_in: list[Status] | None = (\n            list(set(allowed_status_values).intersection(status_values))\n            if status_values is not None\n            else None\n        )\n        persistent_handlers = await self._service.query_handlers(\n            HandlerQuery(status_in=status_in, workflow_name_in=workflow_name_in)\n        )\n        items = [\n            HandlerData(\n                handler_id=h.handler_id,\n                workflow_name=h.workflow_name,\n                run_id=h.run_id,\n                status=h.status,\n                started_at=h.started_at.isoformat() if h.started_at else \"\",\n                updated_at=h.updated_at.isoformat() if h.updated_at else None,\n                completed_at=h.completed_at.isoformat() if h.completed_at else None,\n                error=h.error,\n                result=EventEnvelopeWithMetadata.from_event(h.result)\n                if h.result\n                else None,\n            )\n            for h in persistent_handlers\n        ]\n        return JSONResponse(HandlersListResponse(handlers=items).model_dump())\n\n    async def _post_event(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Send event to workflow\n        description: Sends an event to a running workflow's context.\n        parameters:\n          - in: path\n            name: handler_id\n            required: true\n            schema:\n              type: string\n            description: Workflow handler identifier.\n        requestBody:\n          required: true\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  event:\n                    description: Serialized event. Accepts object or JSON-encoded string for backward compatibility.\n                    oneOf:\n                      - type: string\n                        description: JSON string of the event envelope or value.\n                        examples:\n                          - '{\"type\": \"ExternalEvent\", \"value\": {\"response\": \"hi\"}}'\n                      - type: object\n                        properties:\n                          type:\n                            type: string\n                            description: The class name of the event.\n                          value:\n                            type: object\n                            description: The event value object (preferred over data).\n                        additionalProperties: true\n                  step:\n                    type: string\n                    description: Optional target step name. If not provided, event is sent to all steps.\n                required: [event]\n        responses:\n          200:\n            description: Event sent successfully\n            content:\n              application/json:\n                schema:\n                  type: object\n                  properties:\n                    status:\n                      type: string\n                      enum: [sent]\n                  required: [status]\n          400:\n            description: Invalid event data\n          404:\n            description: Handler not found\n          409:\n            description: Workflow already completed\n        \"\"\"\n        handler_id = request.path_params[\"handler_id\"]\n\n        # Parse request body\n        try:\n            body = await request.json()\n        except Exception as e:\n            raise HTTPException(\n                detail=f\"Error processing request: {e}\", status_code=400\n            )\n\n        event_data = body.get(\"event\")\n        step = body.get(\"step\")\n\n        if not event_data:\n            raise HTTPException(detail=\"Event data is required\", status_code=400)\n\n        try:\n            handler_data = await self._service.resolve_handler(handler_id)\n        except HandlerNotFoundError:\n            raise HTTPException(detail=\"Handler not found\", status_code=404)\n        except HandlerCompletedError:\n            raise HTTPException(detail=\"Workflow already completed\", status_code=409)\n\n        try:\n            event = EventEnvelope.parse(\n                event_data, self.event_registry(handler_data.workflow_name)\n            )\n        except EventValidationError as e:\n            raise HTTPException(detail=str(e), status_code=400)\n        except Exception as e:\n            raise HTTPException(\n                detail=f\"Failed to deserialize event: {e}\", status_code=400\n            )\n\n        try:\n            await self._service.send_event(handler_id, event, step=step)\n        except HandlerNotFoundError:\n            raise HTTPException(detail=\"Handler not found\", status_code=404)\n        except HandlerCompletedError:\n            raise HTTPException(detail=\"Workflow already completed\", status_code=409)\n        except EventSendError as e:\n            raise HTTPException(detail=str(e), status_code=500)\n\n        return JSONResponse(SendEventResponse(status=\"sent\").model_dump())\n\n    async def _cancel_handler(self, request: Request) -> JSONResponse:\n        \"\"\"\n        ---\n        summary: Stop and delete handler\n        description: |\n          Stops a running workflow handler by cancelling its tasks. Optionally removes the\n          handler from the persistence store if purge=true.\n        parameters:\n          - in: path\n            name: handler_id\n            required: true\n            schema:\n              type: string\n            description: Workflow handler identifier.\n          - in: query\n            name: purge\n            required: false\n            schema:\n              type: boolean\n              default: false\n            description: If true, also deletes the handler from the store, otherwise updates the status to cancelled.\n        responses:\n          200:\n            description: Handler cancelled and deleted or cancelled only\n            content:\n              application/json:\n                schema:\n                  type: object\n                  properties:\n                    status:\n                      type: string\n                      enum: [deleted, cancelled]\n                  required: [status]\n          404:\n            description: Handler not found\n        \"\"\"\n        handler_id = request.path_params[\"handler_id\"]\n        # Simple boolean parsing aligned with other APIs (e.g., `sse`): only \"true\" enables\n        purge = request.query_params.get(\"purge\", \"false\").lower() == \"true\"\n\n        result = await self._service.cancel_handler(handler_id, purge=purge)\n        if result is None:\n            raise HTTPException(detail=\"Handler not found\", status_code=404)\n\n        return JSONResponse(CancelHandlerResponse(status=result).model_dump())\n\n    #\n    # Private methods\n    #\n    def _extract_workflow(self, request: Request) -> Workflow:\n        if \"name\" not in request.path_params:\n            raise HTTPException(detail=\"'name' parameter missing\", status_code=400)\n        name = request.path_params[\"name\"]\n\n        workflow = self._service.get_workflow(name)\n        if workflow is None:\n            raise HTTPException(detail=\"Workflow not found\", status_code=404)\n\n        return workflow\n\n    async def _extract_run_params(\n        self, request: Request, workflow: Workflow, workflow_name: str\n    ) -> tuple[Context | None, StartEvent | None, str]:\n        try:\n            try:\n                body = await request.json()\n            except Exception as e:\n                raise HTTPException(detail=f\"Invalid JSON body: {e}\", status_code=400)\n            context_data = body.get(\"context\")\n            run_kwargs = body.get(\"kwargs\", {})\n            start_event_data = body.get(\"start_event\", run_kwargs)\n            handler_id = body.get(\"handler_id\")\n\n            # Extract custom StartEvent if present\n            start_event = None\n            if start_event_data is not None:\n                try:\n                    start_event = EventEnvelope.parse(\n                        start_event_data,\n                        self.event_registry(workflow_name),\n                        explicit_event=workflow.start_event_class,\n                    )\n\n                except Exception as e:\n                    raise HTTPException(\n                        detail=f\"Validation error for 'start_event': {e}\",\n                        status_code=400,\n                    )\n                if start_event is not None and not isinstance(\n                    start_event, workflow.start_event_class\n                ):\n                    raise HTTPException(\n                        detail=f\"Start event must be an instance of {workflow.start_event_class}\",\n                        status_code=400,\n                    )\n\n            # Extract custom Context if present\n            context = None\n            if context_data:\n                if not self._accept_context_api:\n                    raise HTTPException(\n                        detail=\"Context API is disabled. Set accept_context_api=True on WorkflowServer to enable it.\",\n                        status_code=400,\n                    )\n                context = Context.from_dict(workflow=workflow, data=context_data)\n\n            handler_id = handler_id or nanoid()\n            return (context, start_event, handler_id)\n\n        except HTTPException:\n            # Re-raise HTTPExceptions as-is (like start_event validation errors)\n            raise\n        except Exception as e:\n            raise HTTPException(\n                detail=f\"Error processing request body: {e}\", status_code=400\n            )\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_keyed_lock.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Keyed lock utility for per-key mutual exclusion with automatic cleanup.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom contextlib import asynccontextmanager\nfrom typing import AsyncIterator\n\n\nclass KeyedLock:\n    \"\"\"A collection of locks keyed by string, with automatic cleanup.\n\n    Locks are created on-demand and automatically removed when no longer\n    in use (no waiters or holders). Safe for concurrent asyncio coroutines.\n\n    Usage:\n        locks = KeyedLock()\n\n        async with locks(\"my-key\"):\n            # critical section for \"my-key\"\n            pass\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._main_lock: asyncio.Lock | None = None\n        self._locks: dict[str, asyncio.Lock] = {}\n        self._refs: dict[str, int] = {}\n\n    def _get_main_lock(self) -> asyncio.Lock:\n        if self._main_lock is None:\n            self._main_lock = asyncio.Lock()\n        return self._main_lock\n\n    @asynccontextmanager\n    async def __call__(self, key: str) -> AsyncIterator[None]:\n        \"\"\"Acquire a lock for the given key.\n\n        The lock is created if it doesn't exist and removed when the last\n        holder/waiter releases it.\n        \"\"\"\n        # Get or create lock and register interest.\n        # No await between these lines = atomic in asyncio.\n        async with self._get_main_lock():\n            if key not in self._locks:\n                self._locks[key] = asyncio.Lock()\n                self._refs[key] = 0\n            self._refs[key] += 1\n\n        try:\n            async with self._locks[key]:\n                yield\n        finally:\n            # Deregister and cleanup if last.\n            # No await between these lines = atomic in asyncio.\n            async with self._get_main_lock():\n                self._refs[key] -= 1\n                if self._refs[key] == 0:\n                    del self._locks[key]\n                    del self._refs[key]\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_lru_cache.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Simple thread-safe LRU cache backed by OrderedDict.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections import OrderedDict\nfrom typing import Generic, TypeVar\n\nK = TypeVar(\"K\")\nV = TypeVar(\"V\")\n\n\nclass LRUCache(Generic[K, V]):\n    \"\"\"Bounded LRU cache.\n\n    On ``get`` or ``put`` the accessed key moves to the end (most-recently\n    used).  When the cache exceeds *maxsize*, the least-recently used entry\n    is evicted.\n    \"\"\"\n\n    def __init__(self, maxsize: int = 256) -> None:\n        self._maxsize = maxsize\n        self._data: OrderedDict[K, V] = OrderedDict()\n\n    def get(self, key: K) -> V | None:\n        if key in self._data:\n            self._data.move_to_end(key)\n            return self._data[key]\n        return None\n\n    def put(self, key: K, value: V) -> None:\n        if key in self._data:\n            self._data.move_to_end(key)\n        self._data[key] = value\n        if len(self._data) > self._maxsize:\n            self._data.popitem(last=False)\n\n    def delete(self, key: K) -> None:\n        self._data.pop(key, None)\n\n    def __len__(self) -> int:\n        return len(self._data)\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_pool.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Awaitable, Callable\n\nimport asyncpg\n\n\nclass PoolProvider:\n    \"\"\"Lazy asyncpg pool provider with explicit ownership semantics.\"\"\"\n\n    def __init__(\n        self,\n        factory: Callable[[], Awaitable[asyncpg.Pool]],\n        *,\n        owns_pool: bool,\n    ) -> None:\n        self._factory = factory\n        self._owns_pool = owns_pool\n        self._lock = asyncio.Lock()\n        self._pool: asyncpg.Pool | None = None\n        self._closed = False\n        self._terminated = False\n\n    @classmethod\n    def create(cls, dsn: str, min_size: int, max_size: int) -> PoolProvider:\n        async def factory() -> asyncpg.Pool:\n            return await asyncpg.create_pool(\n                dsn,\n                min_size=min_size,\n                max_size=max_size,\n            )\n\n        return cls(factory, owns_pool=True)\n\n    @classmethod\n    def borrowed(\n        cls,\n        factory: Callable[[], Awaitable[asyncpg.Pool]],\n    ) -> PoolProvider:\n        return cls(factory, owns_pool=False)\n\n    async def get(self) -> asyncpg.Pool:\n        self._raise_if_shutdown()\n        if self._pool is not None:\n            return self._pool\n\n        async with self._lock:\n            self._raise_if_shutdown()\n            if self._pool is None:\n                self._pool = await self._factory()\n            return self._pool\n\n    async def close(self) -> None:\n        if not self._owns_pool:\n            return\n        if self._closed:\n            return\n        self._closed = True\n        if self._terminated:\n            return\n        if self._pool is not None:\n            await self._pool.close()\n\n    def terminate(self) -> None:\n        if not self._owns_pool:\n            return\n        if self._terminated:\n            return\n        self._terminated = True\n        if self._closed:\n            return\n        if self._pool is not None:\n            self._pool.terminate()\n\n    def _raise_if_shutdown(self) -> None:\n        if self._terminated:\n            raise RuntimeError(\"PoolProvider has been terminated.\")\n        if self._closed:\n            raise RuntimeError(\"PoolProvider has been closed.\")\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_runtime/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_runtime/event_interceptor.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nEventInterceptorDecorator: blocks write_to_event_stream from reaching the\ninner runtime while allowing all other operations (ticks, send/recv, close)\nto pass through normally.\n\nUsed when the ServerRuntimeDecorator already writes events to the workflow\nstore and forwarding them to the inner runtime (e.g. DBOS) would cause\nduplicate writes.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nfrom typing_extensions import override\nfrom workflows.events import Event\nfrom workflows.runtime.runtime_decorators import (\n    BaseInternalRunAdapterDecorator,\n    BaseRuntimeDecorator,\n)\nfrom workflows.runtime.types.plugin import InternalRunAdapter\nfrom workflows.workflow import Workflow\n\nlogger = logging.getLogger(__name__)\n\n\nclass _InterceptorInternalAdapter(BaseInternalRunAdapterDecorator):\n    \"\"\"Internal adapter that swallows write_to_event_stream calls.\"\"\"\n\n    @override\n    async def write_to_event_stream(self, event: Event) -> None:\n        # No-op: do NOT forward to inner adapter.\n        pass\n\n\nclass EventInterceptorDecorator(BaseRuntimeDecorator):\n    \"\"\"Runtime decorator that prevents published events from reaching the\n    inner runtime's event stream.\n\n    All other methods (on_tick, send_event, wait_receive, close, etc.)\n    pass through to the inner runtime normally.\n    \"\"\"\n\n    @override\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        inner = self._decorated.get_internal_adapter(workflow)\n        return _InterceptorInternalAdapter(inner)\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_runtime/idle_release_runtime.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"IdleReleaseDecorator and supporting adapters.\n\nWraps a PersistenceDecorator to add idle detection, memory release, and\nreload-on-demand for idle workflow handlers.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom collections.abc import Coroutine\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nfrom typing_extensions import override\nfrom workflows.context.serializers import BaseSerializer\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    WorkflowIdleEvent,\n)\nfrom workflows.runtime.runtime_decorators import (\n    BaseExternalRunAdapterDecorator,\n    BaseInternalRunAdapterDecorator,\n    BaseRuntimeDecorator,\n)\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    V2RuntimeCompatibilityShim,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\nfrom workflows.workflow import Workflow\n\nfrom .._keyed_lock import KeyedLock\nfrom .._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n)\nfrom .persistence_runtime import TickPersistenceDecorator\n\nlogger = logging.getLogger(__name__)\n\n\nclass _IdleReleaseInternalRunAdapter(BaseInternalRunAdapterDecorator):\n    \"\"\"Internal adapter that detects idle events and schedules release.\"\"\"\n\n    def __init__(\n        self,\n        decorated: InternalRunAdapter,\n        runtime: IdleReleaseDecorator,\n        store: AbstractWorkflowStore,\n    ) -> None:\n        super().__init__(decorated)\n        self._runtime = runtime\n        self._store = store\n\n    @override\n    async def write_to_event_stream(self, event: Event) -> None:\n        if isinstance(event, WorkflowIdleEvent):\n            idle_since = datetime.now(timezone.utc)\n            await self._store.update_handler_status(\n                self.run_id, status=\"running\", idle_since=idle_since\n            )\n        await super().write_to_event_stream(event)\n        if isinstance(event, WorkflowIdleEvent):\n            self._runtime._spawn_task(self._runtime._deferred_release(self.run_id))\n\n\nclass IdleReleaseExternalRunAdapter(BaseExternalRunAdapterDecorator):\n    \"\"\"Proxy adapter that adds reload-on-demand for idle-released handlers.\n\n    The inner adapter is resolved lazily via a property because\n    ``get_external_adapter`` is sync but reload is async — the inner run\n    may not exist yet when this adapter is constructed.\n    \"\"\"\n\n    def __init__(self, runtime: IdleReleaseDecorator, run_id: str) -> None:\n        # Intentionally skip super().__init__ — _decorated is a lazy property.\n        self._runtime = runtime\n        self._run_id = run_id\n\n    @property  # type: ignore[override]\n    def _decorated(self) -> ExternalRunAdapter:\n        return self._runtime._decorated.get_external_adapter(self._run_id)\n\n    @_decorated.setter\n    def _decorated(self, value: ExternalRunAdapter) -> None:\n        pass\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    @override\n    async def send_event(self, tick: WorkflowTick) -> None:\n        async with self._runtime._reload_lock(self.run_id):\n            if self.run_id not in self._runtime._active_run_ids:\n                await self._runtime._ensure_active_run_locked(self.run_id)\n            else:\n                await self._runtime._store.update_handler_status(\n                    self.run_id, idle_since=None\n                )\n            await self._decorated.send_event(tick)\n\n\nclass IdleReleaseDecorator(BaseRuntimeDecorator):\n    \"\"\"Runtime decorator for idle detection, memory release, and reload-on-demand.\n\n    Must wrap a TickPersistenceDecorator (or compatible runtime) to access\n    context_from_ticks for reloading released handlers.\n    \"\"\"\n\n    def __init__(\n        self,\n        decorated: TickPersistenceDecorator,\n        store: AbstractWorkflowStore,\n        idle_timeout: float = 60.0,\n    ) -> None:\n        super().__init__(decorated)\n        self._store = store\n        self._persistence: TickPersistenceDecorator = decorated\n        self._reload_lock = KeyedLock()\n        self._active_run_ids: set[str] = set()\n        self._background_tasks: set[asyncio.Task[None]] = set()\n        self.stop_task: asyncio.Task[None] | None = None\n        self._idle_timeout = idle_timeout\n\n    def _spawn_task(self, coro: Coroutine[Any, Any, None]) -> asyncio.Task[None]:\n        task = asyncio.create_task(coro)\n        self._background_tasks.add(task)\n        task.add_done_callback(self._background_tasks.discard)\n        return task\n\n    @override\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> ExternalRunAdapter:\n        self._active_run_ids.add(run_id)\n        return super().run_workflow(\n            run_id,\n            workflow,\n            init_state,\n            start_event=start_event,\n            serialized_state=serialized_state,\n            serializer=serializer,\n        )\n\n    @override\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        inner_adapter = self._decorated.get_internal_adapter(workflow)\n        return _IdleReleaseInternalRunAdapter(inner_adapter, self, self._store)\n\n    @override\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        return IdleReleaseExternalRunAdapter(self, run_id)\n\n    async def _deferred_release(self, run_id: str) -> None:\n        \"\"\"Wait for idle_timeout then release the handler if still idle.\"\"\"\n        await asyncio.sleep(self._idle_timeout)\n        await self._release_idle_handler(run_id)\n\n    async def _release_idle_handler(self, run_id: str) -> None:\n        \"\"\"Release an idle handler from memory.\"\"\"\n        async with self._reload_lock(run_id):\n            handlers = await self._store.query(HandlerQuery(run_id_in=[run_id]))\n            if len(handlers) != 1 or handlers[0].idle_since is None:\n                return\n            elapsed = (\n                datetime.now(timezone.utc) - handlers[0].idle_since\n            ).total_seconds()\n            if elapsed < self._idle_timeout:\n                return\n            if run_id not in self._active_run_ids:\n                return\n            self._active_run_ids.discard(run_id)\n            self._abort_inner_run(run_id)\n            logger.info(f\"Released idle handler [run_id={run_id}] from memory\")\n\n    def _abort_inner_run(self, run_id: str) -> None:\n        \"\"\"Cancel the inner runtime's control loop task for a run.\"\"\"\n        try:\n            inner_adapter = self._decorated.get_external_adapter(run_id)\n        except Exception:\n            return\n        if isinstance(inner_adapter, V2RuntimeCompatibilityShim):\n            inner_adapter.abort()\n        else:\n            raise ValueError(f\"Inner adapter {inner_adapter} does not support abort\")\n\n    async def _ensure_active_run(self, run_id: str) -> None:\n        if run_id in self._active_run_ids:\n            return\n        async with self._reload_lock(run_id):\n            await self._ensure_active_run_locked(run_id)\n\n    async def _ensure_active_run_locked(self, run_id: str) -> None:\n        if run_id in self._active_run_ids:\n            return\n        handlers = await self._store.query(HandlerQuery(run_id_in=[run_id]))\n        if len(handlers) != 1:\n            raise ValueError(\n                f\"Expected 1 handler for run {run_id}, got {len(handlers)}\"\n            )\n        handler = handlers[0]\n        workflow = self._persistence.get_tracked_workflow(handler.workflow_name)\n        if workflow is None:\n            raise ValueError(f\"Workflow {handler.workflow_name} not found\")\n        replayed = await self._persistence.context_from_ticks(workflow, run_id)\n        context = replayed.context if replayed is not None else None\n        workflow.run(ctx=context, run_id=run_id)\n        self._active_run_ids.add(run_id)\n        await self._store.update_handler_status(run_id, idle_since=None)\n        logger.info(\n            f\"Reloaded workflow [handler_id={handler.handler_id}, run_id={run_id}] from persistence\"\n        )\n\n    @override\n    async def destroy(self) -> None:\n        await super().destroy()\n        if self.stop_task is not None:\n            try:\n                self.stop_task.cancel()\n            except Exception:\n                pass\n        self.stop_task = self._spawn_task(self._on_server_stop())\n\n    async def _on_server_stop(self) -> None:\n        \"\"\"Cancel all active runs.\"\"\"\n        run_ids = list(self._active_run_ids)\n        logger.info(f\"Shutting down. Cancelling {len(run_ids)} handlers.\")\n        for run_id in run_ids:\n            self._abort_inner_run(run_id)\n        self._active_run_ids.clear()\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_runtime/persistence_runtime.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"TickPersistenceDecorator, PersistenceDecorator, and _PersistenceInternalRunAdapter.\n\nTickPersistenceDecorator provides tick persistence, workflow tracking, and\ncontext_from_ticks.  PersistenceDecorator extends it with auto-restart on\nserver start.  Neither handles idle detection — that lives in\nIdleReleaseDecorator.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sqlite3\nfrom collections.abc import AsyncIterator, Coroutine\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom typing_extensions import override\nfrom workflows import Context\nfrom workflows.context.context_types import SerializedContext\nfrom workflows.context.serializers import BaseSerializer, JsonSerializer\nfrom workflows.errors import WorkflowCancelledByUser\nfrom workflows.events import IdleReleasedEvent, StartEvent, StopEvent\nfrom workflows.runtime.control_loop import replay_ticks_stream\nfrom workflows.runtime.runtime_decorators import (\n    BaseInternalRunAdapterDecorator,\n    BaseRuntimeDecorator,\n)\nfrom workflows.runtime.types.commands import (\n    CommandCompleteRun,\n    CommandFailWorkflow,\n    CommandHalt,\n)\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    Runtime,\n)\nfrom workflows.runtime.types.ticks import (\n    TickStepResult,\n    WorkflowTick,\n    WorkflowTickAdapter,\n)\nfrom workflows.workflow import Workflow\n\nfrom .._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    Status,\n    as_legacy_context_store,\n    stream_workflow_ticks,\n)\nfrom .._store.sqlite.sqlite_state_store import SqliteStateStore\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass ReplayedContext:\n    \"\"\"Result of replaying persisted ticks into a Context.\n\n    Attributes:\n        context: Rebuilt Context ready to be passed to ``workflow.run()``.\n        exit_command: The reducer-emitted terminal command if the tick stream\n            terminated, else None. Callers map this to handler status (see\n            :func:`handler_status_from_exit_command`).\n    \"\"\"\n\n    context: Context\n    exit_command: CommandCompleteRun | CommandFailWorkflow | CommandHalt | None = None\n\n\ndef handler_status_from_exit_command(\n    command: CommandCompleteRun | CommandFailWorkflow | CommandHalt,\n) -> tuple[Status, StopEvent | None, str | None] | None:\n    \"\"\"Map a reducer exit command to (status, result, error).\n\n    Returns None for CommandCompleteRun(IdleReleasedEvent) — idle release is\n    not a real completion, just how the reducer signals the runner to exit.\n    \"\"\"\n    if isinstance(command, CommandCompleteRun):\n        if isinstance(command.result, IdleReleasedEvent):\n            return None\n        return (\"completed\", command.result, None)\n    if isinstance(command, CommandFailWorkflow):\n        return (\"failed\", None, str(command.exception))\n    # CommandHalt: timeout or cancel (both reach this replay path)\n    if isinstance(command.exception, WorkflowCancelledByUser):\n        return (\"cancelled\", None, None)\n    return (\"failed\", None, str(command.exception))\n\n\nclass _PersistenceInternalRunAdapter(BaseInternalRunAdapterDecorator):\n    \"\"\"Internal adapter that persists ticks to the workflow store.\"\"\"\n\n    def __init__(\n        self,\n        decorated: InternalRunAdapter,\n        store: AbstractWorkflowStore,\n    ) -> None:\n        super().__init__(decorated)\n        self._store = store\n\n    @override\n    async def on_tick(self, tick: WorkflowTick) -> None:\n        await super().on_tick(tick)\n        tick_data = WorkflowTickAdapter.dump_python(tick, mode=\"json\")\n        try:\n            await self._store.append_tick(self.run_id, tick_data)\n        except Exception:\n            logger.exception(\n                \"Failed to persist tick for run %s\",\n                self.run_id,\n            )\n\n    @override\n    async def after_tick(self, tick: WorkflowTick) -> None:\n        await super().after_tick(tick)\n        if not isinstance(tick, TickStepResult):\n            return\n        tick_data = WorkflowTickAdapter.dump_python(tick, mode=\"json\")\n        try:\n            await self._store.after_tick(self.run_id, tick_data)\n        except Exception:\n            logger.exception(\n                \"Failed to gather pending writes for run %s\",\n                self.run_id,\n            )\n\n\nclass TickPersistenceDecorator(BaseRuntimeDecorator):\n    \"\"\"Runtime decorator for tick persistence and workflow tracking.\n\n    Provides tick storage via internal adapter, workflow tracking by name,\n    and context_from_ticks for rebuilding state from persisted ticks.\n    \"\"\"\n\n    def __init__(\n        self,\n        decorated: Runtime,\n        store: AbstractWorkflowStore,\n    ) -> None:\n        super().__init__(decorated)\n        self._store = store\n        self._workflows_by_name: dict[str, Workflow] = {}\n        self._active_run_ids: set[str] = set()\n\n    @override\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> ExternalRunAdapter:\n        self._active_run_ids.add(run_id)\n        return super().run_workflow(\n            run_id,\n            workflow,\n            init_state,\n            start_event=start_event,\n            serialized_state=serialized_state,\n            serializer=serializer,\n        )\n\n    @override\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        inner_adapter = self._decorated.get_internal_adapter(workflow)\n        return _PersistenceInternalRunAdapter(inner_adapter, self._store)\n\n    @override\n    def track_workflow(self, workflow: Workflow) -> None:\n        self._workflows_by_name[workflow.workflow_name] = workflow\n        super().track_workflow(workflow)\n\n    @override\n    def untrack_workflow(self, workflow: Workflow) -> None:\n        self._workflows_by_name.pop(workflow.workflow_name, None)\n        super().untrack_workflow(workflow)\n\n    def get_tracked_workflow(self, name: str) -> Workflow | None:\n        \"\"\"Look up a tracked workflow by name (used by IdleReleaseDecorator).\"\"\"\n        return self._workflows_by_name.get(name)\n\n    async def context_from_ticks(\n        self, workflow: Workflow, run_id: str\n    ) -> ReplayedContext | None:\n        \"\"\"Rebuild a Context from persisted ticks (and legacy ctx if available).\n\n        Returns the Context plus the reducer's exit command if the tick\n        stream already terminated. Callers use ``exit_command`` to finalize\n        handlers instead of resuming them.\n        \"\"\"\n        serializer = JsonSerializer()\n        legacy_ctx = self._get_legacy_ctx(run_id)\n\n        tick_stream = stream_workflow_ticks(self._store, run_id)\n        try:\n            first_tick = await tick_stream.__anext__()\n        except StopAsyncIteration:\n            first_tick = None\n\n        if first_tick is None and not legacy_ctx:\n            return None\n\n        if legacy_ctx:\n            self._seed_legacy_state(run_id, legacy_ctx)\n            parsed = SerializedContext.from_dict_auto(legacy_ctx)\n            init_state = BrokerState.from_serialized(parsed, workflow, serializer)\n        else:\n            init_state = BrokerState.from_workflow(workflow)\n\n        exit_command: CommandCompleteRun | CommandFailWorkflow | CommandHalt | None = (\n            None\n        )\n        if first_tick is not None:\n\n            async def _with_first() -> AsyncIterator[WorkflowTick]:\n                yield first_tick\n                async for tick in tick_stream:\n                    yield tick\n\n            replay = await replay_ticks_stream(init_state, _with_first())\n            init_state = replay.state\n            exit_command = replay.exit_command\n\n        serialized = init_state.to_serialized(serializer)\n        context = Context.from_dict(\n            workflow=workflow, data=serialized.model_dump(), serializer=serializer\n        )\n        return ReplayedContext(context=context, exit_command=exit_command)\n\n    def _get_legacy_ctx(self, run_id: str) -> dict[str, Any] | None:\n        legacy_store = as_legacy_context_store(self._store)\n        if legacy_store is None:\n            return None\n        try:\n            return legacy_store.get_legacy_ctx(run_id)\n        except Exception:\n            logger.warning(\n                \"Failed to read legacy ctx for run %s\", run_id, exc_info=True\n            )\n            return None\n\n    def _seed_legacy_state(self, run_id: str, legacy_ctx: dict[str, Any]) -> None:\n        try:\n            parsed = SerializedContext.from_dict_auto(legacy_ctx)\n        except Exception:\n            logger.warning(\n                \"Failed to parse legacy ctx for state migration, run %s\", run_id\n            )\n            return\n\n        state_data = parsed.state\n        if not state_data:\n            return\n\n        state_store = self._store.create_state_store(run_id)\n        if not isinstance(state_store, SqliteStateStore):\n            return\n\n        conn = sqlite3.connect(state_store._db_path)\n        try:\n            row = conn.execute(\n                \"SELECT 1 FROM workflow_state WHERE run_id = ?\", (run_id,)\n            ).fetchone()\n            if row is not None:\n                return\n        finally:\n            conn.close()\n\n        state_store._write_in_memory_state(state_data)\n\n\nclass PersistenceDecorator(TickPersistenceDecorator):\n    \"\"\"Runtime decorator that extends TickPersistenceDecorator with auto-restart.\n\n    Resumes previously running workflows on server start.\n    \"\"\"\n\n    def __init__(\n        self,\n        decorated: Runtime,\n        store: AbstractWorkflowStore,\n    ) -> None:\n        super().__init__(decorated, store)\n        self._background_tasks: set[asyncio.Task[None]] = set()\n        self.resume_task: asyncio.Task[None] | None = None\n\n    def _spawn_task(self, coro: Coroutine[Any, Any, None]) -> asyncio.Task[None]:\n        task = asyncio.create_task(coro)\n        self._background_tasks.add(task)\n        task.add_done_callback(self._background_tasks.discard)\n        return task\n\n    @override\n    async def launch(self) -> None:\n        await super().launch()\n        self.resume_task = self._spawn_task(\n            self._on_server_start(self._workflows_by_name)\n        )\n\n    async def _on_server_start(self, registered_workflows: dict[str, Workflow]) -> None:\n        \"\"\"Resume previously running (non-idle) workflows from persistence.\"\"\"\n        handlers = await self._store.query(\n            HandlerQuery(\n                status_in=[\"running\"],\n                workflow_name_in=list(registered_workflows.keys()),\n                is_idle=False,\n            )\n        )\n        for persistent in handlers:\n            workflow = registered_workflows.get(persistent.workflow_name)\n            if workflow is None:\n                continue\n            if persistent.run_id is None:\n                logger.error(f\"Run ID is required for handler {persistent.handler_id}\")\n                continue\n            run_id = persistent.run_id\n            if run_id in self._active_run_ids:\n                continue\n            try:\n                replayed = await self.context_from_ticks(workflow, run_id)\n\n                if replayed is None:\n                    # A fresh-start attempt here would build a StartEvent from\n                    # empty kwargs and loop on every boot for workflows with\n                    # required fields. Mark failed so the handler stops being\n                    # picked up by the next resume query.\n                    logger.warning(\n                        \"No replayable state for handler %s (workflow %s); \"\n                        \"marking as failed\",\n                        persistent.handler_id,\n                        persistent.workflow_name,\n                    )\n                    await self._store.update_handler_status(\n                        run_id,\n                        status=\"failed\",\n                        error=\"handler crashed before persisting any state; cannot resume\",\n                    )\n                    continue\n\n                finalize = (\n                    handler_status_from_exit_command(replayed.exit_command)\n                    if replayed.exit_command is not None\n                    else None\n                )\n                if finalize is not None:\n                    status, result, error = finalize\n                    logger.warning(\n                        \"Replay for handler %s (workflow %s) terminated as %s; \"\n                        \"finalizing without resume\",\n                        persistent.handler_id,\n                        persistent.workflow_name,\n                        status,\n                    )\n                    await self._store.update_handler_status(\n                        run_id,\n                        status=status,\n                        result=result,\n                        error=error,\n                    )\n                    continue\n\n                workflow.run(ctx=replayed.context, run_id=run_id)\n            except Exception as e:\n                logger.error(\n                    f\"Failed to resume handler {persistent.handler_id} for workflow {persistent.workflow_name}: {e}\"\n                )\n                try:\n                    await self._store.update_handler_status(\n                        run_id, status=\"failed\", error=str(e)\n                    )\n                except Exception:\n                    logger.exception(\n                        \"Failed to mark resume-failed handler %s as failed\",\n                        persistent.handler_id,\n                    )\n                continue\n\n    @override\n    async def destroy(self) -> None:\n        await super().destroy()\n        if self.resume_task is not None:\n            try:\n                self.resume_task.cancel()\n            except Exception:\n                pass\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_runtime/server_runtime.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nServer runtime decorator: the main required runtime decorator for workflows\nserved by the WorkflowServer. Handles event recording, handler persistence,\nand status updates.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom datetime import datetime, timezone\nfrom typing import Any, Awaitable, Callable\n\nfrom llama_agents.client.protocol.serializable_events import (\n    EventEnvelopeWithMetadata,\n)\nfrom typing_extensions import override\nfrom workflows.context.serializers import BaseSerializer\nfrom workflows.context.state_store import (\n    StateStore,\n    infer_state_type,\n)\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n    WorkflowTimedOutEvent,\n)\nfrom workflows.runtime.runtime_decorators import (\n    BaseInternalRunAdapterDecorator,\n    BaseRuntimeDecorator,\n)\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    Runtime,\n)\nfrom workflows.workflow import Workflow\n\nfrom .._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    PersistentHandler,\n    Status,\n)\n\nlogger = logging.getLogger(__name__)\n\n\n# ---------------------------------------------------------------------------\n# _ServerInternalRunAdapter\n# ---------------------------------------------------------------------------\n\n\nclass _ServerInternalRunAdapter(BaseInternalRunAdapterDecorator):\n    \"\"\"Internal adapter that records every emitted event to the workflow store.\n\n    Handles event recording and terminal-event status updates.\n    \"\"\"\n\n    def __init__(\n        self,\n        decorated: InternalRunAdapter,\n        runtime: ServerRuntimeDecorator,\n        *,\n        state_type: type[Any] | None = None,\n    ) -> None:\n        super().__init__(decorated)\n        self._runtime = runtime\n        self._store = runtime._store\n        self._state_type = state_type\n        self._state_store: StateStore[Any] | None = None\n        self._write_lock: asyncio.Lock | None = None\n\n    @override\n    def get_state_store(self) -> StateStore[Any]:\n        if self._state_store is not None:\n            return self._state_store\n        initial = self._runtime._initial_state.pop(self.run_id, None)\n        if initial is not None:\n            serialized_state, serializer = initial\n            store = self._store.create_state_store(\n                self.run_id, self._state_type, serialized_state, serializer\n            )\n        else:\n            store = self._store.create_state_store(self.run_id, self._state_type)\n        self._state_store = store\n        return store\n\n    @override\n    async def write_to_event_stream(self, event: Event) -> None:\n        \"\"\"Record events to the workflow store, skipping duplicates on replay.\n\n        Uses a lock to serialize writes, ensuring events are stored in the\n        order they were emitted even when called from concurrent tasks.\n        \"\"\"\n        if self._write_lock is None:\n            self._write_lock = asyncio.Lock()\n        async with self._write_lock:\n            replaying = self.is_replaying()\n\n            if not replaying:\n                if isinstance(event, WorkflowFailedEvent):\n                    exc_type = type(event.exception)\n                    logger.error(\n                        \"Workflow step %s failed (run=%s): [%s.%s] %s\",\n                        event.step_name,\n                        self.run_id,\n                        exc_type.__module__,\n                        exc_type.__qualname__,\n                        event.exception,\n                    )\n                    await self._runtime._handle_status_update(\n                        run_id=self.run_id,\n                        status=\"failed\",\n                        error=str(event.exception),\n                    )\n                elif isinstance(event, WorkflowTimedOutEvent):\n                    logger.error(\n                        \"Workflow timed out after %ss (run=%s)\",\n                        event.timeout,\n                        self.run_id,\n                    )\n                    await self._runtime._handle_status_update(\n                        run_id=self.run_id,\n                        status=\"failed\",\n                        error=f\"Workflow timed out after {event.timeout}s\",\n                    )\n                elif isinstance(event, WorkflowCancelledEvent):\n                    await self._runtime._handle_status_update(\n                        run_id=self.run_id, status=\"cancelled\"\n                    )\n                elif isinstance(event, StopEvent):\n                    await self._runtime._handle_status_update(\n                        run_id=self.run_id,\n                        status=\"completed\",\n                        result=event,\n                    )\n\n                envelope = EventEnvelopeWithMetadata.from_event(event)\n                await self._store.append_event(self.run_id, envelope)\n\n            # Always forward to inner adapter (e.g. idle detection, DBOS stream)\n            await super().write_to_event_stream(event)\n\n\n# ---------------------------------------------------------------------------\n# ServerRuntimeDecorator -- adapter wrapping, handler persistence,\n# status updates, and workflow registry\n# ---------------------------------------------------------------------------\n\n\nclass ServerRuntimeDecorator(BaseRuntimeDecorator):\n    \"\"\"\n    Runtime decorator that wraps the main runtime to also record events to a configured\n    workflow store, for integration with the WorkflowService for querying\n    workflow run state.\n    \"\"\"\n\n    def __init__(\n        self,\n        decorated: Runtime,\n        store: AbstractWorkflowStore,\n        *,\n        persistence_backoff: list[float] | None = None,\n    ) -> None:\n        super().__init__(decorated)\n        self._store: AbstractWorkflowStore = store\n        self._registered_workflows: dict[str, Workflow] = {}\n        self._initial_state: dict[str, Any] = {}\n        self._persistence_backoff = (\n            list(persistence_backoff) if persistence_backoff is not None else [0.5, 3]\n        )\n\n    async def _retry_store_write(self, coro_fn: Callable[[], Awaitable[None]]) -> None:\n        \"\"\"Wrap a store write with retry/backoff.\"\"\"\n        backoffs = list(self._persistence_backoff)\n        while True:\n            try:\n                await coro_fn()\n                return\n            except Exception as e:\n                backoff = backoffs.pop(0) if backoffs else None\n                if backoff is None:\n                    logger.error(\n                        \"Store write failed after final attempt\",\n                        exc_info=True,\n                    )\n                    raise\n                logger.error(f\"Store write failed, retrying in {backoff}s: {e}\")\n                await asyncio.sleep(backoff)\n\n    # ------------------------------------------------------------------\n    # Workflow registration\n    # ------------------------------------------------------------------\n\n    @override\n    def track_workflow(self, workflow: Workflow) -> None:\n        # Keep a strong reference — the base WorkflowSet uses weak refs,\n        # so without this the workflow can be GC'd before launch().\n        self._registered_workflows[workflow.workflow_name] = workflow\n        super().track_workflow(workflow)\n\n    @override\n    def untrack_workflow(self, workflow: Workflow) -> None:\n        self._registered_workflows.pop(workflow.workflow_name, None)\n        super().untrack_workflow(workflow)\n\n    def get_workflow(self, name: str) -> Workflow | None:\n        return self._registered_workflows.get(name)\n\n    def get_workflow_names(self) -> list[str]:\n        return list(self._registered_workflows.keys())\n\n    # ------------------------------------------------------------------\n    # Adapter wiring\n    # ------------------------------------------------------------------\n\n    async def _handle_status_update(\n        self,\n        run_id: str,\n        status: Status,\n        result: StopEvent | None = None,\n        error: str | None = None,\n    ) -> None:\n        \"\"\"Callback for adapter terminal-event status updates.\"\"\"\n        await self._retry_store_write(\n            lambda: self._store.update_handler_status(\n                run_id, status=status, result=result, error=error\n            )\n        )\n\n    @override\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> ExternalRunAdapter:\n        # Intercept serialized state: we handle seeding ourselves in get_state_store\n        # so non-InMemory formats don't leak to the base runtime.\n        passthrough_state = serialized_state\n        if serialized_state and serializer:\n            self._initial_state[run_id] = (serialized_state, serializer)\n            store_type = serialized_state.get(\"store_type\")\n            if store_type is not None and store_type != \"in_memory\":\n                passthrough_state = None\n        return super().run_workflow(\n            run_id,\n            workflow,\n            init_state,\n            start_event=start_event,\n            serialized_state=passthrough_state,\n            serializer=serializer,\n        )\n\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        \"\"\"Wraps the inner runtime's adapter in _ServerInternalRunAdapter.\"\"\"\n        inner_adapter = self._decorated.get_internal_adapter(workflow)\n        state_type = infer_state_type(workflow)\n        return _ServerInternalRunAdapter(inner_adapter, self, state_type=state_type)\n\n    # ------------------------------------------------------------------\n    # Handler persistence\n    # ------------------------------------------------------------------\n\n    async def run_workflow_handler(\n        self,\n        handler_id: str,\n        workflow_name: str,\n        run_id: str,\n    ) -> None:\n        \"\"\"Persist initial handler record to store.\n\n        Must be called before the workflow is started so that\n        ``update_handler_status`` can find the handler row when\n        the workflow completes.\n        \"\"\"\n        started_at = datetime.now(timezone.utc)\n\n        await self._retry_store_write(\n            lambda: self._store.update(\n                PersistentHandler(\n                    handler_id=handler_id,\n                    workflow_name=workflow_name,\n                    status=\"running\",\n                    run_id=run_id,\n                    started_at=started_at,\n                    updated_at=started_at,\n                )\n            )\n        )\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_service.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nApplication-level orchestration layer for workflow handler lifecycle.\n\n_WorkflowService is a plain class (not a Runtime subclass) that provides\nthe public interface consumed by _api.py. It delegates to the decorated\nruntime for persistence and adapter wiring, and to the store for queries.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom datetime import datetime, timezone\nfrom typing import Literal\n\nfrom llama_agents.client.protocol import HandlerData\nfrom llama_agents.client.protocol.serializable_events import (\n    EventEnvelopeWithMetadata,\n)\nfrom llama_agents.server._runtime.server_runtime import ServerRuntimeDecorator\nfrom llama_index_instrumentation.dispatcher import instrument_tags\nfrom workflows import Context\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.events import Event, StartEvent\nfrom workflows.handler import WorkflowHandler\nfrom workflows.utils import _nanoid as nanoid\nfrom workflows.workflow import Workflow\n\nfrom ._store.abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    PersistentHandler,\n    is_terminal_status,\n)\n\nlogger = logging.getLogger(__name__)\n\n# ---------------------------------------------------------------------------\n# Exceptions\n# ---------------------------------------------------------------------------\n\n\nclass HandlerNotFoundError(Exception):\n    pass\n\n\nclass HandlerCompletedError(Exception):\n    pass\n\n\nclass EventSendError(Exception):\n    pass\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef handler_data_from_persistent(persistent: PersistentHandler) -> HandlerData:\n    return HandlerData(\n        handler_id=persistent.handler_id,\n        workflow_name=persistent.workflow_name,\n        run_id=persistent.run_id,\n        status=persistent.status,\n        started_at=persistent.started_at.isoformat()\n        if persistent.started_at is not None\n        else datetime.now(timezone.utc).isoformat(),\n        updated_at=persistent.updated_at.isoformat()\n        if persistent.updated_at is not None\n        else None,\n        completed_at=persistent.completed_at.isoformat()\n        if persistent.completed_at is not None\n        else None,\n        error=persistent.error,\n        result=EventEnvelopeWithMetadata.from_event(persistent.result)\n        if persistent.result is not None\n        else None,\n    )\n\n\n# ---------------------------------------------------------------------------\n# _WorkflowService\n# ---------------------------------------------------------------------------\n\n\nclass _WorkflowService:\n    \"\"\"Application-level service facade for workflow handler lifecycle.\n\n    This is NOT a Runtime. It holds references to the decorated runtime\n    (for running workflows and getting adapters) and the store (for queries).\n    \"\"\"\n\n    def __init__(\n        self,\n        runtime: ServerRuntimeDecorator,\n        store: AbstractWorkflowStore,\n    ) -> None:\n        self._runtime: ServerRuntimeDecorator = runtime\n        self._store = store\n\n    # ------------------------------------------------------------------\n    # Workflow registration\n    # ------------------------------------------------------------------\n\n    def get_workflow(self, name: str) -> Workflow | None:\n        return self._runtime.get_workflow(name)\n\n    def get_workflow_names(self) -> list[str]:\n        return self._runtime.get_workflow_names()\n\n    # ------------------------------------------------------------------\n    # Store access\n    # ------------------------------------------------------------------\n\n    @property\n    def store(self) -> AbstractWorkflowStore:\n        return self._store\n\n    async def query_handlers(self, query: HandlerQuery) -> list[PersistentHandler]:\n        return await self._store.query(query)\n\n    # ------------------------------------------------------------------\n    # Handler lifecycle\n    # ------------------------------------------------------------------\n\n    async def load_handler(self, handler_id: str) -> HandlerData | None:\n        found = await self._store.query(HandlerQuery(handler_id_in=[handler_id]))\n        if not found:\n            return None\n        return handler_data_from_persistent(found[0])\n\n    async def resolve_handler(self, handler_id: str) -> HandlerData:\n        handler_data = await self.load_handler(handler_id)\n        if handler_data is None:\n            raise HandlerNotFoundError()\n        if is_terminal_status(handler_data.status):\n            raise HandlerCompletedError()\n        return handler_data\n\n    async def send_event(\n        self,\n        handler_id: str,\n        event: Event,\n        step: str | None = None,\n    ) -> None:\n        \"\"\"Send a parsed event to a running handler.\"\"\"\n        handler_data = await self.resolve_handler(handler_id)\n\n        workflow = self._runtime.get_workflow(handler_data.workflow_name)\n        if workflow is None:\n            raise EventSendError(\n                f\"Workflow {handler_data.workflow_name} not registered\"\n            )\n        if handler_data.run_id is None:\n            raise EventSendError(f\"Handler {handler_id} has no run ID\")\n\n        try:\n            handler = WorkflowHandler(\n                workflow, self._runtime.get_external_adapter(handler_data.run_id)\n            )\n            await handler.send_event(event, step=step)\n        except Exception as e:\n            raise EventSendError(f\"Failed to send event: {e}\") from e\n\n    async def cancel_handler(\n        self, handler_id: str, purge: bool = False\n    ) -> Literal[\"cancelled\", \"deleted\"] | None:\n        found = await self._store.query(HandlerQuery(handler_id_in=[handler_id]))\n        if not found:\n            return None\n        persisted = handler_data_from_persistent(found[0])\n        if not purge and (\n            persisted.run_id is None or is_terminal_status(persisted.status)\n        ):\n            return None\n\n        is_terminal = is_terminal_status(persisted.status)\n        if not is_terminal and persisted.run_id is not None:\n            handler = self._workflow_run_handler(\n                persisted.workflow_name, persisted.run_id\n            )\n            await self._cancel_run(handler)\n\n        if purge:\n            n_deleted = await self._store.delete(\n                HandlerQuery(handler_id_in=[handler_id])\n            )\n            if n_deleted == 0:\n                return None\n\n        return \"deleted\" if purge else \"cancelled\"\n\n    async def start_workflow(\n        self,\n        workflow: Workflow,\n        handler_id: str,\n        start_event: StartEvent | None = None,\n        context: Context | None = None,\n    ) -> HandlerData:\n        with instrument_tags({\"llamaindex.handler_id\": handler_id}):\n            if context is None:\n                context = await self._context_from_handler_id(workflow, handler_id)\n            # Pre-generate run_id and persist the handler record BEFORE starting\n            # the workflow. This prevents a race where a fast workflow completes\n            # and tries to update_handler_status before the handler row exists,\n            # causing the status update to be silently skipped.\n            run_id = nanoid()\n            await self._runtime.run_workflow_handler(\n                handler_id, workflow.workflow_name, run_id\n            )\n            _ = workflow.run(\n                ctx=context,\n                start_event=start_event,\n                run_id=run_id,\n            )\n            handler_data = await self.load_handler(handler_id)\n            if handler_data is None:\n                raise RuntimeError(f\"Handler {handler_id} not found after creation\")\n            return handler_data\n\n    async def await_workflow(self, handler: HandlerData) -> HandlerData:\n        if handler.run_id is None:\n            raise HandlerNotFoundError(\"Handler exists, but has no run ID\")\n        run = self._workflow_run_handler(handler.workflow_name, handler.run_id)\n\n        try:\n            await run\n        except Exception:\n            logger.error(\n                \"Workflow %s (handler=%s, run=%s) raised an exception\",\n                handler.workflow_name,\n                handler.handler_id,\n                handler.run_id,\n                exc_info=True,\n            )\n        handler_data = await self.load_handler(handler.handler_id)\n        if handler_data is None:\n            raise HandlerNotFoundError()\n        return handler_data\n\n    # ------------------------------------------------------------------\n    # Start / stop\n    # ------------------------------------------------------------------\n\n    async def start(self) -> None:\n        \"\"\"Launch runtimes and register tracked workflows.\"\"\"\n        await self._runtime.launch()\n\n    async def stop(self) -> None:\n        \"\"\"Stop active runs and destroy the runtime.\"\"\"\n        await self._runtime.destroy()\n\n    # ------------------------------------------------------------------\n    # Private helpers\n    # ------------------------------------------------------------------\n\n    async def _context_from_handler_id(\n        self, workflow: Workflow, handler_id: str\n    ) -> Context | None:\n        \"\"\"Look up a completed handler's final state and build a Context from it.\n\n        Returns the lightweight serialized state reference so that SQL-backed\n        stores can do an optimized copy rather than round-tripping through memory.\n\n        Returns None if the handler doesn't exist, isn't completed, or has no state.\n        \"\"\"\n        found = await self._store.query(HandlerQuery(handler_id_in=[handler_id]))\n        if not found:\n            return None\n        handler = found[0]\n        if not is_terminal_status(handler.status) or handler.run_id is None:\n            return None\n\n        try:\n            serializer = JsonSerializer()\n            old_state_store = self._store.create_state_store(handler.run_id)\n            state_dict = old_state_store.to_dict(serializer)\n            if not state_dict:\n                return None\n            return Context.from_dict(\n                workflow=workflow,\n                data={\"version\": 1, \"state\": state_dict},\n                serializer=serializer,\n            )\n        except Exception:\n            logger.warning(\n                \"Failed to read state from previous handler %s\",\n                handler_id,\n                exc_info=True,\n            )\n            return None\n\n    def _workflow_run_handler(self, workflow_name: str, run_id: str) -> WorkflowHandler:\n        workflow = self._runtime.get_workflow(workflow_name)\n        if workflow is None:\n            raise HandlerNotFoundError(f\"Workflow {workflow_name} not registered\")\n        return WorkflowHandler(\n            workflow=workflow,\n            external_adapter=workflow._runtime.get_external_adapter(run_id),\n        )\n\n    async def _cancel_run(self, run: WorkflowHandler) -> None:\n        \"\"\"Gracefully cancel the workflow run, then kill tasks.\"\"\"\n        if not run.done():\n            try:\n                await run.cancel_run()\n            except Exception:\n                pass\n            try:\n                await run\n            except (asyncio.CancelledError, Exception):\n                pass\n        await self._kill_run(run)\n\n    async def _kill_run(self, run: WorkflowHandler) -> None:\n        \"\"\"Force-kill the handler without graceful cancellation.\"\"\"\n        if not run.done():\n            try:\n                run.cancel()\n            except Exception:\n                pass\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nSQLITE_MIGRATION_SOURCE: tuple[str, str] = (\n    \"server\",\n    \"llama_agents.server._store.sqlite.migrations\",\n)\n\nPOSTGRES_MIGRATION_SOURCE: tuple[str, str] = (\n    \"server\",\n    \"llama_agents.server._store.postgres.migrations\",\n)\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/abstract_workflow_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom abc import ABC, abstractmethod\nfrom collections.abc import AsyncIterator\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom typing import Any, Literal, Protocol, runtime_checkable\n\nfrom llama_agents.client.protocol.serializable_events import (\n    EventEnvelopeWithMetadata,\n)\nfrom pydantic import (\n    BaseModel,\n    field_serializer,\n    field_validator,\n)\nfrom workflows.context import JsonSerializer\nfrom workflows.context.serializers import BaseSerializer\nfrom workflows.context.state_store import StateStore\nfrom workflows.events import StopEvent\nfrom workflows.runtime.types.ticks import WorkflowTick, WorkflowTickAdapter\n\nlogger = logging.getLogger(__name__)\n\nStatus = Literal[\"running\", \"completed\", \"failed\", \"cancelled\"]\n\nTERMINAL_STATUSES: frozenset[Status] = frozenset((\"completed\", \"failed\", \"cancelled\"))\n\n\ndef is_terminal_status(status: Status) -> bool:\n    return status in TERMINAL_STATUSES\n\n\nclass _Unset(Enum):\n    UNSET = \"UNSET\"\n\n\n_UNSET = _Unset.UNSET\n\n\n@dataclass()\nclass HandlerQuery:\n    # Matches if any of the handler_ids match\n    handler_id_in: list[str] | None = None\n    # Matches if any of the run_ids match\n    run_id_in: list[str] | None = None\n    # Matches if any of the workflow_names match\n    workflow_name_in: list[str] | None = None\n    # Matches if the status flag matches\n    status_in: list[Status] | None = None\n    # True = only idle handlers, False = only non-idle handlers, None = all\n    is_idle: bool | None = None\n\n\nclass PersistentHandler(BaseModel):\n    handler_id: str\n    workflow_name: str\n    status: Status\n    run_id: str | None = None\n    error: str | None = None\n    result: StopEvent | None = None\n    started_at: datetime | None = None\n    updated_at: datetime | None = None\n    completed_at: datetime | None = None\n    idle_since: datetime | None = None\n\n    @field_validator(\"result\", mode=\"before\")\n    @classmethod\n    def _parse_stop_event(cls, data: Any) -> StopEvent | None:\n        if isinstance(data, StopEvent):\n            return data\n        elif isinstance(data, dict):\n            deserialized = JsonSerializer().deserialize_value(data)\n            if isinstance(deserialized, StopEvent):\n                return deserialized\n            else:\n                return StopEvent(result=data)\n        elif data is None:\n            return None\n        else:\n            return StopEvent(result=data)\n\n    @field_serializer(\"result\", mode=\"plain\")\n    def _serialize_stop_event(self, data: StopEvent | None) -> Any:\n        if data is None:\n            return None\n        result = JsonSerializer().serialize_value(data)\n        return result\n\n\nclass StoredTick(BaseModel):\n    run_id: str\n    sequence: int\n    timestamp: datetime\n    tick_data: dict[str, Any]\n\n\nclass StoredEvent(BaseModel):\n    run_id: str\n    sequence: int\n    timestamp: datetime\n    event: EventEnvelopeWithMetadata\n\n\nclass AbstractWorkflowStore(ABC):\n    poll_interval: float = 0.1\n\n    @abstractmethod\n    def create_state_store(\n        self,\n        run_id: str,\n        state_type: type[Any] | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> StateStore[Any]:\n        \"\"\"Create a persistent state store for the given run.\n\n        If *serialized_state* and *serializer* are provided, the store is\n        seeded from the serialized data during construction.\n        \"\"\"\n\n    @abstractmethod\n    async def query(self, query: HandlerQuery) -> list[PersistentHandler]: ...\n\n    @abstractmethod\n    async def update(self, handler: PersistentHandler) -> None: ...\n\n    @abstractmethod\n    async def delete(self, query: HandlerQuery) -> int: ...\n\n    @abstractmethod\n    async def append_event(\n        self, run_id: str, event: EventEnvelopeWithMetadata\n    ) -> None: ...\n\n    @abstractmethod\n    async def query_events(\n        self, run_id: str, after_sequence: int | None = None, limit: int | None = None\n    ) -> list[StoredEvent]: ...\n\n    @abstractmethod\n    async def append_tick(self, run_id: str, tick_data: dict[str, Any]) -> None: ...\n\n    @abstractmethod\n    async def get_ticks(self, run_id: str) -> list[StoredTick]: ...\n\n    async def stream_ticks(self, run_id: str) -> AsyncIterator[StoredTick]:\n        \"\"\"Async-iterate stored ticks in sequence order (ascending).\n\n        Default loads all ticks via :meth:`get_ticks`. Override for true\n        streaming (e.g. cursor-based pagination).\n        \"\"\"\n        for tick in await self.get_ticks(run_id):\n            yield tick\n\n    async def after_tick(self, run_id: str, tick_data: dict[str, Any]) -> None:\n        \"\"\"Called after a tick's commands have been processed.\n\n        Stores can override to gather in-flight writes, update caches, etc.\n        Default is no-op.\n        \"\"\"\n        pass\n\n    async def update_handler_status(\n        self,\n        run_id: str,\n        *,\n        status: Status | None = None,\n        result: StopEvent | None = None,\n        error: str | None = None,\n        idle_since: datetime | None | _Unset = _UNSET,\n    ) -> None:\n        \"\"\"Update status and related fields for an existing handler.\n\n        Loads the handler by run_id, updates status/timestamps/provided fields,\n        and writes back. If the handler is not found, logs a warning and returns.\n        \"\"\"\n        found = await self.query(HandlerQuery(run_id_in=[run_id]))\n        if not found:\n            logger.warning(\"update_handler_status: run %s not found, skipping\", run_id)\n            return\n        handler = found[0]\n        now = datetime.now(timezone.utc)\n        if status is not None:\n            handler.status = status\n        handler.updated_at = now\n        if status in (\"completed\", \"failed\", \"cancelled\"):\n            handler.completed_at = now\n        if result is not None:\n            handler.result = result\n        if error is not None:\n            handler.error = error\n        if not isinstance(idle_since, _Unset):\n            handler.idle_since = idle_since\n        await self.update(handler)\n\n    @staticmethod\n    def _is_terminal_event(event: StoredEvent) -> bool:\n        \"\"\"Check if a stored event is terminal (StopEvent or subclass, etc.).\"\"\"\n\n        types = (event.event.types or []) + [event.event.type]\n        return StopEvent.__name__ in types\n\n    async def subscribe_events(\n        self, run_id: str, after_sequence: int = -1\n    ) -> AsyncIterator[StoredEvent]:\n        \"\"\"Stream events starting after *after_sequence*, yielding in real time.\n\n        The default implementation polls via :meth:`query_events`.\n        :class:`MemoryWorkflowStore` overrides this with condition-based\n        notification so there is no polling.\n\n        The iterator terminates once a terminal event\n        (``StopEvent``, ``WorkflowFailedEvent``, ``WorkflowCancelledEvent``)\n        is yielded.\n        \"\"\"\n        cursor = after_sequence\n        while True:\n            events = await self.query_events(run_id, after_sequence=cursor)\n            for event in events:\n                yield event\n                cursor = event.sequence\n                if self._is_terminal_event(event):\n                    return\n            if not events:\n                await asyncio.sleep(self.poll_interval)\n\n\n@runtime_checkable\nclass LegacyContextStore(Protocol):\n    \"\"\"Opt-in protocol for stores that can provide old serialized context data from the ctx column.\"\"\"\n\n    def get_legacy_ctx(self, run_id: str) -> dict[str, Any] | None:\n        \"\"\"Return the old serialized context dict for a run, or None if not available.\"\"\"\n        ...\n\n\ndef as_legacy_context_store(store: AbstractWorkflowStore) -> LegacyContextStore | None:\n    \"\"\"Return the store as a LegacyContextStore if it supports it, else None.\"\"\"\n    if isinstance(store, LegacyContextStore):\n        return store\n    return None\n\n\nasync def stream_workflow_ticks(\n    store: AbstractWorkflowStore,\n    run_id: str,\n) -> AsyncIterator[WorkflowTick]:\n    \"\"\"Stream validated WorkflowTick objects for *run_id* from *store*.\"\"\"\n    async for stored in store.stream_ticks(run_id):\n        yield WorkflowTickAdapter.validate_python(stored.tick_data)\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/agent_data_client.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"AgentDataClient — shared HTTP client for the LlamaCloud Agent Data API.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nimport httpx\n\nlogger = logging.getLogger(__name__)\n\n\nclass AgentDataClient:\n    \"\"\"HTTP client for the LlamaCloud Agent Data API.\n\n    Holds connection parameters and exposes search/create/update/delete methods.\n    Both AgentDataStore and AgentDataStateStore use this instead of duplicating\n    HTTP helpers.\n\n    Uses a shared ``httpx.AsyncClient`` for connection pooling. The client is\n    lazily created on first use to avoid requiring an event loop at init time.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        base_url: str,\n        api_key: str,\n        project_id: str,\n        deployment_name: str,\n    ) -> None:\n        self._base_url = base_url.rstrip(\"/\")\n        self._api_key = api_key\n        self._project_id = project_id\n        self._deployment_name = deployment_name\n        self._shared_client: httpx.AsyncClient | None = None\n\n    @property\n    def deployment_name(self) -> str:\n        return self._deployment_name\n\n    def _headers(self) -> dict[str, str]:\n        return {\n            \"Authorization\": f\"Bearer {self._api_key}\",\n            \"Content-Type\": \"application/json\",\n        }\n\n    def http_client(self) -> httpx.AsyncClient:\n        \"\"\"Return the shared async HTTP client, creating it lazily.\n\n        The client is reused across operations for connection pooling.\n        ``httpx.AsyncClient`` is safe for concurrent use.\n        \"\"\"\n        if self._shared_client is None or self._shared_client.is_closed:\n            self._shared_client = httpx.AsyncClient(\n                base_url=self._base_url,\n                headers=self._headers(),\n                params={\"project_id\": self._project_id},\n            )\n        return self._shared_client\n\n    async def close(self) -> None:\n        \"\"\"Close the shared HTTP client and release connections.\"\"\"\n        if self._shared_client is not None and not self._shared_client.is_closed:\n            await self._shared_client.aclose()\n            self._shared_client = None\n\n    async def search(\n        self,\n        collection: str,\n        filters: dict[str, Any] | None = None,\n        page_size: int = 100,\n        order_by: str | None = None,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Search the Agent Data API and return matching items.\"\"\"\n        body: dict[str, Any] = {\n            \"deployment_name\": self._deployment_name,\n            \"collection\": collection,\n            \"page_size\": page_size,\n        }\n        if filters:\n            body[\"filter\"] = filters\n        if order_by:\n            body[\"order_by\"] = order_by\n        client = self.http_client()\n        resp = await client.post(\"/api/v1/beta/agent-data/:search\", json=body)\n        resp.raise_for_status()\n        return resp.json().get(\"items\", [])\n\n    async def create(self, collection: str, data: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Create an item in the Agent Data API.\"\"\"\n        body = {\n            \"deployment_name\": self._deployment_name,\n            \"collection\": collection,\n            \"data\": data,\n        }\n        client = self.http_client()\n        resp = await client.post(\"/api/v1/beta/agent-data\", json=body)\n        resp.raise_for_status()\n        return resp.json()\n\n    async def update_item(self, item_id: str, data: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Update an existing item by its Agent Data API ID.\"\"\"\n        client = self.http_client()\n        resp = await client.put(\n            f\"/api/v1/beta/agent-data/{item_id}\",\n            json={\"data\": data},\n        )\n        resp.raise_for_status()\n        return resp.json()\n\n    async def delete_item(self, item_id: str) -> None:\n        \"\"\"Delete an item by its Agent Data API ID.\"\"\"\n        client = self.http_client()\n        resp = await client.delete(f\"/api/v1/beta/agent-data/{item_id}\")\n        resp.raise_for_status()\n\n    async def delete_many(\n        self,\n        collection: str,\n        filters: dict[str, Any],\n    ) -> int:\n        \"\"\"Delete items matching the given filters. Returns the number deleted.\"\"\"\n        body: dict[str, Any] = {\n            \"deployment_name\": self._deployment_name,\n            \"collection\": collection,\n            \"filter\": filters,\n        }\n        client = self.http_client()\n        resp = await client.post(\"/api/v1/beta/agent-data/:delete\", json=body)\n        resp.raise_for_status()\n        return resp.json().get(\"deleted_count\", 0)\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/agent_data_state_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"AgentDataStateStore — StateStore backed by the LlamaCloud Agent Data API.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport json\nimport logging\nfrom contextlib import asynccontextmanager\nfrom typing import Any, AsyncGenerator, Generic, Literal\n\nfrom pydantic import BaseModel\nfrom typing_extensions import TypeVar\nfrom workflows.context.serializers import BaseSerializer, JsonSerializer\nfrom workflows.context.state_store import (\n    DictState,\n    create_cleared_state,\n    deserialize_dict_state_data,\n    get_by_path,\n    merge_state,\n    serialize_dict_state_data,\n    set_by_path,\n)\n\nfrom .agent_data_client import AgentDataClient\n\nlogger = logging.getLogger(__name__)\n\nMODEL_T = TypeVar(\"MODEL_T\", bound=BaseModel, default=DictState)  # type: ignore[reportGeneralTypeIssues]\n\n_FIELD_RUN_ID = \"run_id\"\n_FIELD_DATA = \"data\"\n\n\nclass _StoredStateRecord(BaseModel):\n    \"\"\"Validates the shape persisted in the Agent Data API.\"\"\"\n\n    run_id: str\n    data: str\n\n\nclass AgentDataSerializedState(BaseModel):\n    \"\"\"Serialized state referencing an agent data store.\"\"\"\n\n    store_type: Literal[\"agent_data\"] = \"agent_data\"\n    run_id: str\n    collection: str = \"workflow_state\"\n\n\nclass AgentDataStateStore(Generic[MODEL_T]):\n    \"\"\"StateStore backed by the LlamaCloud Agent Data API.\n\n    Uses a single item in a ``workflow_state`` collection, keyed by ``run_id``.\n    \"\"\"\n\n    state_type: type[MODEL_T]\n\n    def __init__(\n        self,\n        *,\n        client: AgentDataClient,\n        run_id: str,\n        state_type: type[MODEL_T] | None = None,\n        collection: str = \"workflow_state\",\n        serializer: BaseSerializer | None = None,\n    ) -> None:\n        self._client = client\n        self._run_id = run_id\n        self.state_type = state_type or DictState  # type: ignore[assignment]  # ty: ignore[invalid-assignment]\n        self._collection = collection\n        self._serializer = serializer or JsonSerializer()\n        # Cache the agent data item ID once found\n        self._item_id: str | None = None\n        # Write-through state cache — avoids HTTP searches when state is\n        # already known from a previous load or save.\n        self._cached_state: MODEL_T | None = None\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    @functools.cached_property\n    def _lock(self) -> asyncio.Lock:\n        return asyncio.Lock()\n\n    # ------------------------------------------------------------------\n    # State serialization\n    # ------------------------------------------------------------------\n\n    def _serialize_state(self, state: MODEL_T) -> str:\n        if isinstance(state, DictState):\n            return json.dumps(serialize_dict_state_data(state, self._serializer))\n        return self._serializer.serialize(state)\n\n    def _deserialize_state(self, state_json: str) -> MODEL_T:\n        if issubclass(self.state_type, DictState):\n            data = json.loads(state_json)\n            return deserialize_dict_state_data(data, self._serializer)  # type: ignore[return-value]  # ty: ignore[invalid-return-type]\n        return self._serializer.deserialize(state_json)\n\n    def _create_default_state(self) -> MODEL_T:\n        return self.state_type()\n\n    # ------------------------------------------------------------------\n    # Load / save through API\n    # ------------------------------------------------------------------\n\n    async def _load_record(self) -> _StoredStateRecord | None:\n        items = await self._client.search(\n            self._collection,\n            {_FIELD_RUN_ID: {\"eq\": self._run_id}},\n            page_size=1,\n        )\n        if not items:\n            return None\n        self._item_id = items[0][\"id\"]\n        return _StoredStateRecord.model_validate(items[0][\"data\"])\n\n    async def _load_state(self) -> MODEL_T:\n        if self._cached_state is not None:\n            return self._cached_state.model_copy()\n        record = await self._load_record()\n        if record is not None:\n            state = self._deserialize_state(record.data)\n            self._cached_state = state\n            return state.model_copy()\n        state = self._create_default_state()\n        await self._save_state(state)\n        return state.model_copy()\n\n    async def _load_state_or_none(self) -> MODEL_T | None:\n        if self._cached_state is not None:\n            return self._cached_state.model_copy()\n        record = await self._load_record()\n        if record is not None:\n            state = self._deserialize_state(record.data)\n            self._cached_state = state\n            return state.model_copy()\n        return None\n\n    async def _save_state(self, state: BaseModel) -> None:\n        record = _StoredStateRecord(\n            run_id=self._run_id,\n            data=self._serialize_state(state),  # type: ignore[arg-type]\n        )\n        payload = record.model_dump()\n        if self._item_id is not None:\n            await self._client.update_item(self._item_id, payload)\n        else:\n            items = await self._client.search(\n                self._collection,\n                {_FIELD_RUN_ID: {\"eq\": self._run_id}},\n                page_size=1,\n            )\n            if items:\n                item_id = items[0][\"id\"]\n                self._item_id = item_id\n                await self._client.update_item(item_id, payload)\n            else:\n                result = await self._client.create(self._collection, payload)\n                self._item_id = result[\"id\"]\n        self._cached_state = state.model_copy()  # type: ignore[assignment]  # ty: ignore[invalid-assignment]\n\n    # ------------------------------------------------------------------\n    # StateStore protocol\n    # ------------------------------------------------------------------\n\n    async def get_state(self) -> MODEL_T:\n        return await self._load_state()\n\n    async def set_state(self, state: MODEL_T) -> None:\n        current = await self._load_state_or_none()\n        if current is None:\n            await self._save_state(state)\n            return\n        merged = merge_state(current, state)\n        await self._save_state(merged)\n\n    async def get(self, path: str, default: Any = ...) -> Any:\n        state = await self._load_state()\n        return get_by_path(state, path, default)\n\n    async def set(self, path: str, value: Any) -> None:\n        async with self.edit_state() as state:\n            set_by_path(state, path, value)\n\n    async def clear(self) -> None:\n        cleared = create_cleared_state(self.state_type)\n        await self._save_state(cleared)\n\n    @asynccontextmanager\n    async def edit_state(self) -> AsyncGenerator[MODEL_T, None]:\n        async with self._lock:\n            state = await self._load_state()\n            yield state\n            await self._save_state(state)\n\n    def to_dict(self, serializer: BaseSerializer) -> dict[str, Any]:\n        payload = AgentDataSerializedState(\n            run_id=self._run_id, collection=self._collection\n        )\n        return payload.model_dump()\n\n    @classmethod\n    def from_dict(\n        cls,\n        serialized_state: dict[str, Any],\n        serializer: BaseSerializer,\n        *,\n        client: AgentDataClient,\n        state_type: type[BaseModel] | None = None,\n        run_id: str | None = None,\n    ) -> AgentDataStateStore[Any]:\n        if not serialized_state:\n            raise ValueError(\"Cannot restore AgentDataStateStore from empty dict\")\n        parsed = AgentDataSerializedState.model_validate(serialized_state)\n        effective_run_id = run_id or parsed.run_id\n        return cls(\n            client=client,\n            run_id=effective_run_id,\n            state_type=state_type,  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]\n            collection=parsed.collection,\n            serializer=serializer,\n        )\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/agent_data_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"AgentDataStore — AbstractWorkflowStore backed by the LlamaCloud Agent Data API.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom collections.abc import AsyncIterator\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom workflows.context.serializers import BaseSerializer\nfrom workflows.context.state_store import StateStore\n\nfrom .._keyed_lock import KeyedLock\nfrom .._lru_cache import LRUCache\nfrom .abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    PersistentHandler,\n    StoredEvent,\n    StoredTick,\n)\nfrom .agent_data_client import AgentDataClient\nfrom .agent_data_state_store import AgentDataStateStore\n\nlogger = logging.getLogger(__name__)\n\n_TICK_PAGE_SIZE = 100\n\n\nclass AgentDataStore(AbstractWorkflowStore):\n    \"\"\"Workflow store backed by the LlamaCloud Agent Data API.\n\n    Optimized for streaming performance:\n    - Same-process subscribers receive events via in-memory queues (no HTTP).\n    - Tick and event writes are fire-and-forget, gathered at step boundaries.\n    - Terminal events gather all pending writes before cleanup.\n    - HTTP connections are reused across operations.\n\n    State stores are in-memory (``InMemoryStateStore``) — workflow state\n    is reconstructed from ticks on reload, so cloud persistence of the\n    mutable state object is unnecessary.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        base_url: str,\n        api_key: str,\n        project_id: str,\n        deployment_name: str,\n        collection: str = \"workflow_contexts\",\n        poll_interval: float = 30.0,\n    ) -> None:\n        self._client = AgentDataClient(\n            base_url=base_url,\n            api_key=api_key,\n            project_id=project_id,\n            deployment_name=deployment_name,\n        )\n        self._collection = collection\n        self.poll_interval = poll_interval\n\n        self._events_collection = f\"{collection}_events\"\n        self._ticks_collection = f\"{collection}_ticks\"\n\n        self._id_cache: LRUCache[str, str] = LRUCache(maxsize=256)\n        self._locks = KeyedLock()\n\n        self._event_sequences: dict[str, int] = {}\n        self._tick_sequences: dict[str, int] = {}\n        self._event_seq_lock: asyncio.Lock | None = None\n        self._tick_seq_lock: asyncio.Lock | None = None\n\n        self._subscriber_queues: dict[str, list[asyncio.Queue[StoredEvent | None]]] = {}\n        self._state_stores: dict[str, AgentDataStateStore[Any]] = {}\n        self._pending_ticks: dict[str, list[asyncio.Task[Any]]] = {}\n        self._pending_events: dict[str, list[asyncio.Task[Any]]] = {}\n\n    def _get_event_seq_lock(self) -> asyncio.Lock:\n        if self._event_seq_lock is None:\n            self._event_seq_lock = asyncio.Lock()\n        return self._event_seq_lock\n\n    def _get_tick_seq_lock(self) -> asyncio.Lock:\n        if self._tick_seq_lock is None:\n            self._tick_seq_lock = asyncio.Lock()\n        return self._tick_seq_lock\n\n    # ------------------------------------------------------------------\n    # In-memory subscriber helpers\n    # ------------------------------------------------------------------\n\n    def _add_subscriber_queue(self, run_id: str) -> asyncio.Queue[StoredEvent | None]:\n        \"\"\"Create and register a new subscriber queue for a run.\"\"\"\n        queue: asyncio.Queue[StoredEvent | None] = asyncio.Queue()\n        self._subscriber_queues.setdefault(run_id, []).append(queue)\n        return queue\n\n    def _remove_subscriber_queue(\n        self, run_id: str, queue: asyncio.Queue[StoredEvent | None]\n    ) -> None:\n        \"\"\"Unregister a subscriber queue. Cleans up the list if empty.\"\"\"\n        queues = self._subscriber_queues.get(run_id)\n        if queues is not None:\n            try:\n                queues.remove(queue)\n            except ValueError:\n                pass\n            if not queues:\n                del self._subscriber_queues[run_id]\n\n    def _broadcast_to_subscribers(self, run_id: str, event: StoredEvent) -> None:\n        \"\"\"Deliver an event to all in-memory subscriber queues for a run.\"\"\"\n        for queue in self._subscriber_queues.get(run_id, ()):\n            queue.put_nowait(event)\n\n    def _track_pending(\n        self,\n        pending: dict[str, list[asyncio.Task[Any]]],\n        run_id: str,\n        collection: str,\n        data: dict[str, Any],\n    ) -> None:\n        \"\"\"Create a fire-and-forget task and track it in the pending dict.\"\"\"\n        task = asyncio.create_task(self._client.create(collection, data))\n        tasks = pending.setdefault(run_id, [])\n        tasks.append(task)\n        if len(tasks) > 50:\n            pending[run_id] = [t for t in tasks if not t.done()]\n\n    @staticmethod\n    async def _regroup(\n        pending: dict[str, list[asyncio.Task[Any]]], run_id: str\n    ) -> None:\n        \"\"\"Await all in-flight tasks for a run. Raises the first error.\"\"\"\n        tasks = pending.pop(run_id, [])\n        if not tasks:\n            return\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n        errors = [r for r in results if isinstance(r, BaseException)]\n        if errors:\n            raise errors[0]\n\n    async def _regroup_ticks(self, run_id: str) -> None:\n        await self._regroup(self._pending_ticks, run_id)\n\n    async def _regroup_events(self, run_id: str) -> None:\n        await self._regroup(self._pending_events, run_id)\n\n    async def after_tick(self, run_id: str, tick_data: dict[str, Any]) -> None:\n        \"\"\"Gather all in-flight tick and event writes for a run.\"\"\"\n        await self._regroup_ticks(run_id)\n        await self._regroup_events(run_id)\n\n    async def _cleanup_run(self, run_id: str) -> None:\n        \"\"\"Clean up pending writes and subscriber queues for a completed run.\"\"\"\n        await self._regroup_ticks(run_id)\n        await self._regroup_events(run_id)\n        # Signal subscribers that the run is done, then remove the key\n        for queue in self._subscriber_queues.get(run_id, []):\n            queue.put_nowait(None)\n        self._subscriber_queues.pop(run_id, None)\n        # Clean up sequence counters and cached state store\n        self._event_sequences.pop(run_id, None)\n        self._tick_sequences.pop(run_id, None)\n        self._state_stores.pop(run_id, None)\n\n    # ------------------------------------------------------------------\n    # Sequence helpers\n    # ------------------------------------------------------------------\n\n    async def _max_sequence(self, collection: str, run_id: str) -> int:\n        \"\"\"Query the API for the max sequence in a collection for a run_id.\n\n        Returns -1 if no items exist.\n        \"\"\"\n        items = await self._client.search(\n            collection,\n            {\"run_id\": {\"eq\": run_id}},\n            page_size=1,\n            order_by=\"sequence desc\",\n        )\n        if items:\n            return items[0][\"data\"].get(\"sequence\", -1)\n        return -1\n\n    # ------------------------------------------------------------------\n    # Handler CRUD\n    # ------------------------------------------------------------------\n\n    @staticmethod\n    def _in_filter(field: str, values: list[Any] | None) -> tuple[bool, dict[str, Any]]:\n        \"\"\"Build an ``includes`` filter for *field*.\n\n        Returns ``(ok, filter_fragment)`` where *ok* is ``False`` when the\n        caller should short-circuit with \"match nothing\" (empty list).\n        \"\"\"\n        if values is None:\n            return True, {}\n        if len(values) == 0:\n            return False, {}\n        return True, {field: {\"includes\": values}}\n\n    @staticmethod\n    def _build_handler_filters(query: HandlerQuery) -> dict[str, Any] | None:\n        \"\"\"Convert a HandlerQuery to Agent Data API filter format.\n\n        Returns ``{}`` for \"match everything\" or ``None`` for \"match nothing\".\n        \"\"\"\n        filters: dict[str, Any] = {}\n\n        for field, values in [\n            (\"handler_id\", query.handler_id_in),\n            (\"run_id\", query.run_id_in),\n            (\"workflow_name\", query.workflow_name_in),\n            (\"status\", query.status_in),\n        ]:\n            ok, fragment = AgentDataStore._in_filter(field, values)\n            if not ok:\n                return None\n            filters.update(fragment)\n\n        if query.is_idle is not None:\n            if query.is_idle:\n                filters[\"idle_since\"] = {\"ne\": None}\n            else:\n                filters[\"idle_since\"] = {\"eq\": None}\n\n        return filters\n\n    @staticmethod\n    def _item_to_handler(item: dict[str, Any]) -> PersistentHandler:\n        \"\"\"Convert an Agent Data API item to a PersistentHandler.\"\"\"\n        data = item[\"data\"]\n        return PersistentHandler.model_validate(data)\n\n    async def query(self, query: HandlerQuery) -> list[PersistentHandler]:\n        filters = self._build_handler_filters(query)\n        if filters is None:\n            return []\n\n        items = await self._client.search(self._collection, filters or None)\n        handlers = [self._item_to_handler(item) for item in items]\n        return handlers\n\n    async def update(self, handler: PersistentHandler) -> None:\n        data = handler.model_dump(mode=\"json\")\n        handler_id = handler.handler_id\n\n        async with self._locks(handler_id):\n            # Check cache for existing agent_data_id\n            cached_id = self._id_cache.get(handler_id)\n            if cached_id is not None:\n                await self._client.update_item(cached_id, data)\n                return\n\n            # Search for existing item. Sort newest-first so that items[-1]\n            # is deterministically the oldest row — relied on by the\n            # dedupe-survivor choice below.\n            items = await self._client.search(\n                self._collection,\n                {\"handler_id\": {\"eq\": handler_id}},\n                order_by=\"created_at desc\",\n            )\n            if not items:\n                result = await self._client.create(self._collection, data)\n                self._id_cache.put(handler_id, result[\"id\"])\n                return\n\n            if len(items) == 1:\n                item_id = items[0][\"id\"]\n                self._id_cache.put(handler_id, item_id)\n                await self._client.update_item(item_id, data)\n                return\n\n            # Duplicate handler rows — converge to one survivor. The\n            # invariant is one row per handler_id; run_id is a mutable field\n            # on the row, so mismatched run_ids just mean an earlier run was\n            # superseded. Collapse regardless. Oldest survivor preserves the\n            # row's original created_at.\n            survivor_id = items[-1][\"id\"]\n            victim_ids = [item[\"id\"] for item in items[:-1]]\n            logger.warning(\n                \"Collapsing %d duplicate rows for handler %s; survivor=%s\",\n                len(items),\n                handler_id,\n                survivor_id,\n            )\n            # Tolerate partial delete failures. The next update() will see\n            # whatever duplicates remain and retry; letting one failed delete\n            # abort the whole operation would also skip the survivor write,\n            # leaving the row stale.\n            results = await asyncio.gather(\n                *(self._client.delete_item(vid) for vid in victim_ids),\n                return_exceptions=True,\n            )\n            for vid, outcome in zip(victim_ids, results):\n                if isinstance(outcome, BaseException):\n                    logger.warning(\n                        \"Failed to delete duplicate handler row %s for %s: %s\",\n                        vid,\n                        handler_id,\n                        outcome,\n                    )\n            self._id_cache.put(handler_id, survivor_id)\n            await self._client.update_item(survivor_id, data)\n\n    async def delete(self, query: HandlerQuery) -> int:\n        filters = self._build_handler_filters(query)\n        if filters is None:\n            return 0\n\n        # Invalidate cached IDs for matching handlers before bulk delete\n        items = await self._client.search(self._collection, filters or None)\n        for item in items:\n            handler_id = item[\"data\"].get(\"handler_id\")\n            if handler_id:\n                self._id_cache.delete(handler_id)\n\n        if not items:\n            return 0\n\n        return await self._client.delete_many(self._collection, filters or {})\n\n    # ------------------------------------------------------------------\n    # Event journal\n    # ------------------------------------------------------------------\n\n    async def _next_event_sequence(self, run_id: str) -> int:\n        async with self._get_event_seq_lock():\n            if run_id not in self._event_sequences:\n                self._event_sequences[run_id] = await self._max_sequence(\n                    self._events_collection, run_id\n                )\n            seq = self._event_sequences[run_id] + 1\n            self._event_sequences[run_id] = seq\n            return seq\n\n    async def append_event(self, run_id: str, event: EventEnvelopeWithMetadata) -> None:\n        seq = await self._next_event_sequence(run_id)\n        now = datetime.now(timezone.utc)\n        stored = StoredEvent(\n            run_id=run_id,\n            sequence=seq,\n            timestamp=now,\n            event=event,\n        )\n\n        # Instant in-memory delivery\n        self._broadcast_to_subscribers(run_id, stored)\n\n        # Fire-and-forget HTTP persistence\n        self._track_pending(\n            self._pending_events,\n            run_id,\n            self._events_collection,\n            stored.model_dump(mode=\"json\"),\n        )\n\n        if self._is_terminal_event(stored):\n            await self._cleanup_run(run_id)\n\n    async def query_events(\n        self,\n        run_id: str,\n        after_sequence: int | None = None,\n        limit: int | None = None,\n    ) -> list[StoredEvent]:\n        await self._regroup_events(run_id)\n\n        filters: dict[str, Any] = {\"run_id\": {\"eq\": run_id}}\n        if after_sequence is not None:\n            filters[\"sequence\"] = {\"gte\": after_sequence + 1}\n\n        items = await self._client.search(\n            self._events_collection,\n            filters,\n            page_size=limit or 1000,\n            order_by=\"sequence\",\n        )\n\n        return [StoredEvent.model_validate(item[\"data\"]) for item in items]\n\n    # ------------------------------------------------------------------\n    # Event subscription\n    # ------------------------------------------------------------------\n\n    async def subscribe_events(\n        self, run_id: str, after_sequence: int = -1\n    ) -> AsyncIterator[StoredEvent]:\n        \"\"\"In-memory queue-based subscription.\n\n        Subscribes to the queue *before* running backfill to avoid losing\n        events in the race window. Deduplicates by sequence number.\n        \"\"\"\n        # Register queue before backfill to avoid race condition\n        queue = self._add_subscriber_queue(run_id)\n        try:\n            cursor = after_sequence\n\n            # Backfill: yield historical events already persisted\n            backfill = await self.query_events(run_id, after_sequence=cursor)\n            for event in backfill:\n                yield event\n                cursor = event.sequence\n                if self._is_terminal_event(event):\n                    return\n\n            # Stream from in-memory queue\n            while True:\n                event = await queue.get()\n                if event is None:\n                    # Run completed, queue was signaled\n                    return\n                # Deduplicate: skip events already yielded in backfill\n                if event.sequence <= cursor:\n                    continue\n                yield event\n                cursor = event.sequence\n                if self._is_terminal_event(event):\n                    return\n        finally:\n            self._remove_subscriber_queue(run_id, queue)\n\n    # ------------------------------------------------------------------\n    # Tick journal\n    # ------------------------------------------------------------------\n\n    async def _next_tick_sequence(self, run_id: str) -> int:\n        async with self._get_tick_seq_lock():\n            if run_id not in self._tick_sequences:\n                self._tick_sequences[run_id] = await self._max_sequence(\n                    self._ticks_collection, run_id\n                )\n            seq = self._tick_sequences[run_id] + 1\n            self._tick_sequences[run_id] = seq\n            return seq\n\n    async def append_tick(self, run_id: str, tick_data: dict[str, Any]) -> None:\n        seq = await self._next_tick_sequence(run_id)\n        now = datetime.now(timezone.utc)\n        stored = StoredTick(\n            run_id=run_id,\n            sequence=seq,\n            timestamp=now,\n            tick_data=tick_data,\n        )\n\n        # Fire-and-forget: tick creates run in the background so they don't\n        # block the control loop.  Failures surface at _regroup_ticks time.\n        self._track_pending(\n            self._pending_ticks,\n            run_id,\n            self._ticks_collection,\n            stored.model_dump(mode=\"json\"),\n        )\n\n    async def get_ticks(self, run_id: str) -> list[StoredTick]:\n        return [t async for t in self.stream_ticks(run_id)]\n\n    async def stream_ticks(self, run_id: str) -> AsyncIterator[StoredTick]:\n        await self._regroup_ticks(run_id)\n        cursor: int | None = None\n        while True:\n            filters: dict[str, Any] = {\"run_id\": {\"eq\": run_id}}\n            if cursor is not None:\n                filters[\"sequence\"] = {\"gt\": cursor}\n            page = await self._client.search(\n                self._ticks_collection,\n                filters,\n                page_size=_TICK_PAGE_SIZE,\n                order_by=\"sequence\",\n            )\n            for item in page:\n                tick = StoredTick.model_validate(item[\"data\"])\n                yield tick\n                cursor = tick.sequence\n            if len(page) < _TICK_PAGE_SIZE:\n                return\n\n    # ------------------------------------------------------------------\n    # State store\n    # ------------------------------------------------------------------\n\n    def create_state_store(\n        self,\n        run_id: str,\n        state_type: type[Any] | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> StateStore[Any]:\n        cached = self._state_stores.get(run_id)\n        if cached is not None:\n            return cached\n        store = AgentDataStateStore(\n            client=self._client,\n            run_id=run_id,\n            state_type=state_type,\n            collection=f\"{self._collection}_state\",\n        )\n        self._state_stores[run_id] = store\n        return store\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/memory_workflow_store.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nimport weakref\nfrom collections import deque\nfrom collections.abc import AsyncIterator\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom workflows.context.serializers import BaseSerializer\nfrom workflows.context.state_store import DictState, InMemoryStateStore\n\nfrom .abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    PersistentHandler,\n    StoredEvent,\n    StoredTick,\n    is_terminal_status,\n)\n\nlogger = logging.getLogger(__name__)\n\n\ndef _matches_query(handler: PersistentHandler, query: HandlerQuery) -> bool:\n    # Empty lists should match nothing (short-circuit)\n    if query.handler_id_in is not None:\n        if len(query.handler_id_in) == 0:\n            return False\n        if handler.handler_id not in query.handler_id_in:\n            return False\n\n    if query.run_id_in is not None:\n        if len(query.run_id_in) == 0:\n            return False\n        if handler.run_id not in query.run_id_in:\n            return False\n\n    if query.workflow_name_in is not None:\n        if len(query.workflow_name_in) == 0:\n            return False\n        if handler.workflow_name not in query.workflow_name_in:\n            return False\n\n    if query.status_in is not None:\n        if len(query.status_in) == 0:\n            return False\n        if handler.status not in query.status_in:\n            return False\n\n    if query.is_idle is not None:\n        handler_is_idle = handler.idle_since is not None\n        if query.is_idle != handler_is_idle:\n            return False\n\n    return True\n\n\nclass MemoryWorkflowStore(AbstractWorkflowStore):\n    def __init__(self, max_completed: int | None = 1000) -> None:\n        if max_completed is not None and max_completed < 0:\n            raise ValueError(\"max_completed must be >= 0 or None\")\n\n        self.handlers: dict[str, PersistentHandler] = {}\n        self.events: dict[str, list[StoredEvent]] = {}\n        self.ticks: dict[str, list[StoredTick]] = {}\n        self.state_stores: dict[str, InMemoryStateStore[Any]] = {}\n        self._conditions: weakref.WeakValueDictionary[str, asyncio.Condition] = (\n            weakref.WeakValueDictionary()\n        )\n        self.max_completed = max_completed\n        self._terminal_queue: deque[str] = deque()\n\n    def create_state_store(\n        self,\n        run_id: str,\n        state_type: type[Any] | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> InMemoryStateStore[Any]:\n        if run_id not in self.state_stores:\n            if serialized_state is not None and serializer is not None:\n                try:\n                    self.state_stores[run_id] = InMemoryStateStore.from_dict(\n                        serialized_state, serializer\n                    )\n                except Exception:\n                    logger.warning(\"Failed to seed InMemoryStateStore\", exc_info=True)\n                    self.state_stores[run_id] = InMemoryStateStore(\n                        state_type() if state_type else DictState()\n                    )\n            else:\n                self.state_stores[run_id] = InMemoryStateStore(\n                    state_type() if state_type else DictState()\n                )\n        return self.state_stores[run_id]\n\n    async def query(self, query: HandlerQuery) -> list[PersistentHandler]:\n        return [\n            handler\n            for handler in self.handlers.values()\n            if _matches_query(handler, query)\n        ]\n\n    async def update(self, handler: PersistentHandler) -> None:\n        self.handlers[handler.handler_id] = handler\n        if is_terminal_status(handler.status):\n            self._terminal_queue.append(handler.handler_id)\n            self._evict_oldest_completed()\n\n    async def delete(self, query: HandlerQuery) -> int:\n        to_delete = [\n            handler_id\n            for handler_id, handler in list(self.handlers.items())\n            if _matches_query(handler, query)\n        ]\n        for handler_id in to_delete:\n            del self.handlers[handler_id]\n        return len(to_delete)\n\n    def _evict_oldest_completed(self) -> None:\n        \"\"\"Remove the oldest completed handlers when the cap is exceeded.\n\n        Uses _terminal_queue (insertion-ordered deque) for O(1) eviction\n        instead of scanning and sorting all handlers.\n        \"\"\"\n        if self.max_completed is None:\n            return\n\n        while len(self._terminal_queue) > self.max_completed:\n            handler_id = self._terminal_queue.popleft()\n            handler = self.handlers.get(handler_id)\n            if handler is None:\n                # Already removed (e.g. via delete()), skip.\n                continue\n            if not is_terminal_status(handler.status):\n                # Stale terminal-queue entry for a handler_id that was upserted\n                # into a newer non-terminal row.\n                continue\n\n            self.handlers.pop(handler_id, None)\n            run_id = handler.run_id\n            if run_id is not None:\n                self.events.pop(run_id, None)\n                self.ticks.pop(run_id, None)\n                self.state_stores.pop(run_id, None)\n\n    def _get_or_create_condition(self, run_id: str) -> asyncio.Condition:\n        cond = self._conditions.get(run_id)\n        if cond is None:\n            cond = asyncio.Condition()\n            self._conditions[run_id] = cond\n        return cond\n\n    async def append_event(self, run_id: str, event: EventEnvelopeWithMetadata) -> None:\n        if run_id not in self.events:\n            self.events[run_id] = []\n        existing = self.events[run_id]\n        next_seq = (existing[-1].sequence + 1) if existing else 0\n        stored = StoredEvent(\n            run_id=run_id,\n            sequence=next_seq,\n            timestamp=datetime.now(timezone.utc),\n            event=event,\n        )\n        existing.append(stored)\n        condition = self._conditions.get(run_id)\n        if condition is not None:\n            async with condition:\n                condition.notify_all()\n\n    async def query_events(\n        self,\n        run_id: str,\n        after_sequence: int | None = None,\n        limit: int | None = None,\n    ) -> list[StoredEvent]:\n        events = self.events.get(run_id, [])\n        if after_sequence is not None:\n            events = [e for e in events if e.sequence > after_sequence]\n        if limit is not None:\n            events = events[:limit]\n        return events\n\n    async def append_tick(self, run_id: str, tick_data: dict[str, Any]) -> None:\n        if run_id not in self.ticks:\n            self.ticks[run_id] = []\n        existing = self.ticks[run_id]\n        next_seq = (existing[-1].sequence + 1) if existing else 0\n        stored = StoredTick(\n            run_id=run_id,\n            sequence=next_seq,\n            timestamp=datetime.now(timezone.utc),\n            tick_data=tick_data,\n        )\n        existing.append(stored)\n\n    async def get_ticks(self, run_id: str) -> list[StoredTick]:\n        return list(self.ticks.get(run_id, []))\n\n    async def subscribe_events(\n        self, run_id: str, after_sequence: int = -1\n    ) -> AsyncIterator[StoredEvent]:\n        \"\"\"Condition-based subscription — no polling.\n\n        Uses list-index cursoring rather than sequence-field cursoring to\n        handle duplicate sequence numbers (which occur when multiple internal\n        adapters share the same run_id).\n        \"\"\"\n        # Determine starting index: skip events with sequence <= after_sequence\n        all_events = self.events.get(run_id, [])\n        if after_sequence >= 0:\n            cursor = 0\n            for i, e in enumerate(all_events):\n                if e.sequence <= after_sequence:\n                    cursor = i + 1\n        else:\n            cursor = 0\n\n        condition = self._get_or_create_condition(run_id)\n\n        while True:\n            async with condition:\n                all_events = self.events.get(run_id, [])\n                batch = all_events[cursor:]\n                if not batch:\n                    await condition.wait()\n                    continue\n\n            for event in batch:\n                yield event\n                cursor += 1\n                if self._is_terminal_event(event):\n                    return\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/migration_utils.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport re\nfrom importlib import import_module, resources\nfrom typing import Any\n\nVERSION_PATTERN = re.compile(r\"--\\s*migration:\\s*(\\d+)\")\n\n\ndef iter_migration_files(source_pkg: str) -> list[Any]:\n    \"\"\"Return packaged SQL migration files in lexicographic order.\"\"\"\n    pkg = import_module(source_pkg)\n    root = resources.files(pkg)\n    files = (p for p in root.iterdir() if p.name.endswith(\".sql\"))\n    return sorted(files, key=lambda p: p.name)  # type: ignore[reportReturnType]\n\n\ndef parse_target_version(sql_text: str) -> int | None:\n    \"\"\"Return target schema version declared in a ``-- migration: N`` comment.\"\"\"\n    first_line = sql_text.splitlines()[0] if sql_text else \"\"\n    match = VERSION_PATTERN.search(first_line)\n    return int(match.group(1)) if match else None\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/postgres/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/postgres/migrate.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport logging\nimport re\n\nimport asyncpg\nfrom llama_agents.server._store import POSTGRES_MIGRATION_SOURCE\nfrom llama_agents.server._store.migration_utils import (\n    iter_migration_files,\n    parse_target_version,\n)\n\nlogger = logging.getLogger(__name__)\n\n_MIGRATIONS_PKG = POSTGRES_MIGRATION_SOURCE[1]\n# Arbitrary but fixed int64 used as a pg_advisory_lock key so that\n# concurrent replicas serialize their migration runs.\n_LOCK_ID = 7_201_407_233_458_173\n\n_VALID_IDENTIFIER = re.compile(r\"^[A-Za-z_][A-Za-z0-9_]*$\")\n\n\ndef _quote_identifier(name: str) -> str:\n    \"\"\"Quote a SQL identifier, raising on invalid names.\"\"\"\n    if not _VALID_IDENTIFIER.match(name):\n        msg = f\"Invalid SQL identifier: {name!r}\"\n        raise ValueError(msg)\n    return f'\"{name}\"'\n\n\nasync def run_migrations(\n    conn: asyncpg.Connection,\n    schema: str | None = None,\n    sources: list[tuple[str, str]] | None = None,\n) -> None:\n    \"\"\"Apply pending migrations found under the migrations package(s).\n\n    Each migration file should start with a ``-- migration: N`` line.\n    Files are applied in lexicographic order and only when N > current_version\n    for the corresponding package.\n\n    *sources* is a list of ``(package_name, importable_pkg)`` pairs.  When\n    ``None`` it defaults to ``[(\"server\", _MIGRATIONS_PKG)]``.\n\n    A session-level advisory lock ensures that concurrent replicas serialize\n    their migration runs so DDL and version bookkeeping never race.\n    \"\"\"\n    if sources is None:\n        sources = [(\"server\", _MIGRATIONS_PKG)]\n\n    await conn.execute(\"SELECT pg_advisory_lock($1)\", _LOCK_ID)\n    try:\n        await _run_migrations_locked(conn, schema, sources)\n    finally:\n        await conn.execute(\"SELECT pg_advisory_unlock($1)\", _LOCK_ID)\n\n\nasync def _run_migrations_locked(\n    conn: asyncpg.Connection,\n    schema: str | None,\n    sources: list[tuple[str, str]],\n) -> None:\n    if schema:\n        quoted = _quote_identifier(schema)\n        await conn.execute(f\"CREATE SCHEMA IF NOT EXISTS {quoted}\")\n        await conn.execute(f\"SET search_path TO {quoted}\")\n\n    # Create the schema_migrations table with a composite PK on (package, version).\n    await conn.execute(\"\"\"\n        CREATE TABLE IF NOT EXISTS schema_migrations (\n            package TEXT NOT NULL DEFAULT 'server',\n            version INTEGER NOT NULL,\n            applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n            PRIMARY KEY (package, version)\n        )\n    \"\"\")\n\n    # Defensive upgrade: if table exists but lacks the package column, add it.\n    has_package = await conn.fetchval(\"\"\"\n        SELECT EXISTS (\n            SELECT 1 FROM information_schema.columns\n            WHERE table_name = 'schema_migrations' AND column_name = 'package'\n        )\n    \"\"\")\n    if not has_package:\n        await conn.execute(\n            \"ALTER TABLE schema_migrations ADD COLUMN package TEXT NOT NULL DEFAULT 'server'\"\n        )\n        await conn.execute(\n            \"ALTER TABLE schema_migrations DROP CONSTRAINT IF EXISTS schema_migrations_pkey\"\n        )\n        await conn.execute(\n            \"ALTER TABLE schema_migrations ADD PRIMARY KEY (package, version)\"\n        )\n\n    for package_name, source_pkg in sources:\n        row = await conn.fetchval(\n            \"SELECT MAX(version) FROM schema_migrations WHERE package = $1\",\n            package_name,\n        )\n        current_version = row if row is not None else 0\n\n        for path in iter_migration_files(source_pkg):\n            sql_text = path.read_text()\n            target_version = parse_target_version(sql_text) or 0\n            if target_version <= current_version:\n                continue\n\n            try:\n                logger.debug(\n                    \"Applying migration %s [%s] -> target version %s\",\n                    path.name,\n                    package_name,\n                    target_version,\n                )\n                async with conn.transaction():\n                    await conn.execute(sql_text)\n                    await conn.execute(\n                        \"INSERT INTO schema_migrations (package, version) VALUES ($1, $2)\",\n                        package_name,\n                        target_version,\n                    )\n            except Exception:\n                logger.error(\"Failed migration %s [%s]\", path.name, package_name)\n                raise\n\n            current_version = target_version\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/postgres/migrations/0001_init.sql",
    "content": "-- migration: 1\n\nCREATE TABLE IF NOT EXISTS wf_handlers (\n    handler_id VARCHAR(255) PRIMARY KEY,\n    workflow_name VARCHAR(255) NOT NULL,\n    status VARCHAR(50) NOT NULL,\n    run_id VARCHAR(255),\n    error TEXT,\n    result TEXT,\n    started_at TIMESTAMPTZ,\n    updated_at TIMESTAMPTZ,\n    completed_at TIMESTAMPTZ,\n    idle_since TIMESTAMPTZ\n);\n\nCREATE TABLE IF NOT EXISTS wf_events (\n    id SERIAL PRIMARY KEY,\n    run_id VARCHAR(255) NOT NULL,\n    sequence INTEGER NOT NULL,\n    timestamp TIMESTAMPTZ NOT NULL,\n    event_json TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_wf_events_run_id ON wf_events (run_id);\nCREATE INDEX IF NOT EXISTS idx_wf_handlers_run_id ON wf_handlers (run_id);\n\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM pg_constraint WHERE conname = 'uq_wf_events_run_id_sequence'\n    ) THEN\n        ALTER TABLE wf_events\n            ADD CONSTRAINT uq_wf_events_run_id_sequence UNIQUE (run_id, sequence);\n    END IF;\nEND\n$$;\n\nCREATE TABLE IF NOT EXISTS workflow_state (\n    run_id VARCHAR(255) PRIMARY KEY,\n    state_json TEXT NOT NULL,\n    state_type VARCHAR(255),\n    state_module VARCHAR(255),\n    created_at TIMESTAMPTZ,\n    updated_at TIMESTAMPTZ\n);\n\nCREATE TABLE IF NOT EXISTS wf_ticks (\n    id SERIAL PRIMARY KEY,\n    run_id VARCHAR(255) NOT NULL,\n    sequence INTEGER NOT NULL,\n    timestamp TIMESTAMPTZ NOT NULL,\n    tick_data JSONB NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_wf_ticks_run_id ON wf_ticks (run_id);\n\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM pg_constraint WHERE conname = 'uq_wf_ticks_run_id_sequence'\n    ) THEN\n        ALTER TABLE wf_ticks\n            ADD CONSTRAINT uq_wf_ticks_run_id_sequence UNIQUE (run_id, sequence);\n    END IF;\nEND\n$$;\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/postgres/migrations/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/postgres_state_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport json\nimport logging\nimport uuid\nfrom contextlib import asynccontextmanager\nfrom datetime import datetime, timezone\nfrom typing import Any, AsyncGenerator, Generic, Literal\n\nimport asyncpg\nfrom pydantic import BaseModel\nfrom typing_extensions import TypeVar\nfrom workflows.context.serializers import BaseSerializer, JsonSerializer\nfrom workflows.context.state_store import (\n    DictState,\n    create_cleared_state,\n    deserialize_dict_state_data,\n    deserialize_state_from_dict,\n    get_by_path,\n    merge_state,\n    parse_in_memory_state,\n    serialize_dict_state_data,\n    set_by_path,\n)\n\nlogger = logging.getLogger(__name__)\n\nMODEL_T = TypeVar(\"MODEL_T\", bound=BaseModel, default=DictState)  # type: ignore[reportGeneralTypeIssues]\n\n\nclass PostgresSerializedState(BaseModel):\n    \"\"\"Serialized state referencing a postgres database row.\"\"\"\n\n    store_type: Literal[\"postgres\"] = \"postgres\"\n    run_id: str\n\n\ndef _utc_now() -> datetime:\n    return datetime.now(timezone.utc)\n\n\nclass PostgresStateStore(Generic[MODEL_T]):\n    \"\"\"Asyncpg-backed StateStore implementation.\n\n    Every get() reads from the database, every set() writes through.\n    No in-memory cache — the database is the source of truth.\n    \"\"\"\n\n    state_type: type[MODEL_T]\n\n    def __init__(\n        self,\n        pool: asyncpg.Pool,\n        run_id: str,\n        state_type: type[MODEL_T] | None = None,\n        serializer: BaseSerializer | None = None,\n        schema: str | None = None,\n    ) -> None:\n        self._pool = pool\n        self._run_id = run_id\n        self.state_type = state_type or DictState  # type: ignore[assignment]  # ty: ignore[invalid-assignment]\n        self._serializer = serializer or JsonSerializer()\n        self._schema = schema\n        self._pending_seed: tuple[dict[str, Any], BaseSerializer] | None = None\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    @property\n    def _table_ref(self) -> str:\n        if self._schema:\n            return f\"{self._schema}.workflow_state\"\n        return \"workflow_state\"\n\n    @functools.cached_property\n    def _lock(self) -> asyncio.Lock:\n        \"\"\"Lazy lock initialization for Python 3.14+ compatibility.\"\"\"\n        return asyncio.Lock()\n\n    def _serialize_state(self, state: MODEL_T) -> str:\n        \"\"\"Serialize state model to JSON string.\"\"\"\n        if isinstance(state, DictState):\n            return json.dumps(serialize_dict_state_data(state, self._serializer))\n        return self._serializer.serialize(state)\n\n    def _deserialize_state(self, state_json: str) -> MODEL_T:\n        \"\"\"Deserialize state from JSON string.\"\"\"\n        if issubclass(self.state_type, DictState):\n            data = json.loads(state_json)\n            return deserialize_dict_state_data(data, self._serializer)  # type: ignore[return-value]  # ty: ignore[invalid-return-type]\n        return self._serializer.deserialize(state_json)\n\n    def _create_default_state(self) -> MODEL_T:\n        return self.state_type()\n\n    async def _write_in_memory_state(self, serialized_state: dict[str, Any]) -> None:\n        \"\"\"Migrate InMemory-format state into the database.\"\"\"\n        state = deserialize_state_from_dict(serialized_state, self._serializer)\n        await self._save_state(state)  # type: ignore[arg-type]\n\n    async def _flush_pending_seed(self) -> None:\n        \"\"\"Flush pending seed data to the database if present.\"\"\"\n        if self._pending_seed is None:\n            return\n        serialized_state, serializer = self._pending_seed\n        self._pending_seed = None\n        store_type = serialized_state.get(\"store_type\")\n        if store_type == \"postgres\":\n            source_run_id = serialized_state.get(\"run_id\")\n            if source_run_id and source_run_id != self._run_id:\n                await self._copy_state_from_run(source_run_id)\n        else:\n            await self._write_in_memory_state(serialized_state)\n\n    async def _copy_state_from_run(self, source_run_id: str) -> None:\n        \"\"\"Copy state from another run_id using SQL INSERT...SELECT.\"\"\"\n        async with self._pool.acquire() as conn:\n            now = _utc_now()\n            await conn.execute(\n                f\"\"\"\n                INSERT INTO {self._table_ref} (run_id, state_json, state_type, state_module, created_at, updated_at)\n                SELECT $1, state_json, state_type, state_module, $2, $3\n                FROM {self._table_ref} WHERE run_id = $4\n                ON CONFLICT(run_id) DO UPDATE SET\n                    state_json = EXCLUDED.state_json,\n                    state_type = EXCLUDED.state_type,\n                    state_module = EXCLUDED.state_module,\n                    updated_at = EXCLUDED.updated_at\n                \"\"\",\n                self._run_id,\n                now,\n                now,\n                source_run_id,\n            )\n\n    async def _load_state(\n        self,\n        conn: asyncpg.Connection | None = None,\n    ) -> MODEL_T:\n        \"\"\"Load state from database. Creates default if row doesn't exist.\"\"\"\n        await self._flush_pending_seed()\n        should_release = conn is None\n        if conn is None:\n            conn = await self._pool.acquire()  # type: ignore[assignment]\n        try:\n            row = await conn.fetchrow(  # type: ignore[union-attr]\n                f\"SELECT state_json FROM {self._table_ref} WHERE run_id = $1\",\n                self._run_id,\n            )\n            if row is None:\n                state = self._create_default_state()\n                await self._save_state(state, conn)\n                return state\n            return self._deserialize_state(row[\"state_json\"])\n        finally:\n            if should_release:\n                await self._pool.release(conn)  # type: ignore[arg-type]\n\n    async def _save_state(\n        self,\n        state: MODEL_T,\n        conn: asyncpg.Connection | asyncpg.pool.PoolConnectionProxy | None = None,\n    ) -> None:\n        \"\"\"Save state to database via upsert.\"\"\"\n        should_release = conn is None\n        if conn is None:\n            conn = await self._pool.acquire()  # type: ignore[assignment]\n        try:\n            now = _utc_now()\n            state_json = self._serialize_state(state)\n            await conn.execute(  # type: ignore[union-attr]\n                f\"\"\"\n                INSERT INTO {self._table_ref} (run_id, state_json, state_type, state_module, created_at, updated_at)\n                VALUES ($1, $2, $3, $4, $5, $6)\n                ON CONFLICT(run_id) DO UPDATE SET\n                    state_json = EXCLUDED.state_json,\n                    state_type = EXCLUDED.state_type,\n                    state_module = EXCLUDED.state_module,\n                    updated_at = EXCLUDED.updated_at\n                \"\"\",\n                self._run_id,\n                state_json,\n                type(state).__name__,\n                type(state).__module__,\n                now,\n                now,\n            )\n        finally:\n            if should_release:\n                await self._pool.release(conn)  # type: ignore[arg-type]\n\n    async def get_state(self) -> MODEL_T:\n        \"\"\"Return a copy of the current state model.\"\"\"\n        state = await self._load_state()\n        return state.model_copy()\n\n    async def set_state(self, state: MODEL_T) -> None:\n        \"\"\"Replace or merge into the current state model.\"\"\"\n        async with self._pool.acquire() as conn:\n            row = await conn.fetchrow(\n                f\"SELECT state_json FROM {self._table_ref} WHERE run_id = $1\",\n                self._run_id,\n            )\n            if row is None:\n                await self._save_state(state, conn)\n                return\n\n            current_state = self._deserialize_state(row[\"state_json\"])\n            merged = merge_state(current_state, state)\n            await self._save_state(merged, conn)  # type: ignore[arg-type]\n\n    async def get(self, path: str, default: Any = ...) -> Any:\n        \"\"\"Get a nested value using dot-separated paths.\"\"\"\n        state = await self._load_state()\n        return get_by_path(state, path, default)\n\n    async def set(self, path: str, value: Any) -> None:\n        \"\"\"Set a nested value using dot-separated paths.\"\"\"\n        async with self.edit_state() as state:\n            set_by_path(state, path, value)\n\n    async def clear(self) -> None:\n        \"\"\"Reset the state to its type defaults.\"\"\"\n        await self.set_state(create_cleared_state(self.state_type))\n\n    @asynccontextmanager\n    async def edit_state(self) -> AsyncGenerator[MODEL_T, None]:\n        \"\"\"Edit state transactionally under a lock.\"\"\"\n        async with self._lock:\n            state = await self._load_state()\n            yield state\n            await self._save_state(state)\n\n    def to_dict(self, serializer: BaseSerializer) -> dict[str, Any]:\n        \"\"\"Serialize state store metadata for persistence.\n\n        Returns metadata only — actual state lives in the database.\n        \"\"\"\n        payload = PostgresSerializedState(run_id=self._run_id)\n        return payload.model_dump()\n\n    @classmethod\n    def from_dict(\n        cls,\n        serialized_state: dict[str, Any],\n        serializer: BaseSerializer,\n        pool: asyncpg.Pool | None = None,\n        state_type: type[BaseModel] | None = None,\n        run_id: str | None = None,\n        schema: str | None = None,\n    ) -> PostgresStateStore[Any]:\n        \"\"\"Restore a state store from serialized payload.\n\n        Handles both InMemorySerializedState (migrates data to DB on first use)\n        and PostgresSerializedState (reconnects to existing row).\n        \"\"\"\n        if not serialized_state:\n            raise ValueError(\"Cannot restore PostgresStateStore from empty dict\")\n        if pool is None:\n            raise ValueError(\"pool is required for PostgresStateStore.from_dict()\")\n\n        store_type = serialized_state.get(\"store_type\")\n\n        if store_type == \"postgres\":\n            parsed = PostgresSerializedState.model_validate(serialized_state)\n            effective_run_id = run_id or parsed.run_id\n            return cls(\n                pool=pool,\n                run_id=effective_run_id,\n                state_type=state_type,  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]\n                serializer=serializer,\n                schema=schema,\n            )\n\n        # InMemory format — will need async migration\n        parse_in_memory_state(serialized_state)\n\n        effective_run_id = run_id or str(uuid.uuid4())\n        store = cls(\n            pool=pool,\n            run_id=effective_run_id,\n            state_type=state_type,  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]\n            serializer=serializer,\n            schema=schema,\n        )\n        # Note: caller must await store._write_in_memory_state(serialized_state)\n        # since from_dict is synchronous but migration requires async DB access\n        return store\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/postgres_workflow_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nimport concurrent.futures\nimport contextlib\nimport json\nimport logging\nimport weakref\nfrom collections.abc import AsyncIterator\nfrom datetime import datetime, timezone\nfrom typing import Any, Sequence, cast\n\nimport asyncpg\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom workflows.context import JsonSerializer\nfrom workflows.context.serializers import BaseSerializer\n\nfrom .._pool import PoolProvider\nfrom .abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    PersistentHandler,\n    StoredEvent,\n    StoredTick,\n)\nfrom .postgres.migrate import run_migrations as _run_migrations\nfrom .postgres_state_store import PostgresStateStore\n\nlogger = logging.getLogger(__name__)\n\n_TICK_PAGE_SIZE = 100\n\n# Bounds for the LISTEN-connection reconnect backoff.\n_LISTEN_RECONNECT_INITIAL_DELAY = 0.5\n_LISTEN_RECONNECT_MAX_DELAY = 30.0\n\n\ndef _utc_now() -> datetime:\n    return datetime.now(timezone.utc)\n\n\nclass PostgresWorkflowStore(AbstractWorkflowStore):\n    \"\"\"Async Postgres workflow store using asyncpg with LISTEN/NOTIFY.\"\"\"\n\n    def __init__(\n        self,\n        dsn: str,\n        schema: str | None = None,\n        poll_interval: float = 1.0,\n        handlers_table_name: str = \"wf_handlers\",\n        events_table_name: str = \"wf_events\",\n        pool_min_size: int = 2,\n        pool_max_size: int = 10,\n        auto_migrate: bool = True,\n        pool: PoolProvider | None = None,\n    ) -> None:\n        \"\"\"Construct a PostgresWorkflowStore.\n\n        When ``pool`` is provided, the provider controls ownership semantics.\n        When it is omitted, the store owns a lazily-created asyncpg pool using\n        the provided DSN and pool size settings.\n        \"\"\"\n        self._dsn = dsn\n        self._schema = schema\n        self.poll_interval = poll_interval\n        self._handlers_table_name = handlers_table_name\n        self._events_table_name = events_table_name\n        self._pool_min_size = pool_min_size\n        self._pool_max_size = pool_max_size\n        self._auto_migrate = auto_migrate\n        self._pool_provider = pool or PoolProvider.create(\n            dsn,\n            min_size=pool_min_size,\n            max_size=pool_max_size,\n        )\n        self._pool: asyncpg.Pool | None = None\n        self._listen_conn: asyncpg.Connection | None = None\n        self._conditions: weakref.WeakValueDictionary[str, asyncio.Condition] = (\n            weakref.WeakValueDictionary()\n        )\n        # LISTEN reconnect bookkeeping.\n        self._closing = False\n        self._reconnect_lock = asyncio.Lock()\n        self._reconnect_task: asyncio.Task[None] | None = None\n\n    @property\n    def _handlers_ref(self) -> str:\n        if self._schema:\n            return f\"{self._schema}.{self._handlers_table_name}\"\n        return self._handlers_table_name\n\n    @property\n    def _events_ref(self) -> str:\n        if self._schema:\n            return f\"{self._schema}.{self._events_table_name}\"\n        return self._events_table_name\n\n    @property\n    def _ticks_ref(self) -> str:\n        if self._schema:\n            return f\"{self._schema}.wf_ticks\"\n        return \"wf_ticks\"\n\n    @property\n    def _notify_channel(self) -> str:\n        return self._events_table_name\n\n    async def start(self) -> None:\n        \"\"\"Resolve the connection pool, run migrations if enabled, and set up LISTEN.\"\"\"\n        if self._pool is not None:\n            return\n        # Reset for re-start after a close().\n        self._closing = False\n        self._pool = await self._pool_provider.get()\n        if self._auto_migrate:\n            await self.run_migrations()\n        await self._setup_listener()\n\n    async def _setup_listener(self) -> None:\n        \"\"\"Set up a dedicated connection for LISTEN/NOTIFY.\"\"\"\n        assert self._pool is not None\n        conn = cast(asyncpg.Connection, await self._pool.acquire())\n        try:\n            await conn.add_listener(self._notify_channel, self._on_notify)\n            # Recover from network blips / Postgres restarts.\n            try:\n                conn.add_termination_listener(self._on_listen_termination)\n            except AttributeError:\n                # asyncpg < 0.27 (or a stub during tests) may not expose this.\n                logger.debug(\n                    \"Connection.add_termination_listener unavailable; LISTEN reconnect disabled\"\n                )\n        except Exception:\n            await self._pool.release(conn)\n            raise\n        self._listen_conn = conn\n\n    def _on_notify(\n        self,\n        connection: asyncpg.Connection,\n        pid: int,\n        channel: str,\n        payload: str,\n    ) -> None:\n        \"\"\"Handle NOTIFY callback — schedule condition notification.\"\"\"\n        run_id = payload\n        condition = self._conditions.get(run_id)\n        if condition is not None:\n            asyncio.ensure_future(self._notify_condition(condition))\n\n    @staticmethod\n    async def _notify_condition(condition: asyncio.Condition) -> None:\n        async with condition:\n            condition.notify_all()\n\n    def _wake_all_subscribers(self) -> None:\n        \"\"\"Wake every active condition so subscribe_events re-queries.\n\n        Used after a LISTEN reconnect — we may have missed NOTIFYs while the\n        listener connection was down. The polling fallback in\n        ``subscribe_events`` would catch them within ``poll_interval``; this\n        just makes recovery immediate.\n        \"\"\"\n        # Snapshot to avoid mutation-during-iteration on the WeakValueDictionary.\n        for cond in list(self._conditions.values()):\n            asyncio.ensure_future(self._notify_condition(cond))\n\n    def _on_listen_termination(self, connection: asyncpg.Connection) -> None:\n        \"\"\"asyncpg termination callback — schedule a reconnect.\n\n        Fires for normal close too, so we guard with ``self._closing``. The\n        callback runs on asyncpg's internal task; do real work via\n        ``ensure_future``.\n        \"\"\"\n        if self._closing:\n            return\n        if self._reconnect_task is not None and not self._reconnect_task.done():\n            return  # already reconnecting\n        try:\n            self._reconnect_task = asyncio.ensure_future(self._reconnect_listener())\n        except RuntimeError:\n            # No running loop (e.g. termination during shutdown). Nothing to do.\n            logger.debug(\"No running loop to schedule LISTEN reconnect\")\n\n    async def _reconnect_listener(self) -> None:\n        \"\"\"Reconnect the LISTEN connection with bounded exponential backoff.\n\n        On success, wakes every subscribe_events consumer so they re-query\n        immediately rather than waiting for the polling fallback.\n        \"\"\"\n        async with self._reconnect_lock:\n            if self._closing or self._pool is None:\n                return\n\n            logger.warning(\"LISTEN connection dropped; reconnecting\")\n\n            # Release the dead conn back to the pool. asyncpg handles already-\n            # closed connections gracefully on release.\n            if self._listen_conn is not None:\n                try:\n                    await self._pool.release(self._listen_conn)\n                except Exception:\n                    logger.debug(\n                        \"Failed to release dead listen connection\", exc_info=True\n                    )\n                self._listen_conn = None\n\n            delay = _LISTEN_RECONNECT_INITIAL_DELAY\n            while not self._closing:\n                try:\n                    await self._setup_listener()\n                except Exception:\n                    logger.warning(\n                        \"LISTEN reconnect attempt failed; retrying in %.1fs\",\n                        delay,\n                        exc_info=True,\n                    )\n                    try:\n                        await asyncio.sleep(delay)\n                    except asyncio.CancelledError:\n                        return\n                    delay = min(delay * 2, _LISTEN_RECONNECT_MAX_DELAY)\n                    continue\n\n                logger.info(\"LISTEN connection re-established\")\n                self._wake_all_subscribers()\n                return\n\n    async def close(self) -> None:\n        \"\"\"Tear down the LISTEN connection and close the pool (if owned).\"\"\"\n        # Block any in-flight reconnect coroutine from re-installing the listener.\n        self._closing = True\n        if self._reconnect_task is not None and not self._reconnect_task.done():\n            self._reconnect_task.cancel()\n            try:\n                await self._reconnect_task\n            except (asyncio.CancelledError, Exception):\n                pass\n        self._reconnect_task = None\n        if self._listen_conn is not None:\n            try:\n                await self._listen_conn.remove_listener(\n                    self._notify_channel, self._on_notify\n                )\n            except Exception:\n                logger.debug(\"Failed to remove listener during close\", exc_info=True)\n            try:\n                await self._pool.release(self._listen_conn)  # type: ignore[union-attr]  # ty: ignore[unresolved-attribute]\n            except Exception:\n                logger.debug(\n                    \"Failed to release listen connection during close\", exc_info=True\n                )\n            self._listen_conn = None\n        if self._pool is not None:\n            await self._pool_provider.close()\n            self._pool = None\n\n    async def _ensure_pool(self) -> asyncpg.Pool:\n        if self._pool is None:\n            await self.start()\n        assert self._pool is not None\n        return self._pool\n\n    def _get_or_create_condition(self, run_id: str) -> asyncio.Condition:\n        cond = self._conditions.get(run_id)\n        if cond is None:\n            cond = asyncio.Condition()\n            self._conditions[run_id] = cond\n        return cond\n\n    def create_state_store(\n        self,\n        run_id: str,\n        state_type: type[Any] | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> PostgresStateStore[Any]:\n        if self._pool is None:\n            raise RuntimeError(\n                \"PostgresWorkflowStore pool not initialized. Call start() first.\"\n            )\n        store = PostgresStateStore(\n            pool=self._pool,\n            run_id=run_id,\n            state_type=state_type,\n            schema=self._schema,\n        )\n        if serialized_state is not None and serializer is not None:\n            store._pending_seed = (serialized_state, serializer)\n        return store\n\n    # ── Migrations ──────────────────────────────────────────────────────\n\n    async def run_migrations(self) -> None:\n        \"\"\"Apply file-based migrations to create/update schema.\"\"\"\n        pool = await self._ensure_pool()\n        async with pool.acquire() as conn:\n            await _run_migrations(cast(asyncpg.Connection, conn), schema=self._schema)\n\n    @staticmethod\n    def run_migrations_sync(dsn: str, schema: str | None = None) -> None:\n        \"\"\"Run migrations synchronously, handling event loop detection.\n\n        Safe to call from both sync and async contexts. When called from\n        within a running event loop, runs migrations in a background thread.\n        \"\"\"\n\n        async def _migrate() -> None:\n            store = PostgresWorkflowStore(dsn=dsn, schema=schema)\n            await store.start()\n            try:\n                await store.run_migrations()\n            finally:\n                await store.close()\n\n        try:\n            loop = asyncio.get_running_loop()\n        except RuntimeError:\n            loop = None\n\n        if loop is not None and loop.is_running():\n            with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:\n                executor.submit(lambda: asyncio.run(_migrate())).result()\n        else:\n            asyncio.run(_migrate())\n\n    # ── Handlers ────────────────────────────────────────────────────────\n\n    async def query(self, query: HandlerQuery) -> list[PersistentHandler]:\n        filter_spec = self._build_filters(query)\n        if filter_spec is None:\n            return []\n\n        clauses, params = filter_spec\n        sql = f\"\"\"\n            SELECT handler_id, workflow_name, status, run_id, error, result,\n                   started_at, updated_at, completed_at, idle_since\n            FROM {self._handlers_ref}\n        \"\"\"\n        if clauses:\n            sql = f\"{sql} WHERE {' AND '.join(clauses)}\"\n\n        pool = await self._ensure_pool()\n        async with pool.acquire() as conn:\n            rows = await conn.fetch(sql, *params)\n\n        return [self._row_to_handler(row) for row in rows]\n\n    async def update(self, handler: PersistentHandler) -> None:\n        result_json = None\n        if handler.result is not None:\n            result_json = JsonSerializer().serialize(handler.result)\n\n        pool = await self._ensure_pool()\n        async with pool.acquire() as conn:\n            await conn.execute(\n                f\"\"\"\n                INSERT INTO {self._handlers_ref}\n                    (handler_id, workflow_name, status, run_id, error, result,\n                     started_at, updated_at, completed_at, idle_since)\n                VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n                ON CONFLICT (handler_id) DO UPDATE SET\n                    workflow_name = EXCLUDED.workflow_name,\n                    status = EXCLUDED.status,\n                    run_id = EXCLUDED.run_id,\n                    error = EXCLUDED.error,\n                    result = EXCLUDED.result,\n                    started_at = EXCLUDED.started_at,\n                    updated_at = EXCLUDED.updated_at,\n                    completed_at = EXCLUDED.completed_at,\n                    idle_since = EXCLUDED.idle_since\n                \"\"\",\n                handler.handler_id,\n                handler.workflow_name,\n                handler.status,\n                handler.run_id,\n                handler.error,\n                result_json,\n                handler.started_at,\n                handler.updated_at,\n                handler.completed_at,\n                handler.idle_since,\n            )\n\n    async def delete(self, query: HandlerQuery) -> int:\n        filter_spec = self._build_filters(query)\n        if filter_spec is None:\n            return 0\n\n        clauses, params = filter_spec\n        if not clauses:\n            return 0\n\n        sql = f\"DELETE FROM {self._handlers_ref} WHERE {' AND '.join(clauses)}\"\n        pool = await self._ensure_pool()\n        async with pool.acquire() as conn:\n            result = await conn.execute(sql, *params)\n            # asyncpg returns \"DELETE N\"\n            return int(result.split()[-1])\n\n    # ── Events ──────────────────────────────────────────────────────────\n\n    _MAX_SEQUENCE_RETRIES = 5\n\n    async def append_event(self, run_id: str, event: EventEnvelopeWithMetadata) -> None:\n        now = _utc_now()\n        event_json = event.model_dump_json()\n\n        pool = await self._ensure_pool()\n        insert_sql = f\"\"\"\n            INSERT INTO {self._events_ref} (run_id, sequence, timestamp, event_json)\n            VALUES (\n                $1,\n                COALESCE((SELECT MAX(sequence) FROM {self._events_ref} WHERE run_id = $1::varchar), -1) + 1,\n                $2,\n                $3\n            )\n        \"\"\"\n        # Retry on unique constraint violation from concurrent sequence assignment\n        for attempt in range(self._MAX_SEQUENCE_RETRIES):\n            try:\n                async with pool.acquire() as conn:\n                    await conn.execute(insert_sql, run_id, now, event_json)\n                    await conn.execute(\n                        \"SELECT pg_notify($1, $2)\",\n                        self._notify_channel,\n                        run_id,\n                    )\n                    return\n            except asyncpg.UniqueViolationError:\n                if attempt == self._MAX_SEQUENCE_RETRIES - 1:\n                    raise\n                logger.debug(\n                    \"Sequence conflict for run_id=%s, retrying (attempt %d)\",\n                    run_id,\n                    attempt + 1,\n                )\n\n    async def query_events(\n        self,\n        run_id: str,\n        after_sequence: int | None = None,\n        limit: int | None = None,\n    ) -> list[StoredEvent]:\n        sql = f\"\"\"\n            SELECT run_id, sequence, timestamp, event_json\n            FROM {self._events_ref}\n            WHERE run_id = $1\n        \"\"\"\n        params: list[Any] = [run_id]\n        param_idx = 2\n\n        if after_sequence is not None:\n            sql += f\" AND sequence > ${param_idx}\"\n            params.append(after_sequence)\n            param_idx += 1\n\n        sql += \" ORDER BY sequence\"\n\n        if limit is not None:\n            sql += f\" LIMIT ${param_idx}\"\n            params.append(limit)\n\n        pool = await self._ensure_pool()\n        async with pool.acquire() as conn:\n            rows = await conn.fetch(sql, *params)\n\n        return [\n            StoredEvent(\n                run_id=row[\"run_id\"],\n                sequence=row[\"sequence\"],\n                timestamp=row[\"timestamp\"],\n                event=EventEnvelopeWithMetadata.model_validate_json(row[\"event_json\"]),\n            )\n            for row in rows\n        ]\n\n    async def subscribe_events(\n        self, run_id: str, after_sequence: int = -1\n    ) -> AsyncIterator[StoredEvent]:\n        condition = self._get_or_create_condition(run_id)\n        cursor = after_sequence\n\n        while True:\n            async with condition:\n                batch = await self.query_events(run_id, after_sequence=cursor)\n                if not batch:\n                    with contextlib.suppress(TimeoutError):\n                        await asyncio.wait_for(\n                            condition.wait(), timeout=self.poll_interval\n                        )\n                    continue\n\n            for event in batch:\n                yield event\n                cursor = event.sequence\n                if self._is_terminal_event(event):\n                    return\n\n    # ── Ticks ──────────────────────────────────────────────────────────\n\n    _MAX_TICK_SEQUENCE_RETRIES = 5\n\n    async def append_tick(self, run_id: str, tick_data: dict[str, Any]) -> None:\n        now = _utc_now()\n        tick_json = json.dumps(tick_data)\n\n        pool = await self._ensure_pool()\n        insert_sql = f\"\"\"\n            INSERT INTO {self._ticks_ref} (run_id, sequence, timestamp, tick_data)\n            VALUES (\n                $1,\n                COALESCE((SELECT MAX(sequence) FROM {self._ticks_ref} WHERE run_id = $1::varchar), -1) + 1,\n                $2,\n                $3::jsonb\n            )\n        \"\"\"\n        for attempt in range(self._MAX_TICK_SEQUENCE_RETRIES):\n            try:\n                async with pool.acquire() as conn:\n                    await conn.execute(insert_sql, run_id, now, tick_json)\n                    return\n            except asyncpg.UniqueViolationError:\n                if attempt == self._MAX_TICK_SEQUENCE_RETRIES - 1:\n                    raise\n                logger.debug(\n                    \"Tick sequence conflict for run_id=%s, retrying (attempt %d)\",\n                    run_id,\n                    attempt + 1,\n                )\n\n    async def get_ticks(self, run_id: str) -> list[StoredTick]:\n        pool = await self._ensure_pool()\n        async with pool.acquire() as conn:\n            rows = await conn.fetch(\n                f\"\"\"\n                SELECT run_id, sequence, timestamp, tick_data\n                FROM {self._ticks_ref}\n                WHERE run_id = $1\n                ORDER BY sequence\n                \"\"\",\n                run_id,\n            )\n\n        return [\n            StoredTick(\n                run_id=row[\"run_id\"],\n                sequence=row[\"sequence\"],\n                timestamp=row[\"timestamp\"],\n                tick_data=json.loads(row[\"tick_data\"])\n                if isinstance(row[\"tick_data\"], str)\n                else row[\"tick_data\"],\n            )\n            for row in rows\n        ]\n\n    async def stream_ticks(self, run_id: str) -> AsyncIterator[StoredTick]:\n        pool = await self._ensure_pool()\n        cursor: int | None = None\n        while True:\n            if cursor is None:\n                sql = (\n                    f\"SELECT run_id, sequence, timestamp, tick_data \"\n                    f\"FROM {self._ticks_ref} WHERE run_id = $1 \"\n                    f\"ORDER BY sequence LIMIT $2\"\n                )\n                params: list[Any] = [run_id, _TICK_PAGE_SIZE]\n            else:\n                sql = (\n                    f\"SELECT run_id, sequence, timestamp, tick_data \"\n                    f\"FROM {self._ticks_ref} WHERE run_id = $1 AND sequence > $2 \"\n                    f\"ORDER BY sequence LIMIT $3\"\n                )\n                params = [run_id, cursor, _TICK_PAGE_SIZE]\n            async with pool.acquire() as conn:\n                rows = await conn.fetch(sql, *params)\n            for row in rows:\n                tick = StoredTick(\n                    run_id=row[\"run_id\"],\n                    sequence=row[\"sequence\"],\n                    timestamp=row[\"timestamp\"],\n                    tick_data=json.loads(row[\"tick_data\"])\n                    if isinstance(row[\"tick_data\"], str)\n                    else row[\"tick_data\"],\n                )\n                yield tick\n                cursor = tick.sequence\n            if len(rows) < _TICK_PAGE_SIZE:\n                return\n\n    # ── Helpers ─────────────────────────────────────────────────────────\n\n    def _build_filters(self, query: HandlerQuery) -> tuple[list[str], list[Any]] | None:\n        clauses: list[str] = []\n        params: list[Any] = []\n        param_idx = 1\n\n        def add_in_clause(column: str, values: Sequence[str]) -> None:\n            nonlocal param_idx\n            placeholders = \", \".join([f\"${param_idx + i}\" for i in range(len(values))])\n            clauses.append(f\"{column} IN ({placeholders})\")\n            params.extend(values)\n            param_idx += len(values)\n\n        for field, column in [\n            (query.workflow_name_in, \"workflow_name\"),\n            (query.handler_id_in, \"handler_id\"),\n            (query.run_id_in, \"run_id\"),\n            (query.status_in, \"status\"),\n        ]:\n            if field is not None:\n                if len(field) == 0:\n                    return None\n                add_in_clause(column, field)\n\n        if query.is_idle is not None:\n            if query.is_idle:\n                clauses.append(\"idle_since IS NOT NULL\")\n            else:\n                clauses.append(\"idle_since IS NULL\")\n\n        return clauses, params\n\n    @staticmethod\n    def _row_to_handler(row: asyncpg.Record) -> PersistentHandler:\n        return PersistentHandler(\n            handler_id=row[\"handler_id\"],\n            workflow_name=row[\"workflow_name\"],\n            status=row[\"status\"],\n            run_id=row[\"run_id\"],\n            error=row[\"error\"],\n            result=json.loads(row[\"result\"]) if row[\"result\"] else None,\n            started_at=row[\"started_at\"],\n            updated_at=row[\"updated_at\"],\n            completed_at=row[\"completed_at\"],\n            idle_since=row[\"idle_since\"],\n        )\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/migrate.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport logging\nimport sqlite3\n\nfrom llama_agents.server._store import SQLITE_MIGRATION_SOURCE\nfrom llama_agents.server._store.migration_utils import (\n    iter_migration_files,\n    parse_target_version,\n)\n\nlogger = logging.getLogger(__name__)\n\n_MIGRATIONS_PKG = SQLITE_MIGRATION_SOURCE[1]\n\n_SCHEMA_MIGRATIONS_DDL = \"\"\"\\\nCREATE TABLE IF NOT EXISTS schema_migrations (\n    package TEXT NOT NULL,\n    version INTEGER NOT NULL,\n    applied_at TEXT NOT NULL DEFAULT (datetime('now')),\n    PRIMARY KEY (package, version)\n)\n\"\"\"\n\n\ndef _bootstrap_schema_migrations(conn: sqlite3.Connection) -> None:\n    \"\"\"Create the schema_migrations table, seeding from PRAGMA user_version if needed.\"\"\"\n    cur = conn.cursor()\n\n    # Check if schema_migrations already exists\n    row = cur.execute(\n        \"SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'\"\n    ).fetchone()\n    if row:\n        return\n\n    # Check legacy user_version\n    uv_row = cur.execute(\"PRAGMA user_version\").fetchone()\n    legacy_version = int(uv_row[0]) if uv_row else 0\n\n    cur.executescript(_SCHEMA_MIGRATIONS_DDL)\n\n    if legacy_version > 0:\n        # Seed rows for existing server migrations\n        for v in range(1, legacy_version + 1):\n            cur.execute(\n                \"INSERT OR IGNORE INTO schema_migrations (package, version) VALUES (?, ?)\",\n                (\"server\", v),\n            )\n        conn.commit()\n        logger.debug(\n            \"Bootstrapped schema_migrations from PRAGMA user_version=%d\", legacy_version\n        )\n\n\ndef run_migrations(\n    conn: sqlite3.Connection,\n    sources: list[tuple[str, str]] | None = None,\n) -> None:\n    \"\"\"Apply pending migrations for one or more packages.\n\n    Parameters\n    ----------\n    conn:\n        An open SQLite connection.\n    sources:\n        List of ``(package_name, importable_migrations_pkg)`` pairs.\n        Defaults to ``[(\"server\", _MIGRATIONS_PKG)]``.\n    \"\"\"\n    # Enable WAL mode for concurrent read/write access.\n    # Switching journal mode requires a write lock.  On container/network\n    # filesystems (or after a previous crash) the DB may be transiently\n    # locked, so retry a few times before falling back to the default\n    # (DELETE) journal mode.\n    for attempt in range(3):\n        try:\n            conn.execute(\"PRAGMA journal_mode=WAL\")\n            break\n        except sqlite3.OperationalError:\n            if attempt < 2:\n                import time\n\n                time.sleep(0.5 * (attempt + 1))\n            else:\n                logger.warning(\n                    \"Could not enable WAL journal mode; \"\n                    \"falling back to default journal mode.\"\n                )\n\n    if sources is None:\n        sources = [(\"server\", _MIGRATIONS_PKG)]\n\n    _bootstrap_schema_migrations(conn)\n\n    cur = conn.cursor()\n\n    for package_name, source_pkg in sources:\n        # Determine already-applied versions for this package\n        rows = cur.execute(\n            \"SELECT version FROM schema_migrations WHERE package = ?\",\n            (package_name,),\n        ).fetchall()\n        applied: set[int] = {int(r[0]) for r in rows}\n\n        for path in iter_migration_files(source_pkg):\n            sql_text = path.read_text()\n            target_version = parse_target_version(sql_text) or 0\n            if target_version in applied or target_version == 0:\n                continue\n\n            try:\n                logger.debug(\n                    \"Applying migration %s:%s → version %s\",\n                    package_name,\n                    path.name,\n                    target_version,\n                )\n                cur.executescript(\"BEGIN;\\n\" + sql_text)\n            except Exception as exc:  # noqa: BLE001 – we surface the exact error\n                logger.error(\"Failed migration %s:%s: %s\", package_name, path.name, exc)\n                cur.execute(\"ROLLBACK\")\n                raise\n            else:\n                cur.execute(\n                    \"INSERT INTO schema_migrations (package, version) VALUES (?, ?)\",\n                    (package_name, target_version),\n                )\n                cur.execute(\"COMMIT\")\n                applied.add(target_version)\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/migrations/0001_init.sql",
    "content": "-- migration: 1\n\n-- Initial table creation matching the original minimal schema\nCREATE TABLE IF NOT EXISTS handlers (\n    handler_id TEXT PRIMARY KEY,\n    workflow_name TEXT,\n    status TEXT,\n    ctx TEXT\n);\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/migrations/0002_extend_handlers.sql",
    "content": "-- migration: 2\n\n-- Add new columns for extended handler persistence\nALTER TABLE handlers ADD COLUMN run_id TEXT;\nALTER TABLE handlers ADD COLUMN error TEXT;\nALTER TABLE handlers ADD COLUMN result TEXT;\nALTER TABLE handlers ADD COLUMN started_at TEXT;\nALTER TABLE handlers ADD COLUMN updated_at TEXT;\nALTER TABLE handlers ADD COLUMN completed_at TEXT;\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/migrations/0003_add_idle_since.sql",
    "content": "-- migration: 3\n\n-- Add idle_since column for tracking when a workflow became idle\nALTER TABLE handlers ADD COLUMN idle_since TEXT;\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/migrations/0004_add_ticks.sql",
    "content": "-- migration: 4\n\nCREATE TABLE IF NOT EXISTS ticks (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    run_id TEXT NOT NULL,\n    sequence INTEGER NOT NULL,\n    timestamp TEXT NOT NULL,\n    tick_data TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_ticks_run_id ON ticks (run_id);\nCREATE INDEX IF NOT EXISTS idx_ticks_run_id_sequence ON ticks (run_id, sequence);\n\nCREATE TABLE IF NOT EXISTS workflow_state (\n    run_id TEXT PRIMARY KEY,\n    state_json TEXT NOT NULL DEFAULT '{}',\n    state_type TEXT NOT NULL DEFAULT 'DictState',\n    state_module TEXT NOT NULL DEFAULT 'workflows.context.state_store',\n    created_at TEXT NOT NULL,\n    updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS events (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    run_id TEXT NOT NULL,\n    sequence INTEGER NOT NULL,\n    timestamp TEXT NOT NULL,\n    event_json TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_run_id_sequence ON events (run_id, sequence);\n\nCREATE INDEX IF NOT EXISTS idx_handlers_run_id ON handlers (run_id);\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/sqlite_state_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport json\nimport logging\nimport sqlite3\nimport uuid\nfrom contextlib import asynccontextmanager\nfrom datetime import datetime, timezone\nfrom typing import Any, AsyncGenerator, Generic, Literal\n\nfrom pydantic import BaseModel\nfrom typing_extensions import TypeVar\nfrom workflows.context.serializers import BaseSerializer, JsonSerializer\nfrom workflows.context.state_store import (\n    DictState,\n    create_cleared_state,\n    deserialize_dict_state_data,\n    deserialize_state_from_dict,\n    get_by_path,\n    merge_state,\n    parse_in_memory_state,\n    serialize_dict_state_data,\n    set_by_path,\n)\n\nlogger = logging.getLogger(__name__)\n\nMODEL_T = TypeVar(\"MODEL_T\", bound=BaseModel, default=DictState)  # type: ignore[reportGeneralTypeIssues]\n\n\nclass SqliteSerializedState(BaseModel):\n    \"\"\"Serialized state referencing a sqlite database row.\"\"\"\n\n    store_type: Literal[\"sqlite\"] = \"sqlite\"\n    run_id: str\n\n\ndef _utc_now() -> datetime:\n    return datetime.now(timezone.utc)\n\n\nclass SqliteStateStore(Generic[MODEL_T]):\n    \"\"\"Sqlite-backed StateStore implementation.\n\n    Every get() reads from the database, every set() writes through.\n    No in-memory cache — the database is the source of truth.\n    \"\"\"\n\n    state_type: type[MODEL_T]\n\n    def __init__(\n        self,\n        db_path: str,\n        run_id: str,\n        state_type: type[MODEL_T] | None = None,\n        serializer: BaseSerializer | None = None,\n        connection: sqlite3.Connection | None = None,\n    ) -> None:\n        self._db_path = db_path\n        self._run_id = run_id\n        self.state_type = state_type or DictState  # type: ignore[assignment]  # ty: ignore[invalid-assignment]\n        self._serializer = serializer or JsonSerializer()\n        self._shared_conn = connection\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    @functools.cached_property\n    def _lock(self) -> asyncio.Lock:\n        \"\"\"Lazy lock initialization for Python 3.14+ compatibility.\"\"\"\n        return asyncio.Lock()\n\n    def _connect(self) -> sqlite3.Connection:\n        if self._shared_conn is not None:\n            return self._shared_conn\n        return sqlite3.connect(self._db_path, timeout=30.0)\n\n    def _write_in_memory_state(self, serialized_state: dict[str, Any]) -> None:\n        \"\"\"Migrate InMemory-format state into the database.\"\"\"\n        state = deserialize_state_from_dict(serialized_state, self._serializer)\n        self._save_state(state)  # type: ignore[arg-type]\n\n    def _seed_from_serialized(\n        self, serialized_state: dict[str, Any], serializer: BaseSerializer\n    ) -> None:\n        \"\"\"Seed this store from serialized state data.\n\n        Handles both sqlite references (SQL-level copy) and InMemory format.\n        \"\"\"\n        store_type = serialized_state.get(\"store_type\")\n        if store_type == \"sqlite\":\n            source_run_id = serialized_state.get(\"run_id\")\n            if source_run_id and source_run_id != self._run_id:\n                self._copy_state_from_run(source_run_id)\n        else:\n            self._write_in_memory_state(serialized_state)\n\n    def _copy_state_from_run(self, source_run_id: str) -> None:\n        \"\"\"Copy state from another run_id using SQL INSERT...SELECT.\"\"\"\n        conn = self._connect()\n        try:\n            now = _utc_now().isoformat()\n            conn.execute(\n                \"\"\"\n                INSERT OR REPLACE INTO workflow_state (run_id, state_json, state_type, state_module, created_at, updated_at)\n                SELECT ?, state_json, state_type, state_module, ?, ?\n                FROM workflow_state WHERE run_id = ?\n                \"\"\",\n                (self._run_id, now, now, source_run_id),\n            )\n            conn.commit()\n        finally:\n            conn.close()\n\n    def _serialize_state(self, state: MODEL_T) -> str:\n        \"\"\"Serialize state model to JSON string.\"\"\"\n        if isinstance(state, DictState):\n            return json.dumps(serialize_dict_state_data(state, self._serializer))\n        return self._serializer.serialize(state)\n\n    def _deserialize_state(self, state_json: str) -> MODEL_T:\n        \"\"\"Deserialize state from JSON string.\"\"\"\n        if issubclass(self.state_type, DictState):\n            data = json.loads(state_json)\n            return deserialize_dict_state_data(data, self._serializer)  # type: ignore[return-value]  # ty: ignore[invalid-return-type]\n        return self._serializer.deserialize(state_json)\n\n    def _create_default_state(self) -> MODEL_T:\n        return self.state_type()\n\n    def _load_state(self) -> MODEL_T:\n        \"\"\"Load state from database. Creates default if row doesn't exist.\"\"\"\n        conn = self._connect()\n        try:\n            cursor = conn.cursor()\n            cursor.execute(\n                \"SELECT state_json FROM workflow_state WHERE run_id = ?\",\n                (self._run_id,),\n            )\n            row = cursor.fetchone()\n            if row is None:\n                state = self._create_default_state()\n                self._save_state(state, conn)\n                conn.commit()\n                return state\n            return self._deserialize_state(row[0])\n        finally:\n            conn.close()\n\n    def _save_state(\n        self, state: MODEL_T, conn: sqlite3.Connection | None = None\n    ) -> None:\n        \"\"\"Save state to database.\"\"\"\n        should_close = conn is None\n        if conn is None:\n            conn = self._connect()\n        try:\n            now = _utc_now().isoformat()\n            state_json = self._serialize_state(state)\n            conn.execute(\n                \"\"\"\n                INSERT INTO workflow_state (run_id, state_json, state_type, state_module, created_at, updated_at)\n                VALUES (?, ?, ?, ?, ?, ?)\n                ON CONFLICT(run_id) DO UPDATE SET\n                    state_json = excluded.state_json,\n                    state_type = excluded.state_type,\n                    state_module = excluded.state_module,\n                    updated_at = excluded.updated_at\n                \"\"\",\n                (\n                    self._run_id,\n                    state_json,\n                    type(state).__name__,\n                    type(state).__module__,\n                    now,\n                    now,\n                ),\n            )\n            if should_close:\n                conn.commit()\n        finally:\n            if should_close:\n                conn.close()\n\n    async def get_state(self) -> MODEL_T:\n        \"\"\"Return a copy of the current state model.\"\"\"\n        state = self._load_state()\n        return state.model_copy()\n\n    async def set_state(self, state: MODEL_T) -> None:\n        \"\"\"Replace or merge into the current state model.\"\"\"\n        conn = self._connect()\n        try:\n            cursor = conn.cursor()\n            cursor.execute(\n                \"SELECT state_json FROM workflow_state WHERE run_id = ?\",\n                (self._run_id,),\n            )\n            row = cursor.fetchone()\n\n            if row is None:\n                self._save_state(state, conn)\n                conn.commit()\n                return\n\n            current_state = self._deserialize_state(row[0])\n            merged = merge_state(current_state, state)\n            self._save_state(merged, conn)  # type: ignore[arg-type]\n            conn.commit()\n        finally:\n            conn.close()\n\n    async def get(self, path: str, default: Any = ...) -> Any:\n        \"\"\"Get a nested value using dot-separated paths.\"\"\"\n        state = self._load_state()\n        return get_by_path(state, path, default)\n\n    async def set(self, path: str, value: Any) -> None:\n        \"\"\"Set a nested value using dot-separated paths.\"\"\"\n        async with self.edit_state() as state:\n            set_by_path(state, path, value)\n\n    async def clear(self) -> None:\n        \"\"\"Reset the state to its type defaults.\"\"\"\n        await self.set_state(create_cleared_state(self.state_type))\n\n    @asynccontextmanager\n    async def edit_state(self) -> AsyncGenerator[MODEL_T, None]:\n        \"\"\"Edit state transactionally under a lock.\"\"\"\n        async with self._lock:\n            state = self._load_state()\n            yield state\n            self._save_state(state)\n\n    def to_dict(self, serializer: BaseSerializer) -> dict[str, Any]:\n        \"\"\"Serialize state store metadata for persistence.\n\n        Returns metadata only — actual state lives in the database.\n        \"\"\"\n        payload = SqliteSerializedState(run_id=self._run_id)\n        return payload.model_dump()\n\n    @classmethod\n    def from_dict(\n        cls,\n        serialized_state: dict[str, Any],\n        serializer: BaseSerializer,\n        db_path: str | None = None,\n        state_type: type[BaseModel] | None = None,\n        run_id: str | None = None,\n    ) -> SqliteStateStore[Any]:\n        \"\"\"Restore a state store from serialized payload.\n\n        Handles both InMemorySerializedState (migrates data to DB on first use)\n        and SqliteSerializedState (reconnects to existing row).\n        \"\"\"\n        if not serialized_state:\n            raise ValueError(\"Cannot restore SqliteStateStore from empty dict\")\n\n        store_type = serialized_state.get(\"store_type\")\n\n        if store_type == \"sqlite\":\n            parsed = SqliteSerializedState.model_validate(serialized_state)\n            effective_run_id = run_id or parsed.run_id\n            if db_path is None:\n                raise ValueError(\"db_path is required for SqliteStateStore.from_dict()\")\n            return cls(\n                db_path=db_path,\n                run_id=effective_run_id,\n                state_type=state_type,  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]\n                serializer=serializer,\n            )\n\n        # InMemory format — migrate data to DB immediately\n        parse_in_memory_state(serialized_state)\n\n        effective_run_id = run_id or str(uuid.uuid4())\n        if db_path is None:\n            raise ValueError(\"db_path is required for SqliteStateStore.from_dict()\")\n        store = cls(\n            db_path=db_path,\n            run_id=effective_run_id,\n            state_type=state_type,  # type: ignore[arg-type]  # ty: ignore[invalid-argument-type]\n            serializer=serializer,\n        )\n        store._write_in_memory_state(serialized_state)\n        return store\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/_store/sqlite/sqlite_workflow_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nimport contextlib\nimport json\nimport sqlite3\nimport weakref\nfrom collections.abc import AsyncIterator\nfrom contextlib import contextmanager\nfrom datetime import datetime\nfrom typing import Any, Iterator, Sequence\n\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom workflows.context import JsonSerializer\nfrom workflows.context.serializers import BaseSerializer\n\nfrom ..abstract_workflow_store import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    PersistentHandler,\n    StoredEvent,\n    StoredTick,\n)\nfrom .migrate import run_migrations as _run_migrations\nfrom .sqlite_state_store import SqliteStateStore\n\n_TICK_PAGE_SIZE = 100\n\n\nclass SqliteWorkflowStore(AbstractWorkflowStore):\n    def __init__(\n        self,\n        db_path: str,\n        poll_interval: float = 1.0,\n        auto_migrate: bool = True,\n        single_connection: bool = False,\n    ) -> None:\n        self.db_path = db_path\n        self.poll_interval = poll_interval\n        self._single_connection = single_connection\n        self._persistent_conn: sqlite3.Connection | None = None\n        self._conditions: weakref.WeakValueDictionary[str, asyncio.Condition] = (\n            weakref.WeakValueDictionary()\n        )\n        if single_connection:\n            self._persistent_conn = self._open_nolock(db_path)\n        if auto_migrate:\n            self._run_migrations()\n\n    @staticmethod\n    def _open_nolock(db_path: str) -> sqlite3.Connection:\n        \"\"\"Open a SQLite connection with file locking disabled.\n\n        Uses the ``unix-none`` VFS so that SQLite never issues ``fcntl``\n        lock calls.  Safe when the database is only accessed by a single\n        process (e.g. AgentCore session storage).\n        \"\"\"\n        return sqlite3.connect(f\"file:{db_path}?vfs=unix-none\", uri=True)\n\n    @contextmanager\n    def _connect(self) -> Iterator[sqlite3.Connection]:\n        if self._single_connection:\n            assert self._persistent_conn is not None\n            yield self._persistent_conn\n        else:\n            conn = sqlite3.connect(self.db_path, timeout=30.0)\n            try:\n                yield conn\n            finally:\n                conn.close()\n\n    def create_state_store(\n        self,\n        run_id: str,\n        state_type: type[Any] | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> SqliteStateStore[Any]:\n        store = SqliteStateStore(\n            db_path=self.db_path,\n            run_id=run_id,\n            state_type=state_type,\n            connection=self._persistent_conn,\n        )\n        if serialized_state is not None and serializer is not None:\n            store._seed_from_serialized(serialized_state, serializer)\n        return store\n\n    def _get_or_create_condition(self, run_id: str) -> asyncio.Condition:\n        cond = self._conditions.get(run_id)\n        if cond is None:\n            cond = asyncio.Condition()\n            self._conditions[run_id] = cond\n        return cond\n\n    def _run_migrations(self) -> None:\n        if self._persistent_conn is not None:\n            _run_migrations(self._persistent_conn)\n            self._persistent_conn.commit()\n        else:\n            self.run_migrations(self.db_path)\n\n    @staticmethod\n    def run_migrations(db_path: str) -> None:\n        \"\"\"Run all pending SQLite schema migrations.\n\n        Safe to call multiple times — only applies migrations not yet applied.\n        \"\"\"\n        conn = sqlite3.connect(db_path, timeout=30.0)\n        try:\n            _run_migrations(conn)\n            conn.commit()\n        finally:\n            conn.close()\n\n    async def query(self, query: HandlerQuery) -> list[PersistentHandler]:\n        filter_spec = self._build_filters(query)\n        if filter_spec is None:\n            return []\n\n        clauses, params = filter_spec\n        sql = \"\"\"SELECT handler_id, workflow_name, status, run_id, error, result,\n                        started_at, updated_at, completed_at, idle_since FROM handlers\"\"\"\n        if clauses:\n            sql = f\"{sql} WHERE {' AND '.join(clauses)}\"\n        with self._connect() as conn:\n            cursor = conn.cursor()\n            cursor.execute(sql, tuple(params))\n            rows = cursor.fetchall()\n\n        return [_row_to_persistent_handler(row) for row in rows]\n\n    async def update(self, handler: PersistentHandler) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                \"\"\"\n                INSERT INTO handlers (handler_id, workflow_name, status, run_id, error, result,\n                                      started_at, updated_at, completed_at, idle_since)\n                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n                ON CONFLICT(handler_id) DO UPDATE SET\n                    workflow_name = excluded.workflow_name,\n                    status = excluded.status,\n                    run_id = excluded.run_id,\n                    error = excluded.error,\n                    result = excluded.result,\n                    started_at = excluded.started_at,\n                    updated_at = excluded.updated_at,\n                    completed_at = excluded.completed_at,\n                    idle_since = excluded.idle_since\n                \"\"\",\n                (\n                    handler.handler_id,\n                    handler.workflow_name,\n                    handler.status,\n                    handler.run_id,\n                    handler.error,\n                    JsonSerializer().serialize(handler.result)\n                    if handler.result is not None\n                    else None,\n                    handler.started_at.isoformat() if handler.started_at else None,\n                    handler.updated_at.isoformat() if handler.updated_at else None,\n                    handler.completed_at.isoformat() if handler.completed_at else None,\n                    handler.idle_since.isoformat() if handler.idle_since else None,\n                ),\n            )\n            conn.commit()\n\n    async def delete(self, query: HandlerQuery) -> int:\n        filter_spec = self._build_filters(query)\n        if filter_spec is None:\n            return 0\n\n        clauses, params = filter_spec\n        if not clauses:\n            return 0\n\n        sql = f\"DELETE FROM handlers WHERE {' AND '.join(clauses)}\"\n        with self._connect() as conn:\n            cursor = conn.cursor()\n            cursor.execute(sql, tuple(params))\n            deleted = cursor.rowcount\n            conn.commit()\n\n        return int(deleted)\n\n    async def append_event(self, run_id: str, event: EventEnvelopeWithMetadata) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                \"\"\"INSERT INTO events (run_id, sequence, timestamp, event_json)\n                VALUES (?, COALESCE((SELECT MAX(sequence) FROM events WHERE run_id = ?), -1) + 1, CURRENT_TIMESTAMP, ?)\"\"\",\n                (\n                    run_id,\n                    run_id,\n                    event.model_dump_json(),\n                ),\n            )\n            conn.commit()\n        condition = self._conditions.get(run_id)\n        if condition is not None:\n            async with condition:\n                condition.notify_all()\n\n    async def query_events(\n        self,\n        run_id: str,\n        after_sequence: int | None = None,\n        limit: int | None = None,\n    ) -> list[StoredEvent]:\n        sql = \"SELECT run_id, sequence, timestamp, event_json FROM events WHERE run_id = ?\"\n        params: list[Any] = [run_id]\n        if after_sequence is not None:\n            sql += \" AND sequence > ?\"\n            params.append(after_sequence)\n        sql += \" ORDER BY sequence\"\n        if limit is not None:\n            sql += \" LIMIT ?\"\n            params.append(limit)\n        with self._connect() as conn:\n            cursor = conn.cursor()\n            cursor.execute(sql, params)\n            rows = cursor.fetchall()\n        return [\n            StoredEvent(\n                run_id=row[0],\n                sequence=row[1],\n                timestamp=datetime.fromisoformat(row[2]),\n                event=EventEnvelopeWithMetadata.model_validate_json(row[3]),\n            )\n            for row in rows\n        ]\n\n    async def subscribe_events(\n        self, run_id: str, after_sequence: int = -1\n    ) -> AsyncIterator[StoredEvent]:\n        condition = self._get_or_create_condition(run_id)\n        cursor = after_sequence\n\n        while True:\n            async with condition:\n                batch = await self.query_events(run_id, after_sequence=cursor)\n                if not batch:\n                    with contextlib.suppress(TimeoutError):\n                        await asyncio.wait_for(\n                            condition.wait(), timeout=self.poll_interval\n                        )\n                    continue\n\n            for event in batch:\n                yield event\n                cursor = event.sequence\n                if self._is_terminal_event(event):\n                    return\n\n    async def append_tick(self, run_id: str, tick_data: dict[str, Any]) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                \"\"\"INSERT INTO ticks (run_id, sequence, timestamp, tick_data)\n                VALUES (?, COALESCE((SELECT MAX(sequence) FROM ticks WHERE run_id = ?), -1) + 1, CURRENT_TIMESTAMP, ?)\"\"\",\n                (\n                    run_id,\n                    run_id,\n                    json.dumps(tick_data),\n                ),\n            )\n            conn.commit()\n\n    async def get_ticks(self, run_id: str) -> list[StoredTick]:\n        with self._connect() as conn:\n            cursor = conn.cursor()\n            cursor.execute(\n                \"SELECT run_id, sequence, timestamp, tick_data FROM ticks WHERE run_id = ? ORDER BY sequence\",\n                (run_id,),\n            )\n            rows = cursor.fetchall()\n        return [\n            StoredTick(\n                run_id=row[0],\n                sequence=row[1],\n                timestamp=datetime.fromisoformat(row[2]),\n                tick_data=json.loads(row[3]),\n            )\n            for row in rows\n        ]\n\n    async def stream_ticks(self, run_id: str) -> AsyncIterator[StoredTick]:\n        seq_cursor: int | None = None\n        while True:\n            if seq_cursor is None:\n                sql = (\n                    \"SELECT run_id, sequence, timestamp, tick_data FROM ticks \"\n                    \"WHERE run_id = ? ORDER BY sequence LIMIT ?\"\n                )\n                params: list[Any] = [run_id, _TICK_PAGE_SIZE]\n            else:\n                sql = (\n                    \"SELECT run_id, sequence, timestamp, tick_data FROM ticks \"\n                    \"WHERE run_id = ? AND sequence > ? ORDER BY sequence LIMIT ?\"\n                )\n                params = [run_id, seq_cursor, _TICK_PAGE_SIZE]\n            with self._connect() as conn:\n                cursor = conn.cursor()\n                cursor.execute(sql, params)\n                rows = cursor.fetchall()\n            for row in rows:\n                tick = StoredTick(\n                    run_id=row[0],\n                    sequence=row[1],\n                    timestamp=datetime.fromisoformat(row[2]),\n                    tick_data=json.loads(row[3]),\n                )\n                yield tick\n                seq_cursor = tick.sequence\n            if len(rows) < _TICK_PAGE_SIZE:\n                return\n\n    def get_legacy_ctx(self, run_id: str) -> dict[str, Any] | None:\n        \"\"\"Read the old ctx column for a run_id, if present.\"\"\"\n        with self._connect() as conn:\n            cursor = conn.cursor()\n            cursor.execute(\n                \"SELECT ctx FROM handlers WHERE run_id = ?\",\n                (run_id,),\n            )\n            row = cursor.fetchone()\n            if row is None or row[0] is None:\n                return None\n            try:\n                data = json.loads(row[0])\n                if not isinstance(data, dict) or not data:\n                    return None\n                return data\n            except (json.JSONDecodeError, TypeError):\n                return None\n\n    def _build_filters(self, query: HandlerQuery) -> tuple[list[str], list[str]] | None:\n        clauses: list[str] = []\n        params: list[str] = []\n\n        def add_in_clause(column: str, values: Sequence[str]) -> None:\n            placeholders = \",\".join([\"?\"] * len(values))\n            clauses.append(f\"{column} IN ({placeholders})\")\n            params.extend(values)\n\n        if query.workflow_name_in is not None:\n            if len(query.workflow_name_in) == 0:\n                return None\n            add_in_clause(\"workflow_name\", query.workflow_name_in)\n\n        if query.handler_id_in is not None:\n            if len(query.handler_id_in) == 0:\n                return None\n            add_in_clause(\"handler_id\", query.handler_id_in)\n\n        if query.run_id_in is not None:\n            if len(query.run_id_in) == 0:\n                return None\n            add_in_clause(\"run_id\", query.run_id_in)\n\n        if query.status_in is not None:\n            if len(query.status_in) == 0:\n                return None\n            add_in_clause(\"status\", query.status_in)\n\n        if query.is_idle is not None:\n            if query.is_idle:\n                clauses.append(\"idle_since IS NOT NULL\")\n            else:\n                clauses.append(\"idle_since IS NULL\")\n\n        if not clauses:\n            return clauses, params\n\n        return clauses, params\n\n\ndef _row_to_persistent_handler(row: tuple) -> PersistentHandler:\n    return PersistentHandler(\n        handler_id=row[0],\n        workflow_name=row[1],\n        status=row[2],\n        run_id=row[3],\n        error=row[4],\n        result=json.loads(row[5]) if row[5] else None,\n        started_at=datetime.fromisoformat(row[6]) if row[6] else None,\n        updated_at=datetime.fromisoformat(row[7]) if row[7] else None,\n        completed_at=datetime.fromisoformat(row[8]) if row[8] else None,\n        idle_since=datetime.fromisoformat(row[9]) if row[9] else None,\n    )\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/py.typed",
    "content": ""
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/server.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport json\nimport logging\nfrom collections.abc import Mapping\nfrom contextlib import asynccontextmanager\nfrom typing import Any, AsyncGenerator\n\nimport uvicorn\nfrom llama_agents.server._runtime.idle_release_runtime import IdleReleaseDecorator\nfrom llama_agents.server._runtime.persistence_runtime import PersistenceDecorator\nfrom llama_agents.server._runtime.server_runtime import ServerRuntimeDecorator\nfrom starlette.middleware import Middleware\nfrom workflows import Workflow\nfrom workflows.events import Event\nfrom workflows.plugins.basic import basic_runtime\nfrom workflows.runtime.types.plugin import Runtime\n\nfrom ._api import _WorkflowAPI\nfrom ._service import _WorkflowService\nfrom ._store.abstract_workflow_store import AbstractWorkflowStore\nfrom ._store.memory_workflow_store import MemoryWorkflowStore\n\nlogger = logging.getLogger(__name__)\n\n\nclass WorkflowServer:\n    \"\"\"HTTP server that exposes workflows as REST APIs.\n\n    Wraps one or more ``Workflow`` instances behind an HTTP API with endpoints\n    for running workflows, streaming events, and sending human-in-the-loop\n    input. Includes a built-in debugging UI served at the root path.\n\n    Example:\n\n        from workflows import Workflow, step\n        from workflows.events import StartEvent, StopEvent\n        from llama_agents.server import WorkflowServer\n\n        class GreetingWorkflow(Workflow):\n            @step\n            async def greet(self, ev: StartEvent) -> StopEvent:\n                name = ev.get(\"name\", \"World\")\n                return StopEvent(result=f\"Hello, {name}!\")\n\n        server = WorkflowServer()\n        server.add_workflow(\"greet\", GreetingWorkflow())\n\n        # Run with: python -m workflows.server my_server.py\n        # Or programmatically:\n        # await server.serve(host=\"0.0.0.0\", port=8080)\n\n    The ASGI application is available as ``server.app`` for embedding in a\n    larger application or mounting behind a reverse proxy.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        middleware: list[Middleware] | None = None,\n        exception_handlers: Mapping[Any, Any] | None = None,\n        workflow_store: AbstractWorkflowStore | None = None,\n        persistence_backoff: list[float] = [0.5, 3],\n        runtime: Runtime | None = None,\n        idle_timeout: float = 60.0,\n        sse_heartbeat_interval: float | None = 25.0,\n        accept_context_api: bool = False,\n    ):\n        \"\"\"Create a new workflow server.\n\n        Args:\n            middleware: Starlette middleware to apply to the ASGI app. Defaults\n                to a permissive CORS configuration. Passing a custom list\n                replaces the default entirely.\n            exception_handlers: Starlette exception handlers mapping exception\n                types to handler callables. Defaults to JSON error responses\n                with logging. Passing a custom mapping replaces the default\n                entirely.\n            workflow_store: Persistence backend for handler state, events, and\n                ticks. Defaults to ``MemoryWorkflowStore``. Use\n                ``SqliteWorkflowStore`` or ``PostgresWorkflowStore`` for\n                durable persistence across restarts.\n            persistence_backoff: Retry delays (in seconds) when writing handler\n                state to the store fails. Each entry is a sleep duration before\n                the next attempt. Defaults to ``[0.5, 3]`` (two retries).\n            runtime: Custom workflow runtime. When ``None`` (the default), the\n                server builds a runtime stack that handles persistence and\n                idle-release automatically. Only override this if you need a\n                custom execution backend.\n            idle_timeout: Seconds to wait after a workflow becomes idle before\n                releasing it from memory. The workflow is automatically\n                reloaded when new events arrive. Defaults to ``60.0``.\n            sse_heartbeat_interval: Seconds between SSE keep-alive comments\n                (``: heartbeat``) on idle connections. Defaults to ``25.0``.\n                Set to ``None`` to disable heartbeats. Only applies to SSE\n                mode; NDJSON streams are unaffected.\n            accept_context_api: Allow the ``\"context\"`` field in run request\n                bodies. Defaults to ``False``. Context deserialization can\n                instantiate arbitrary Pydantic objects via ``importlib``, so\n                only enable this on trusted networks.\n        \"\"\"\n        self._workflow_store = (\n            workflow_store if workflow_store is not None else MemoryWorkflowStore()\n        )\n        inner: Runtime = (\n            runtime\n            if runtime is not None\n            else IdleReleaseDecorator(\n                PersistenceDecorator(basic_runtime, store=self._workflow_store),\n                store=self._workflow_store,\n                idle_timeout=idle_timeout,\n            )\n        )\n        self._runtime: ServerRuntimeDecorator = ServerRuntimeDecorator(\n            inner,\n            store=self._workflow_store,\n            persistence_backoff=list(persistence_backoff),\n        )\n        self._service = _WorkflowService(\n            runtime=self._runtime, store=self._workflow_store\n        )\n\n        self._api = _WorkflowAPI(\n            self._service,\n            middleware=middleware,\n            exception_handlers=dict(exception_handlers) if exception_handlers else None,\n            sse_heartbeat_interval=sse_heartbeat_interval,\n            accept_context_api=accept_context_api,\n        )\n        self.app = self._api.app\n\n    # ------------------------------------------------------------------\n    # Workflow registration\n    # ------------------------------------------------------------------\n\n    def add_workflow(\n        self,\n        name: str,\n        workflow: Workflow,\n        additional_events: list[type[Event]] | None = None,\n    ) -> None:\n        \"\"\"Register a workflow under the given name.\n\n        The workflow becomes available at ``/workflows/{name}/run`` and\n        ``/workflows/{name}/run-nowait``.\n\n        Args:\n            name: URL-safe name for the workflow.\n            workflow: The workflow instance to serve.\n            additional_events: Extra event types to expose in the debugger UI\n                and ``Send Event`` functionality. Use this for events that\n                aren't discoverable from step signatures alone (e.g. events\n                consumed via ``ctx.wait_for_event()``).\n        \"\"\"\n        workflow._switch_workflow_name(name)\n        workflow._switch_runtime(self._runtime)\n\n        if additional_events is not None:\n            self._api.register_additional_events(name, additional_events)\n\n    def get_workflows(self) -> dict[str, Workflow]:\n        \"\"\"Return registered workflows as a dict by name. Only available after start().\"\"\"\n        return {\n            n: wf\n            for n in self._service.get_workflow_names()\n            if (wf := self._service.get_workflow(n)) is not None\n        }\n\n    # ------------------------------------------------------------------\n    # Lifecycle\n    # ------------------------------------------------------------------\n\n    async def start(self) -> WorkflowServer:\n        \"\"\"Resumes previously running workflows, if they were not complete at last shutdown.\n\n        Idle workflows are not resumed - they remain released and will be\n        loaded on-demand when events arrive for them.\n        \"\"\"\n        await self._service.start()\n        return self\n\n    @asynccontextmanager\n    async def contextmanager(self) -> AsyncGenerator[WorkflowServer, None]:\n        \"\"\"Use this server as a context manager to start and stop it\"\"\"\n        await self.start()\n        try:\n            yield self\n        finally:\n            await self.stop()\n\n    async def stop(self) -> None:\n        \"\"\"Gracefully shut down all running workflow handlers.\"\"\"\n        await self._service.stop()\n\n    # ------------------------------------------------------------------\n    # Serve\n    # ------------------------------------------------------------------\n\n    async def serve(\n        self,\n        host: str = \"localhost\",\n        port: int = 80,\n        uvicorn_config: dict[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Start the HTTP server and block until shutdown.\n\n        Calls ``start()`` internally before serving.\n\n        Args:\n            host: Bind address. Defaults to ``\"localhost\"``.\n            port: Bind port. Defaults to ``80``.\n            uvicorn_config: Additional keyword arguments forwarded to\n                ``uvicorn.Config`` (e.g. ``root_path``, ``log_level``).\n        \"\"\"\n        uvicorn_config = uvicorn_config or {}\n\n        config = uvicorn.Config(self.app, host=host, port=port, **uvicorn_config)\n        server = uvicorn.Server(config)\n        logger.info(\n            f\"Starting Workflow server at http://{host}:{port}{uvicorn_config.get('root_path', '/')}\"\n        )\n\n        await server.serve()\n\n    def openapi_schema(self) -> dict:\n        return self._api.openapi_schema()\n\n\nif __name__ == \"__main__\":\n    import argparse\n\n    parser = argparse.ArgumentParser(description=\"Generate OpenAPI schema\")\n    parser.add_argument(\n        \"--output\", type=str, default=\"openapi.json\", help=\"Output file path\"\n    )\n    args = parser.parse_args()\n\n    server = WorkflowServer()\n    dict_schema = server.openapi_schema()\n    with open(args.output, \"w\") as f:\n        json.dump(dict_schema, indent=2, fp=f)\n    print(f\"OpenAPI schema written to {args.output}\")  # noqa: T201\n"
  },
  {
    "path": "packages/llama-agents-server/src/llama_agents/server/static/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>workflow-debugger</title>\n    <script type=\"module\" crossorigin src=\"https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.39/dist/app.js\"></script>\n    <link rel=\"stylesheet\" crossorigin href=\"https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.39/dist/app.css\">\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/conftest.py",
    "content": "# Re-export fixtures from server_test_fixtures for pytest discovery\nfrom __future__ import annotations\n\nfrom collections.abc import Generator\n\nimport pytest\nfrom llama_agents_integration_tests.postgres import (\n    get_asyncpg_dsn,\n    postgres_container,\n)\nfrom server_test_fixtures import *  # noqa: F401, F403\n\n\n@pytest.fixture(scope=\"module\")\ndef postgres_dsn() -> Generator[str, None, None]:\n    \"\"\"Module-scoped disposable Postgres via testcontainers; yields an asyncpg DSN.\"\"\"\n    with postgres_container() as pg:\n        yield get_asyncpg_dsn(pg)\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/server_test_fixtures.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport socket\nimport time\nfrom contextlib import asynccontextmanager\nfrom pathlib import Path\nfrom typing import AsyncGenerator, Awaitable, Callable, TypeVar\n\nimport httpx\nimport pytest\nimport uvicorn\nfrom llama_agents.server import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    MemoryWorkflowStore,\n    SqliteWorkflowStore,\n    WorkflowServer,\n)\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\n\nT = TypeVar(\"T\")\n\n\nasync def async_yield(iterations: int = 10) -> None:\n    \"\"\"Yield to the event loop multiple times to let async tasks run.\"\"\"\n    for _ in range(iterations):\n        await asyncio.sleep(0)\n\n\nasync def wait_for_passing(\n    func: Callable[[], Awaitable[T]],\n    max_duration: float = 5.0,\n    interval: float = 0.05,\n) -> T:\n    start_time = time.monotonic()\n    last_exception = None\n    while time.monotonic() - start_time < max_duration:\n        remaining_duration = max_duration - (time.monotonic() - start_time)\n        try:\n            return await asyncio.wait_for(func(), timeout=remaining_duration)\n        except Exception as e:\n            last_exception = e\n            await asyncio.sleep(interval)\n    if last_exception:\n        raise last_exception\n    else:\n        func_name = getattr(func, \"__name__\", repr(func))\n        raise TimeoutError(\n            f\"Function {func_name} timed out after {max_duration} seconds\"\n        )\n\n\nasync def wait_for_requested_external_event(\n    store: AbstractWorkflowStore,\n    handler_id: str,\n    *,\n    event_type: str = \"RequestedExternalEvent\",\n    max_duration: float = 2.0,\n    interval: float = 0.01,\n) -> str:\n    async def event_was_persisted() -> str:\n        handlers = await store.query(HandlerQuery(handler_id_in=[handler_id]))\n        assert len(handlers) == 1\n        handler = handlers[0]\n        assert handler.run_id is not None\n\n        events = await store.query_events(handler.run_id)\n        if any(\n            event.event.type == event_type or event_type in (event.event.types or [])\n            for event in events\n        ):\n            return handler.run_id\n\n        assert handler.status == \"running\", (\n            f\"handler {handler_id} reached {handler.status} before {event_type}\"\n        )\n        raise AssertionError(f\"{event_type} not persisted for handler {handler_id}\")\n\n    return await wait_for_passing(\n        event_was_persisted,\n        max_duration=max_duration,\n        interval=interval,\n    )\n\n\n@asynccontextmanager\nasync def live_server(\n    server_factory: Callable[[], WorkflowServer],\n) -> AsyncGenerator[tuple[str, WorkflowServer], None]:\n    \"\"\"Start a live HTTP server for testing with atomic port acquisition.\"\"\"\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    try:\n        sock.bind((\"127.0.0.1\", 0))\n        sock.listen(128)\n        port = sock.getsockname()[1]\n\n        server = server_factory()\n\n        config = uvicorn.Config(\n            server.app,\n            host=\"127.0.0.1\",\n            port=port,\n            log_level=\"error\",\n            loop=\"asyncio\",\n        )\n        uv_server = uvicorn.Server(config)\n\n        task = asyncio.create_task(uv_server.serve(sockets=[sock]))\n\n        base_url = f\"http://127.0.0.1:{port}\"\n        async with httpx.AsyncClient(base_url=base_url, timeout=1.0) as client:\n            for _ in range(50):\n                try:\n                    resp = await client.get(\"/health\")\n                    if resp.status_code == 200:\n                        break\n                except Exception:\n                    pass\n                await asyncio.sleep(0.01)\n            else:\n                uv_server.should_exit = True\n                await task\n                raise RuntimeError(\"Live server did not start in time\")\n\n        try:\n            yield base_url, server\n        finally:\n            uv_server.should_exit = True\n            try:\n                await task\n            finally:\n                await server.stop()\n    finally:\n        try:\n            sock.close()\n        except Exception:\n            pass\n\n\nclass SimpleTestWorkflow(Workflow):\n    @step\n    async def process(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        message = await ctx.store.get(\"test_param\", None)\n        if message is None:\n            message = getattr(ev, \"message\", \"default\")\n\n        return StopEvent(result=f\"processed: {message}\")\n\n\nclass ErrorWorkflow(Workflow):\n    @step\n    async def error_step(self, ev: StartEvent) -> StopEvent:\n        raise ValueError(\"Test error\")\n\n\nclass StreamEvent(Event):\n    message: str\n    sequence: int\n\n\nclass StreamingWorkflow(Workflow):\n    @step\n    async def stream_data(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        count = getattr(ev, \"count\", 3)\n\n        for i in range(count):\n            ctx.write_event_to_stream(StreamEvent(message=f\"event_{i}\", sequence=i))\n            await asyncio.sleep(0.01)  # Small delay between events\n\n        return StopEvent(result=f\"completed_{count}_events\")\n\n\nclass RequestedExternalEvent(InputRequiredEvent):\n    message: str\n\n\nclass ExternalEvent(HumanResponseEvent):\n    response: str\n\n\nclass InteractiveWorkflow(Workflow):\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> RequestedExternalEvent:\n        # Wait for an external event\n        return RequestedExternalEvent(message=\"ping\")\n\n    @step\n    async def end(self, ctx: Context, ev: ExternalEvent) -> StopEvent:\n        if ev.response == \"error\":\n            raise RuntimeError(\"Error response received\")\n        return StopEvent(result=f\"received: {ev.response}\")\n\n\nclass CumulativeWorkflow(Workflow):\n    @step\n    async def accumulate(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        # Get the current count from context store, defaulting to 0\n        current_count = await ctx.store.get(\"count\", 0)\n\n        # Get the increment value from the start event, defaulting to 1\n        increment = getattr(ev, \"increment\", 1)\n\n        # Add to the count\n        new_count = current_count + increment\n        await ctx.store.set(\"count\", new_count)\n\n        # Also track run history\n        run_history = await ctx.store.get(\"run_history\", [])\n        run_history.append(f\"run_{len(run_history) + 1}_increment_{increment}\")\n        await ctx.store.set(\"run_history\", run_history)\n\n        return StopEvent(result=f\"count: {new_count}, runs: {len(run_history)}\")\n\n\nclass RequiredStartEvent(StartEvent):\n    message: str\n\n\nclass StructuredStartWorkflow(Workflow):\n    @step\n    async def start(self, ev: RequiredStartEvent) -> StopEvent:\n        return StopEvent(result=ev.message)\n\n\n@pytest.fixture\ndef memory_store() -> MemoryWorkflowStore:\n    return MemoryWorkflowStore()\n\n\n@pytest.fixture\ndef sqlite_store(tmp_path: Path) -> SqliteWorkflowStore:\n    return SqliteWorkflowStore(str(tmp_path / \"test.db\"))\n\n\n@pytest.fixture\ndef simple_test_workflow() -> Workflow:\n    return SimpleTestWorkflow()\n\n\n@pytest.fixture\ndef error_workflow() -> Workflow:\n    return ErrorWorkflow()\n\n\n@pytest.fixture\ndef streaming_workflow() -> Workflow:\n    return StreamingWorkflow()\n\n\n@pytest.fixture\ndef interactive_workflow() -> Workflow:\n    return InteractiveWorkflow()\n\n\n@pytest.fixture\ndef cumulative_workflow() -> Workflow:\n    return CumulativeWorkflow()\n\n\n@pytest.fixture\ndef structured_start_workflow() -> Workflow:\n    return StructuredStartWorkflow()\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_agent_data_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nimport pytest\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom llama_agents.server import (\n    HandlerQuery,\n    PersistentHandler,\n)\nfrom llama_agents.server._store.abstract_workflow_store import Status, StoredEvent\nfrom llama_agents.server._store.agent_data_state_store import AgentDataStateStore\nfrom llama_agents.server._store.agent_data_store import AgentDataStore\nfrom llama_agents_integration_tests.fake_agent_data import (\n    FakeAgentDataBackend,\n    create_agent_data_store,\n)\nfrom server_test_fixtures import wait_for_passing  # type: ignore[import]\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import DictState\nfrom workflows.events import Event, StopEvent\n\n# ---------------------------------------------------------------------------\n# Fixtures\n# ---------------------------------------------------------------------------\n\n\n@pytest.fixture()\ndef backend() -> FakeAgentDataBackend:\n    return FakeAgentDataBackend()\n\n\n@pytest.fixture()\ndef store(\n    backend: FakeAgentDataBackend, monkeypatch: pytest.MonkeyPatch\n) -> AgentDataStore:\n    return create_agent_data_store(backend, monkeypatch)\n\n\ndef make_handler(\n    handler_id: str = \"h1\",\n    workflow_name: str = \"wf\",\n    status: Status = \"running\",\n    run_id: str | None = None,\n    idle_since: datetime | None = None,\n) -> PersistentHandler:\n    return PersistentHandler(\n        handler_id=handler_id,\n        workflow_name=workflow_name,\n        status=status,\n        run_id=run_id,\n        idle_since=idle_since,\n    )\n\n\ndef make_envelope(\n    event: Event | None = None,\n    seq_label: int = 0,\n) -> EventEnvelopeWithMetadata:\n    if event is None:\n        event = Event(data=f\"seq-{seq_label}\")\n    return EventEnvelopeWithMetadata.from_event(event, include_qualified_name=False)\n\n\nasync def _subscribe_and_collect(\n    store: AgentDataStore,\n    run_id: str,\n    after_sequence: int = -1,\n) -> tuple[list[StoredEvent], asyncio.Task[None]]:\n    collected: list[StoredEvent] = []\n    existing_queue_count = len(store._subscriber_queues.get(run_id, ()))\n\n    async def consumer() -> None:\n        async for event in store.subscribe_events(\n            run_id, after_sequence=after_sequence\n        ):\n            collected.append(event)\n\n    task = asyncio.create_task(consumer())\n\n    async def registered() -> None:\n        assert len(store._subscriber_queues.get(run_id, ())) > existing_queue_count\n\n    await wait_for_passing(registered, max_duration=2.0, interval=0.01)\n    return collected, task\n\n\nasync def _wait_collected_count(\n    collected: list[StoredEvent],\n    expected: int,\n) -> None:\n    async def check() -> None:\n        assert len(collected) == expected\n\n    await wait_for_passing(check, max_duration=2.0, interval=0.01)\n\n\n# ---------------------------------------------------------------------------\n# Handler CRUD tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_update_and_query_returns_handler(store: AgentDataStore) -> None:\n    handler = make_handler(handler_id=\"h1\", run_id=\"run-1\")\n    await store.update(handler)\n\n    result = await store.query(HandlerQuery(handler_id_in=[\"h1\"]))\n    assert len(result) == 1\n    assert result[0].handler_id == \"h1\"\n    assert result[0].run_id == \"run-1\"\n\n\n@pytest.mark.asyncio\nasync def test_update_overwrites_existing(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\", status=\"running\"))\n    await store.update(make_handler(handler_id=\"h1\", status=\"completed\"))\n\n    result = await store.query(HandlerQuery(handler_id_in=[\"h1\"]))\n    assert len(result) == 1\n    assert result[0].status == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_run_id(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\", run_id=\"run-1\"))\n    await store.update(make_handler(handler_id=\"h2\", run_id=\"run-2\"))\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert len(result) == 1\n    assert result[0].handler_id == \"h1\"\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_workflow_name(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\", workflow_name=\"wf-a\"))\n    await store.update(make_handler(handler_id=\"h2\", workflow_name=\"wf-b\"))\n\n    result = await store.query(HandlerQuery(workflow_name_in=[\"wf-a\"]))\n    assert len(result) == 1\n    assert result[0].handler_id == \"h1\"\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_status(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\", status=\"running\"))\n    await store.update(make_handler(handler_id=\"h2\", status=\"completed\"))\n\n    result = await store.query(HandlerQuery(status_in=[\"completed\"]))\n    assert len(result) == 1\n    assert result[0].handler_id == \"h2\"\n\n\n@pytest.mark.asyncio\nasync def test_query_with_empty_filter_returns_nothing(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\"))\n    result = await store.query(HandlerQuery(handler_id_in=[]))\n    assert result == []\n\n\n@pytest.mark.asyncio\nasync def test_query_no_filters_returns_all(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\"))\n    await store.update(make_handler(handler_id=\"h2\"))\n\n    result = await store.query(HandlerQuery())\n    assert len(result) == 2\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_is_idle(store: AgentDataStore) -> None:\n    now = datetime.now(timezone.utc)\n    await store.update(make_handler(handler_id=\"h1\", idle_since=now))\n    await store.update(make_handler(handler_id=\"h2\", idle_since=None))\n\n    idle = await store.query(HandlerQuery(is_idle=True))\n    assert len(idle) == 1\n    assert idle[0].handler_id == \"h1\"\n\n    not_idle = await store.query(HandlerQuery(is_idle=False))\n    assert len(not_idle) == 1\n    assert not_idle[0].handler_id == \"h2\"\n\n\n@pytest.mark.asyncio\nasync def test_delete_removes_matching_handlers(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\", workflow_name=\"wf-a\"))\n    await store.update(make_handler(handler_id=\"h2\", workflow_name=\"wf-b\"))\n\n    count = await store.delete(HandlerQuery(workflow_name_in=[\"wf-a\"]))\n    assert count == 1\n\n    remaining = await store.query(HandlerQuery())\n    assert len(remaining) == 1\n    assert remaining[0].handler_id == \"h2\"\n\n\n@pytest.mark.asyncio\nasync def test_delete_invalidates_cache(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\"))\n    # Verify it's cached\n    assert store._id_cache.get(\"h1\") is not None\n\n    await store.delete(HandlerQuery(handler_id_in=[\"h1\"]))\n    assert store._id_cache.get(\"h1\") is None\n\n\ndef _seed_raw_handler(\n    backend: FakeAgentDataBackend,\n    *,\n    handler_id: str,\n    run_id: str | None,\n    status: Status = \"running\",\n    workflow_name: str = \"wf\",\n) -> str:\n    \"\"\"Insert a raw handler row into the fake backend, bypassing update().\"\"\"\n    handler = PersistentHandler(\n        handler_id=handler_id,\n        workflow_name=workflow_name,\n        status=status,\n        run_id=run_id,\n    )\n    item = backend.create(\"test-deploy\", \"handlers\", handler.model_dump(mode=\"json\"))\n    return item[\"id\"]\n\n\n@pytest.mark.asyncio\nasync def test_update_collapses_duplicates_with_matching_run_id(\n    store: AgentDataStore, backend: FakeAgentDataBackend\n) -> None:\n    \"\"\"Duplicate rows for the same handler_id/run_id collapse to one row.\n\n    The survivor is the oldest row, so its created_at is preserved.\n    \"\"\"\n    # Seed two rows for the same handler with identical run_id. The first\n    # insert gets the smaller row-level created_at and should be the survivor.\n    oldest_id = _seed_raw_handler(\n        backend, handler_id=\"dup-1\", run_id=\"run-dup\", status=\"running\"\n    )\n    _seed_raw_handler(backend, handler_id=\"dup-1\", run_id=\"run-dup\", status=\"failed\")\n\n    await store.update(\n        make_handler(handler_id=\"dup-1\", run_id=\"run-dup\", status=\"completed\")\n    )\n\n    rows = backend._get_items(\"test-deploy\", \"handlers\")\n    handler_rows = [r for r in rows if r[\"data\"].get(\"handler_id\") == \"dup-1\"]\n    assert len(handler_rows) == 1\n    assert handler_rows[0][\"id\"] == oldest_id\n    assert handler_rows[0][\"data\"][\"status\"] == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_update_collapses_duplicates_with_mismatched_run_id(\n    store: AgentDataStore, backend: FakeAgentDataBackend\n) -> None:\n    \"\"\"Duplicates collapse regardless of run_id — one row per handler_id.\n\n    The survivor is the oldest row; the latest write's run_id and status are\n    what ends up persisted on it.\n    \"\"\"\n    oldest_id = _seed_raw_handler(\n        backend, handler_id=\"dup-2\", run_id=\"run-a\", status=\"running\"\n    )\n    _seed_raw_handler(backend, handler_id=\"dup-2\", run_id=\"run-b\", status=\"running\")\n\n    await store.update(\n        make_handler(handler_id=\"dup-2\", run_id=\"run-b\", status=\"completed\")\n    )\n\n    rows = backend._get_items(\"test-deploy\", \"handlers\")\n    handler_rows = [r for r in rows if r[\"data\"].get(\"handler_id\") == \"dup-2\"]\n    assert len(handler_rows) == 1\n    assert handler_rows[0][\"id\"] == oldest_id\n    assert handler_rows[0][\"data\"][\"run_id\"] == \"run-b\"\n    assert handler_rows[0][\"data\"][\"status\"] == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_query_multiple_run_ids(store: AgentDataStore) -> None:\n    await store.update(make_handler(handler_id=\"h1\", run_id=\"run-1\"))\n    await store.update(make_handler(handler_id=\"h2\", run_id=\"run-2\"))\n    await store.update(make_handler(handler_id=\"h3\", run_id=\"run-3\"))\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\", \"run-3\"]))\n    ids = {h.handler_id for h in result}\n    assert ids == {\"h1\", \"h3\"}\n\n\n# ---------------------------------------------------------------------------\n# Event journal tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_append_event_and_query(store: AgentDataStore) -> None:\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n\n    result = await store.query_events(\"run-1\")\n    assert len(result) == 1\n    assert result[0].run_id == \"run-1\"\n    assert result[0].sequence == 0\n    assert result[0].event.type == \"Event\"\n\n\n@pytest.mark.asyncio\nasync def test_append_multiple_events(store: AgentDataStore) -> None:\n    for i in range(5):\n        await store.append_event(\"run-1\", make_envelope(seq_label=i))\n\n    result = await store.query_events(\"run-1\")\n    assert len(result) == 5\n    assert [e.sequence for e in result] == [0, 1, 2, 3, 4]\n\n\n@pytest.mark.asyncio\nasync def test_query_events_after_sequence(store: AgentDataStore) -> None:\n    for i in range(5):\n        await store.append_event(\"run-1\", make_envelope(seq_label=i))\n\n    result = await store.query_events(\"run-1\", after_sequence=2)\n    assert len(result) == 2\n    assert [e.sequence for e in result] == [3, 4]\n\n\n@pytest.mark.asyncio\nasync def test_query_events_with_limit(store: AgentDataStore) -> None:\n    for i in range(5):\n        await store.append_event(\"run-1\", make_envelope(seq_label=i))\n\n    result = await store.query_events(\"run-1\", limit=3)\n    assert len(result) == 3\n    assert [e.sequence for e in result] == [0, 1, 2]\n\n\n@pytest.mark.asyncio\nasync def test_query_events_nonexistent_run(store: AgentDataStore) -> None:\n    result = await store.query_events(\"nonexistent\")\n    assert result == []\n\n\n@pytest.mark.asyncio\nasync def test_events_isolated_by_run_id(store: AgentDataStore) -> None:\n    for i in range(3):\n        await store.append_event(\"run-a\", make_envelope(seq_label=i))\n    for i in range(2):\n        await store.append_event(\"run-b\", make_envelope(seq_label=i))\n\n    result_a = await store.query_events(\"run-a\")\n    result_b = await store.query_events(\"run-b\")\n\n    assert len(result_a) == 3\n    assert len(result_b) == 2\n\n\n# ---------------------------------------------------------------------------\n# Event subscription tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_receives_appended(store: AgentDataStore) -> None:\n    collected, task = await _subscribe_and_collect(store, \"run-1\")\n\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    await asyncio.wait_for(task, timeout=2.0)\n    assert len(collected) == 3\n    assert [e.sequence for e in collected] == [0, 1, 2]\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_terminates_on_stop(store: AgentDataStore) -> None:\n    collected, task = await _subscribe_and_collect(store, \"run-1\")\n\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    await asyncio.wait_for(task, timeout=2.0)\n    assert len(collected) == 2\n    assert collected[-1].event.type == \"StopEvent\"\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_already_terminated(store: AgentDataStore) -> None:\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    collected: list[StoredEvent] = []\n    async for event in store.subscribe_events(\"run-1\"):\n        collected.append(event)\n\n    assert len(collected) == 2\n    assert collected[-1].event.type == \"StopEvent\"\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_with_after_sequence(store: AgentDataStore) -> None:\n    for i in range(3):\n        await store.append_event(\"run-1\", make_envelope(seq_label=i))\n\n    collected, task = await _subscribe_and_collect(store, \"run-1\", after_sequence=1)\n\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    await asyncio.wait_for(task, timeout=2.0)\n    assert [e.sequence for e in collected] == [2, 3]\n\n\n# ---------------------------------------------------------------------------\n# Tick journal tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_append_tick_and_get(store: AgentDataStore) -> None:\n    await store.append_tick(\"run-1\", {\"step\": \"a\", \"state\": {}})\n    await store.append_tick(\"run-1\", {\"step\": \"b\", \"state\": {}})\n\n    ticks = await store.get_ticks(\"run-1\")\n    assert len(ticks) == 2\n    assert ticks[0].sequence == 0\n    assert ticks[1].sequence == 1\n    assert ticks[0].tick_data[\"step\"] == \"a\"\n    assert ticks[1].tick_data[\"step\"] == \"b\"\n\n\n@pytest.mark.asyncio\nasync def test_get_ticks_empty(store: AgentDataStore) -> None:\n    ticks = await store.get_ticks(\"nonexistent\")\n    assert ticks == []\n\n\n@pytest.mark.asyncio\nasync def test_ticks_isolated_by_run_id(store: AgentDataStore) -> None:\n    await store.append_tick(\"run-a\", {\"step\": \"a1\"})\n    await store.append_tick(\"run-b\", {\"step\": \"b1\"})\n    await store.append_tick(\"run-b\", {\"step\": \"b2\"})\n\n    assert len(await store.get_ticks(\"run-a\")) == 1\n    assert len(await store.get_ticks(\"run-b\")) == 2\n\n\n# ---------------------------------------------------------------------------\n# State store tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_create_state_store_returns_in_memory(store: AgentDataStore) -> None:\n    state_store = store.create_state_store(\"run-1\")\n    assert isinstance(state_store, AgentDataStateStore)\n\n\n@pytest.mark.asyncio\nasync def test_create_state_store_with_type(store: AgentDataStore) -> None:\n    state_store = store.create_state_store(\"run-1\", state_type=DictState)\n    assert isinstance(state_store, AgentDataStateStore)\n    assert state_store.state_type is DictState\n\n\n# ---------------------------------------------------------------------------\n# LRU cache behavior tests\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_update_uses_cache_on_second_call(store: AgentDataStore) -> None:\n    \"\"\"After the first update caches the ID, subsequent updates use it.\"\"\"\n    await store.update(make_handler(handler_id=\"h1\", status=\"running\"))\n    cached = store._id_cache.get(\"h1\")\n    assert cached is not None\n\n    # Second update should use cached ID (no search)\n    await store.update(make_handler(handler_id=\"h1\", status=\"completed\"))\n    result = await store.query(HandlerQuery(handler_id_in=[\"h1\"]))\n    assert result[0].status == \"completed\"\n\n\n# ---------------------------------------------------------------------------\n# Bug: sequence counters reset across store instances\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_sequence_continues_after_new_store_instance(\n    backend: FakeAgentDataBackend, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    \"\"\"A new store instance should continue sequences from existing data.\"\"\"\n    store1 = create_agent_data_store(backend, monkeypatch)\n    await store1.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store1.append_event(\"run-1\", make_envelope(seq_label=1))\n\n    # Gather in-flight events so they're visible to the next store instance\n    await store1._regroup_events(\"run-1\")\n\n    # Simulate server restart: new store instance, same backend\n    store2 = create_agent_data_store(backend, monkeypatch)\n    await store2.append_event(\"run-1\", make_envelope(seq_label=2))\n\n    events = await store2.query_events(\"run-1\")\n    sequences = [e.sequence for e in events]\n    # Should be [0, 1, 2] with no duplicates\n    assert sequences == [0, 1, 2], (\n        f\"Expected unique sequences [0, 1, 2], got {sequences}\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_tick_sequence_continues_after_new_store_instance(\n    backend: FakeAgentDataBackend, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    \"\"\"Same as above but for ticks.\"\"\"\n    store1 = create_agent_data_store(backend, monkeypatch)\n    await store1.append_tick(\"run-1\", {\"step\": 0})\n    await store1.append_tick(\"run-1\", {\"step\": 1})\n    # Ensure in-flight tick writes land before the new instance queries _max_sequence\n    await store1._regroup_ticks(\"run-1\")\n\n    store2 = create_agent_data_store(backend, monkeypatch)\n    await store2.append_tick(\"run-1\", {\"step\": 2})\n\n    ticks = await store2.get_ticks(\"run-1\")\n    sequences = [t.sequence for t in ticks]\n    assert sequences == [0, 1, 2], (\n        f\"Expected unique sequences [0, 1, 2], got {sequences}\"\n    )\n\n\n# ---------------------------------------------------------------------------\n# Bug: from_dict loses collection name\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_state_store_from_dict_preserves_collection(\n    store: AgentDataStore,\n) -> None:\n    \"\"\"from_dict should produce a store pointing at the same collection.\"\"\"\n    state_store = store.create_state_store(\"run-1\")\n    await state_store.set(path=\"key\", value=\"hello\")\n\n    serialized = state_store.to_dict(JsonSerializer())\n    restored = AgentDataStateStore.from_dict(\n        serialized,\n        JsonSerializer(),\n        client=store._client,\n        run_id=\"run-1\",\n    )\n\n    # Should be able to read data written by the original store\n    val = await restored.get(\"key\")\n    assert val == \"hello\"\n\n\n# ---------------------------------------------------------------------------\n# HTTP client reuse\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_agent_data_client_reuses_http_client(store: AgentDataStore) -> None:\n    client = store._client\n    c1 = client.http_client()\n    c2 = client.http_client()\n    assert c1 is c2\n\n\n# ---------------------------------------------------------------------------\n# In-memory fan-out queues\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_multiple_concurrent_subscribers(\n    store: AgentDataStore,\n) -> None:\n    collected_a, task_a = await _subscribe_and_collect(store, \"run-1\")\n    collected_b, task_b = await _subscribe_and_collect(store, \"run-1\")\n\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    await asyncio.wait_for(task_a, timeout=2.0)\n    await asyncio.wait_for(task_b, timeout=2.0)\n\n    assert len(collected_a) == 3\n    assert len(collected_b) == 3\n    assert [e.sequence for e in collected_a] == [0, 1, 2]\n    assert [e.sequence for e in collected_b] == [0, 1, 2]\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_backfill_and_live(store: AgentDataStore) -> None:\n    # Append some events before subscribing (these will be backfilled)\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n\n    collected, task = await _subscribe_and_collect(store, \"run-1\", after_sequence=-1)\n\n    # Append live events after subscriber is listening\n    await store.append_event(\"run-1\", make_envelope(seq_label=2))\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    await asyncio.wait_for(task, timeout=2.0)\n\n    assert len(collected) == 4\n    assert [e.sequence for e in collected] == [0, 1, 2, 3]\n\n\n# ---------------------------------------------------------------------------\n# Fire-and-forget persistence\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_events_fire_and_forget_during_streaming(\n    store: AgentDataStore, backend: FakeAgentDataBackend\n) -> None:\n    collected, task = await _subscribe_and_collect(store, \"run-1\")\n\n    # Append several non-terminal events (fire-and-forget)\n    for i in range(5):\n        await store.append_event(\"run-1\", make_envelope(seq_label=i))\n\n    # Subscriber receives events immediately via in-memory queue\n    await _wait_collected_count(collected, 5)\n\n    # Terminal event gathers all pending writes\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n    await asyncio.wait_for(task, timeout=2.0)\n\n    events_key = (\"test-deploy\", store._events_collection)\n    persisted_after = len(backend._items.get(events_key, []))\n    assert persisted_after == 6\n    assert len(collected) == 6\n\n\n@pytest.mark.asyncio\nasync def test_terminal_event_gathers_all_pending(\n    store: AgentDataStore, backend: FakeAgentDataBackend\n) -> None:\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    # After terminal append returns, all events should be persisted\n    events_key = (\"test-deploy\", store._events_collection)\n    persisted = len(backend._items.get(events_key, []))\n    assert persisted == 3\n\n\n@pytest.mark.asyncio\nasync def test_events_not_persisted_until_gathered(\n    store: AgentDataStore, backend: FakeAgentDataBackend\n) -> None:\n    \"\"\"Events use fire-and-forget tasks; regroup_events gathers them.\"\"\"\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n\n    # Pending tasks exist but may not have completed yet\n    assert \"run-1\" in store._pending_events\n    assert len(store._pending_events[\"run-1\"]) == 2\n\n    # After regrouping, events are persisted\n    await store._regroup_events(\"run-1\")\n\n    events_key = (\"test-deploy\", store._events_collection)\n    persisted = len(backend._items.get(events_key, []))\n    assert persisted == 2\n\n\n@pytest.mark.asyncio\nasync def test_regroup_events_surfaces_errors(\n    store: AgentDataStore,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"When an event create fails, _regroup_events raises the error.\"\"\"\n    original_create = store._client.create\n\n    async def failing_create(collection: str, data: dict[str, Any]) -> dict[str, Any]:\n        if collection == store._events_collection:\n            raise RuntimeError(\"simulated failure\")\n        return await original_create(collection, data)\n\n    monkeypatch.setattr(store._client, \"create\", failing_create)\n\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n\n    with pytest.raises(RuntimeError, match=\"simulated failure\"):\n        await store._regroup_events(\"run-1\")\n\n\n@pytest.mark.asyncio\nasync def test_cleanup_run_removes_subscriber_queues(store: AgentDataStore) -> None:\n    \"\"\"_cleanup_run should remove the run_id key from _subscriber_queues.\"\"\"\n    store._add_subscriber_queue(\"run-1\")\n    assert \"run-1\" in store._subscriber_queues\n\n    await store._cleanup_run(\"run-1\")\n    assert \"run-1\" not in store._subscriber_queues\n\n\n@pytest.mark.asyncio\nasync def test_cleanup_run_removes_sequence_counters(store: AgentDataStore) -> None:\n    \"\"\"_cleanup_run should remove sequence counters for the completed run.\"\"\"\n    # Populate sequence counters by appending events and ticks\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_tick(\"run-1\", {\"step\": \"a\"})\n\n    assert \"run-1\" in store._event_sequences\n    assert \"run-1\" in store._tick_sequences\n\n    # Trigger cleanup via terminal event\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n\n    assert \"run-1\" not in store._event_sequences\n    assert \"run-1\" not in store._tick_sequences\n\n\n@pytest.mark.asyncio\nasync def test_persist_error_does_not_block_in_memory_delivery(\n    store: AgentDataStore, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    \"\"\"In-memory subscribers receive events even when HTTP persistence fails.\"\"\"\n    collected, task = await _subscribe_and_collect(store, \"run-1\")\n\n    # Make client.create raise to simulate persistence failure\n    original_create = store._client.create\n\n    async def broken_create(collection: str, data: dict[str, Any]) -> dict[str, Any]:\n        if collection == store._events_collection:\n            raise RuntimeError(\"simulated API failure\")\n        return await original_create(collection, data)\n\n    monkeypatch.setattr(store._client, \"create\", broken_create)\n\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n\n    # Subscriber should still receive events via in-memory queue\n    await _wait_collected_count(collected, 2)\n\n    # The failed tasks are still pending — _regroup_events would raise\n    with pytest.raises(RuntimeError, match=\"simulated API failure\"):\n        await store._regroup_events(\"run-1\")\n\n    # Restore create so terminal event can persist and clean up\n    monkeypatch.setattr(store._client, \"create\", original_create)\n    await store.append_event(\"run-1\", make_envelope(event=StopEvent(data=\"done\")))\n    await asyncio.wait_for(task, timeout=2.0)\n    assert len(collected) == 3\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_durable_runtime.py",
    "content": "# ty: ignore[invalid-argument-type, invalid-assignment]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for idle workflow release and reload functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport sqlite3\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nfrom llama_agents.server import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    MemoryWorkflowStore,\n    PersistentHandler,\n    SqliteWorkflowStore,\n    WorkflowServer,\n)\nfrom llama_agents.server._runtime.idle_release_runtime import IdleReleaseDecorator\nfrom llama_agents.server._runtime.persistence_runtime import (\n    PersistenceDecorator,\n)\nfrom server_test_fixtures import wait_for_passing  # type: ignore[import]\nfrom workflows import Context, Workflow, step\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import DictState, serialize_dict_state_data\nfrom workflows.events import Event, HumanResponseEvent, StartEvent, StopEvent\nfrom workflows.runtime.types.internal_state import BrokerState, EventAttempt\n\n\nclass WaitableExternalEvent(HumanResponseEvent):\n    response: str\n\n\nclass WaitingWorkflow(Workflow):\n    \"\"\"Workflow that uses ctx.wait_for_event() to become idle.\"\"\"\n\n    @step\n    async def start_and_wait(self, ctx: Context, ev: StartEvent) -> None:\n        pass\n\n    @step\n    async def end(self, ctx: Context, ev: WaitableExternalEvent) -> StopEvent:\n        return StopEvent(result=f\"received: {ev.response}\")\n\n\ndef _get_idle_release(server: WorkflowServer) -> IdleReleaseDecorator:\n    \"\"\"Extract the IdleReleaseDecorator from the server's runtime stack.\"\"\"\n    inner = server._runtime._decorated\n    assert isinstance(inner, IdleReleaseDecorator)\n    return inner\n\n\ndef _get_persistence(server: WorkflowServer) -> PersistenceDecorator:\n    \"\"\"Extract the PersistenceDecorator from the server's runtime stack.\"\"\"\n    idle_release = _get_idle_release(server)\n    assert isinstance(idle_release._persistence, PersistenceDecorator)\n    return idle_release._persistence\n\n\nasync def wait_handler_status(\n    store: AbstractWorkflowStore,\n    handler_id: str,\n    status: str,\n    max_duration: float = 5.0,\n    interval: float = 0.05,\n) -> PersistentHandler:\n    \"\"\"Wait until a handler reaches the expected status.\"\"\"\n\n    async def check() -> PersistentHandler:\n        found = await store.query(HandlerQuery(handler_id_in=[handler_id]))\n        assert len(found) == 1\n        assert found[0].status == status\n        return found[0]\n\n    return await wait_for_passing(check, max_duration=max_duration, interval=interval)\n\n\nasync def wait_handler_idle(\n    store: AbstractWorkflowStore,\n    handler_id: str,\n    max_duration: float = 5.0,\n    interval: float = 0.05,\n) -> PersistentHandler:\n    \"\"\"Wait until a handler has idle_since set.\"\"\"\n\n    async def check() -> PersistentHandler:\n        found = await store.query(HandlerQuery(handler_id_in=[handler_id]))\n        assert len(found) == 1\n        assert found[0].idle_since is not None\n        return found[0]\n\n    return await wait_for_passing(check, max_duration=max_duration, interval=interval)\n\n\nasync def wait_run_released(\n    idle_release: IdleReleaseDecorator,\n    run_id: str,\n    max_duration: float = 2.0,\n    interval: float = 0.01,\n) -> None:\n    \"\"\"Wait until a run_id is no longer in the active run set.\"\"\"\n\n    async def check() -> None:\n        assert run_id not in idle_release._active_run_ids\n\n    await wait_for_passing(check, max_duration=max_duration, interval=interval)\n\n\nasync def wait_handler_idle_and_released(\n    store: AbstractWorkflowStore,\n    handler_id: str,\n    idle_release: IdleReleaseDecorator,\n    run_id: str,\n    max_duration: float = 5.0,\n) -> PersistentHandler:\n    \"\"\"Wait for the durable idle marker before asserting memory release.\"\"\"\n    handler = await wait_handler_idle(store, handler_id, max_duration=max_duration)\n    await wait_run_released(idle_release, run_id, max_duration=max_duration)\n    return handler\n\n\nasync def wait_state_value(\n    store: AbstractWorkflowStore,\n    run_id: str,\n    key: str,\n    expected: object,\n    max_duration: float = 5.0,\n    interval: float = 0.05,\n) -> None:\n    \"\"\"Wait until the run state store contains the expected value.\"\"\"\n\n    async def check() -> None:\n        state_store = store.create_state_store(run_id)\n        assert await state_store.get(key) == expected\n\n    await wait_for_passing(check, max_duration=max_duration, interval=interval)\n\n\n@pytest.fixture\ndef waiting_workflow() -> WaitingWorkflow:\n    return WaitingWorkflow()\n\n\n@pytest.mark.asyncio\nasync def test_idle_handler_released_from_memory(\n    memory_store: MemoryWorkflowStore, waiting_workflow: WaitingWorkflow\n) -> None:\n    \"\"\"When a workflow becomes idle, its handler is released from memory.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", waiting_workflow)\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(\n            waiting_workflow, \"idle-release-1\"\n        )\n        run_id = handler_data.run_id\n        assert run_id is not None\n\n        idle_release = _get_idle_release(server)\n\n        handler = await wait_handler_idle_and_released(\n            memory_store, \"idle-release-1\", idle_release, run_id\n        )\n\n        # Should still exist in store with status running.\n        assert handler.status == \"running\"\n\n\n@pytest.mark.asyncio\nasync def test_released_handler_reloaded_on_event(\n    memory_store: MemoryWorkflowStore, waiting_workflow: WaitingWorkflow\n) -> None:\n    \"\"\"A released idle handler is reloaded when an event is sent to it.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", waiting_workflow)\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(\n            waiting_workflow, \"reload-test-1\"\n        )\n        run_id = handler_data.run_id\n        assert run_id is not None\n\n        idle_release = _get_idle_release(server)\n\n        await wait_handler_idle_and_released(\n            memory_store, \"reload-test-1\", idle_release, run_id\n        )\n\n        # Send event to wake it up\n        await server._service.send_event(\n            \"reload-test-1\", WaitableExternalEvent(response=\"hello\")\n        )\n\n        # Handler should complete\n        await wait_handler_status(\n            memory_store, \"reload-test-1\", \"completed\", max_duration=2.0, interval=0.01\n        )\n\n\n@pytest.mark.asyncio\nasync def test_idle_since_cleared_on_reload(\n    memory_store: MemoryWorkflowStore, waiting_workflow: WaitingWorkflow\n) -> None:\n    \"\"\"idle_since is cleared in the store when a handler is reloaded.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", waiting_workflow)\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(\n            waiting_workflow, \"idle-clear-1\"\n        )\n        run_id = handler_data.run_id\n        assert run_id is not None\n\n        idle_release = _get_idle_release(server)\n\n        handler = await wait_handler_idle_and_released(\n            memory_store, \"idle-clear-1\", idle_release, run_id\n        )\n\n        # Verify idle_since is set\n        assert handler.idle_since is not None\n\n        # Send event to trigger reload\n        await server._service.send_event(\n            \"idle-clear-1\", WaitableExternalEvent(response=\"wake\")\n        )\n\n        # idle_since should be cleared after reload\n        async def idle_since_cleared() -> None:\n            found = await memory_store.query(\n                HandlerQuery(handler_id_in=[\"idle-clear-1\"])\n            )\n            assert found[0].idle_since is None\n\n        await wait_for_passing(idle_since_cleared, max_duration=2.0, interval=0.01)\n\n\nclass FailingResumeWorkflow(Workflow):\n    \"\"\"Workflow that fails when resumed - used to test error handling in _on_server_start.\"\"\"\n\n    @step\n    async def start_and_fail(self, ev: StartEvent) -> StopEvent:\n        raise ValueError(\"Resume failed intentionally\")\n\n\n@pytest.mark.asyncio\nasync def test_on_server_start_marks_no_ticks_handler_as_failed(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"A handler with no persisted ticks cannot safely fresh-start.\n\n    Such a row represents a zombie: the handler crashed before its first\n    tick landed. Attempting a fresh run builds a StartEvent from empty\n    kwargs, which fails for any workflow with required fields. Mark the\n    handler as failed so it is not retried on every boot.\n    \"\"\"\n    handler_id = \"no-ticks-1\"\n    run_id = \"run-no-ticks-1\"\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=handler_id,\n            workflow_name=\"test\",\n            status=\"running\",\n            run_id=run_id,\n        )\n    )\n\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        handler = await wait_handler_status(memory_store, handler_id, \"failed\")\n        assert handler.error is not None\n        assert \"crashed before persisting any state\" in handler.error\n\n\n@pytest.mark.asyncio\nasync def test_on_server_start_finalizes_terminal_replay_as_completed(\n    memory_store: MemoryWorkflowStore,\n) -> None:\n    \"\"\"A handler whose ticks replay to a terminal StopEvent is marked completed.\"\"\"\n    from server_test_fixtures import SimpleTestWorkflow  # type: ignore[import]\n\n    handler_id = \"terminal-replay-1\"\n\n    # Phase 1: run the workflow to completion so ticks are persisted.\n    server1 = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server1.add_workflow(\"test\", SimpleTestWorkflow())\n    async with server1.contextmanager():\n        wf1 = server1._service._runtime.get_workflow(\"test\")\n        assert wf1 is not None\n        await server1._service.start_workflow(wf1, handler_id)\n        await wait_handler_status(memory_store, handler_id, \"completed\")\n\n    # Phase 2: force the handler back to 'running' so the resume loop picks it\n    # up on next boot — simulates the zombie-row scenario where ticks reached a\n    # terminal state but the handler row was never updated.\n    found = await memory_store.query(HandlerQuery(handler_id_in=[handler_id]))\n    zombie = found[0]\n    zombie.status = \"running\"\n    zombie.result = None\n    zombie.completed_at = None\n    await memory_store.update(zombie)\n\n    server2 = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server2.add_workflow(\"test\", SimpleTestWorkflow())\n    async with server2.contextmanager():\n        handler = await wait_handler_status(memory_store, handler_id, \"completed\")\n        assert handler.result is not None\n        assert handler.result.result == \"processed: default\"\n\n\n@pytest.mark.asyncio\nasync def test_on_server_start_finalizes_terminal_replay_as_failed(\n    memory_store: MemoryWorkflowStore,\n) -> None:\n    \"\"\"A handler whose ticks replay to a terminal failure is marked failed.\"\"\"\n    from server_test_fixtures import ErrorWorkflow  # type: ignore[import]\n\n    handler_id = \"terminal-replay-fail-1\"\n\n    server1 = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server1.add_workflow(\"failing\", ErrorWorkflow())\n    async with server1.contextmanager():\n        wf1 = server1._service._runtime.get_workflow(\"failing\")\n        assert wf1 is not None\n        await server1._service.start_workflow(wf1, handler_id)\n        await wait_handler_status(memory_store, handler_id, \"failed\")\n\n    # Force back to running to simulate the zombie state.\n    found = await memory_store.query(HandlerQuery(handler_id_in=[handler_id]))\n    zombie = found[0]\n    zombie.status = \"running\"\n    zombie.error = None\n    zombie.completed_at = None\n    await memory_store.update(zombie)\n\n    server2 = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server2.add_workflow(\"failing\", ErrorWorkflow())\n    async with server2.contextmanager():\n        handler = await wait_handler_status(memory_store, handler_id, \"failed\")\n        assert handler.error is not None\n\n\n@pytest.mark.asyncio\nasync def test_on_server_start_ignores_unregistered_workflows(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"Only handlers for registered workflows should be touched by resume.\n\n    Both handlers have no ticks. The known-workflow handler gets classified\n    and marked failed by the resume loop; the unknown-workflow handler is\n    skipped entirely and stays untouched.\n    \"\"\"\n\n    # Seed handlers for both registered and unregistered workflow\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=\"known-1\",\n            workflow_name=\"test\",\n            status=\"running\",\n            run_id=\"run-known-1\",\n        )\n    )\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=\"unknown-1\",\n            workflow_name=\"not_registered\",\n            status=\"running\",\n            run_id=\"run-unknown-1\",\n        )\n    )\n\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        idle_release = _get_idle_release(server)\n\n        # Known handler is classified and marked failed (no ticks → zombie)\n        await wait_handler_status(memory_store, \"known-1\", \"failed\")\n\n        # Unknown handler's run_id should NOT be in active runs\n        assert \"run-unknown-1\" not in idle_release._active_run_ids\n\n        # Unknown handler is untouched — still reports running\n        unknown = await memory_store.query(HandlerQuery(handler_id_in=[\"unknown-1\"]))\n        assert unknown[0].status == \"running\"\n\n\n@pytest.mark.asyncio\nasync def test_on_server_start_marks_failed_handler_on_error(\n    memory_store: MemoryWorkflowStore,\n) -> None:\n    \"\"\"If resume fails, handler should be marked as 'failed' in store.\"\"\"\n    handler_id = \"fail-resume-1\"\n    run_id = \"run-fail-resume-1\"\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=handler_id,\n            workflow_name=\"failing\",\n            status=\"running\",\n            run_id=run_id,\n        )\n    )\n\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"failing\", FailingResumeWorkflow())\n\n    async with server.contextmanager():\n        handler = await wait_handler_status(memory_store, handler_id, \"failed\")\n        assert handler.error is not None\n\n\n@pytest.mark.asyncio\nasync def test_on_server_start_ignores_idle_handlers(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"Idle handlers should NOT be resumed on server start.\"\"\"\n    handler_id = \"idle-1\"\n    run_id = \"run-idle-1\"\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=handler_id,\n            workflow_name=\"test\",\n            status=\"running\",\n            run_id=run_id,\n            idle_since=datetime.now(timezone.utc),\n        )\n    )\n\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        idle_release = _get_idle_release(server)\n        persistence = _get_persistence(server)\n\n        async def resume_done() -> None:\n            assert persistence.resume_task is not None\n            assert persistence.resume_task.done()\n\n        await wait_for_passing(resume_done, max_duration=2.0, interval=0.05)\n        assert run_id not in idle_release._active_run_ids\n\n\n@pytest.mark.asyncio\nasync def test_destroy_cancels_resume_task(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"destroy() should cancel the resume_task.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        persistence = _get_persistence(server)\n        assert persistence.resume_task is not None\n\n    # After contextmanager exits, destroy() was called.\n    async def resume_task_stopped() -> None:\n        resume_task = persistence.resume_task\n        assert resume_task is not None\n        assert resume_task.cancelled() or resume_task.done()\n\n    await wait_for_passing(resume_task_stopped, max_duration=2.0, interval=0.01)\n\n\n@pytest.mark.asyncio\nasync def test_destroy_aborts_active_runs(\n    memory_store: MemoryWorkflowStore, waiting_workflow: WaitingWorkflow\n) -> None:\n    \"\"\"destroy() should abort all active runs via _on_server_stop.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", waiting_workflow)\n\n    async with server.contextmanager():\n        idle_release = _get_idle_release(server)\n\n        # Start a workflow that will stay running (waiting for event)\n        await server._service.start_workflow(waiting_workflow, \"destroy-test-1\")\n\n        async def run_is_active() -> None:\n            assert len(idle_release._active_run_ids) > 0\n\n        await wait_for_passing(run_is_active, max_duration=2.0, interval=0.01)\n\n    # After exit, active runs should be cleared.\n    async def active_runs_cleared() -> None:\n        assert len(idle_release._active_run_ids) == 0\n\n    await wait_for_passing(active_runs_cleared, max_duration=2.0, interval=0.01)\n\n\n@pytest.mark.asyncio\nasync def test_ensure_active_run_handler_not_found(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"_ensure_active_run raises ValueError when no handler exists for run_id.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        idle_release = _get_idle_release(server)\n        with pytest.raises(\n            ValueError, match=\"Expected 1 handler for run nonexistent-run-id, got 0\"\n        ):\n            await idle_release._ensure_active_run(\"nonexistent-run-id\")\n\n\n@pytest.mark.asyncio\nasync def test_ensure_active_run_workflow_not_found(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"_ensure_active_run raises ValueError when handler references unregistered workflow.\"\"\"\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=\"h1\",\n            workflow_name=\"unregistered\",\n            status=\"running\",\n            run_id=\"run-unregistered\",\n        )\n    )\n\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        idle_release = _get_idle_release(server)\n        with pytest.raises(ValueError, match=\"Workflow unregistered not found\"):\n            await idle_release._ensure_active_run(\"run-unregistered\")\n\n\n@pytest.mark.asyncio\nasync def test_context_from_ticks_empty_ticks(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"_context_from_ticks returns None when there are no ticks for the run_id.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        persistence = _get_persistence(server)\n        result = await persistence.context_from_ticks(\n            simple_test_workflow, \"run-id-with-no-ticks\"\n        )\n        assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_persistence_retries_on_failure(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"Workflow completes despite transient store write failures thanks to retries.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n    # Use instant retries\n    server._runtime._persistence_backoff = [0, 0]\n\n    original_update = memory_store.update_handler_status\n    fail_count = 0\n\n    async def flaky_update(*args: object, **kwargs: object) -> None:\n        nonlocal fail_count\n        fail_count += 1\n        if fail_count <= 2:\n            raise RuntimeError(\"transient store failure\")\n        await original_update(*args, **kwargs)  # type: ignore[arg-type]\n\n    memory_store.update_handler_status = flaky_update  # type: ignore[assignment]\n\n    async with server.contextmanager():\n        await server._service.start_workflow(simple_test_workflow, \"retry-ok-1\")\n\n        await wait_handler_status(memory_store, \"retry-ok-1\", \"completed\")\n\n    assert fail_count > 2\n\n\n@pytest.mark.asyncio\nasync def test_workflow_cancelled_after_all_retries_fail(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"When store writes always fail, handler never reaches completed status.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n    # Use instant retries with only 2 attempts\n    server._runtime._persistence_backoff = [0, 0]\n    fail_count = 0\n\n    async def always_fail(*args: object, **kwargs: object) -> None:\n        nonlocal fail_count\n        fail_count += 1\n        raise RuntimeError(\"permanent store failure\")\n\n    memory_store.update_handler_status = always_fail  # type: ignore[assignment]\n\n    async with server.contextmanager():\n        await server._service.start_workflow(simple_test_workflow, \"retry-fail-1\")\n\n        async def retries_exhausted() -> None:\n            assert fail_count > 2\n\n        await wait_for_passing(retries_exhausted, max_duration=2.0, interval=0.01)\n\n        # The handler should NOT have reached \"completed\" status\n        found = await memory_store.query(HandlerQuery(handler_id_in=[\"retry-fail-1\"]))\n        assert len(found) == 1\n        assert found[0].status != \"completed\"\n\n\n# --- Legacy ctx migration tests ---\n\n\ndef _make_legacy_ctx_v1(\n    workflow: Workflow, *, state: dict[str, Any] | None = None\n) -> dict[str, Any]:\n    \"\"\"Build a minimal V1 SerializedContext dict with a StartEvent queued.\"\"\"\n    serializer = JsonSerializer()\n    init_state = BrokerState.from_workflow(workflow)\n    init_state.is_running = True\n    # Queue a StartEvent for the first step\n    first_step = next(iter(init_state.workers.keys()))\n    init_state.workers[first_step].queue.append(\n        EventAttempt(event=StartEvent(), attempts=0, first_attempt_at=None)\n    )\n    serialized = init_state.to_serialized(serializer)\n    data = serialized.model_dump()\n    if state:\n        data[\"state\"] = state\n    return data\n\n\ndef _insert_handler_with_ctx(\n    db_path: str,\n    handler_id: str,\n    run_id: str,\n    workflow_name: str,\n    ctx_data: dict[str, Any] | None = None,\n) -> None:\n    \"\"\"Insert a handler row with optional ctx data directly via SQL.\"\"\"\n    conn = sqlite3.connect(db_path)\n    try:\n        conn.execute(\n            \"INSERT INTO handlers (handler_id, workflow_name, status, run_id, ctx) VALUES (?, ?, ?, ?, ?)\",\n            (\n                handler_id,\n                workflow_name,\n                \"running\",\n                run_id,\n                json.dumps(ctx_data) if ctx_data else None,\n            ),\n        )\n        conn.commit()\n    finally:\n        conn.close()\n\n\n@pytest.mark.asyncio\nasync def test_legacy_ctx_no_ticks_resumes_workflow(\n    sqlite_store: SqliteWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"Handler with old ctx data and no ticks should resume from old broker state.\"\"\"\n    ctx_data = _make_legacy_ctx_v1(simple_test_workflow)\n    _insert_handler_with_ctx(\n        sqlite_store.db_path, \"legacy-1\", \"run-legacy-1\", \"test\", ctx_data\n    )\n\n    server = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        await wait_handler_status(sqlite_store, \"legacy-1\", \"completed\")\n\n\n@pytest.mark.asyncio\nasync def test_legacy_ctx_seeds_user_state(\n    sqlite_store: SqliteWorkflowStore,\n) -> None:\n    \"\"\"Handler with old ctx containing user state should seed the state table.\"\"\"\n\n    class StatefulWorkflow(Workflow):\n        @step\n        async def process(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            val = await ctx.store.get(\"my_key\", None)\n            return StopEvent(result=f\"my_key={val}\")\n\n    wf = StatefulWorkflow()\n    serializer = JsonSerializer()\n\n    # Build state data in the InMemory format\n    dict_state = DictState()\n    dict_state[\"my_key\"] = \"hello\"\n    state_data = {\n        \"store_type\": \"in_memory\",\n        \"state_type\": \"DictState\",\n        \"state_module\": \"workflows.context.state_store\",\n        \"state_data\": serialize_dict_state_data(dict_state, serializer, ()),\n    }\n    ctx_data = _make_legacy_ctx_v1(wf, state=state_data)\n    _insert_handler_with_ctx(\n        sqlite_store.db_path, \"state-1\", \"run-state-1\", \"test\", ctx_data\n    )\n\n    server = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", wf)\n\n    async with server.contextmanager():\n        handler = await wait_handler_status(sqlite_store, \"state-1\", \"completed\")\n        assert handler.result is not None\n        assert handler.result.result == \"my_key=hello\"\n\n\n@pytest.mark.asyncio\nasync def test_no_legacy_ctx_no_ticks_marked_failed(\n    sqlite_store: SqliteWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"Handler with no ctx and no ticks is a zombie; mark it failed on resume.\"\"\"\n    _insert_handler_with_ctx(\n        sqlite_store.db_path, \"fresh-1\", \"run-fresh-1\", \"test\", None\n    )\n\n    server = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    async with server.contextmanager():\n        handler = await wait_handler_status(sqlite_store, \"fresh-1\", \"failed\")\n        assert handler.error is not None\n        assert \"crashed before persisting any state\" in handler.error\n\n\n@pytest.mark.asyncio\nasync def test_legacy_ctx_state_not_overwritten_on_second_resume(\n    sqlite_store: SqliteWorkflowStore,\n) -> None:\n    \"\"\"If state table already has data, legacy ctx should not overwrite it.\"\"\"\n\n    class CheckStateWorkflow(Workflow):\n        @step\n        async def process(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            val = await ctx.store.get(\"my_key\", None)\n            return StopEvent(result=f\"my_key={val}\")\n\n    wf = CheckStateWorkflow()\n    serializer = JsonSerializer()\n\n    # Legacy ctx has old_value\n    old_state = DictState()\n    old_state[\"my_key\"] = \"old_value\"\n    state_data = {\n        \"store_type\": \"in_memory\",\n        \"state_type\": \"DictState\",\n        \"state_module\": \"workflows.context.state_store\",\n        \"state_data\": serialize_dict_state_data(old_state, serializer, ()),\n    }\n    ctx_data = _make_legacy_ctx_v1(wf, state=state_data)\n    _insert_handler_with_ctx(\n        sqlite_store.db_path, \"nooverwrite-1\", \"run-nooverwrite-1\", \"test\", ctx_data\n    )\n\n    # Pre-seed the state table with new_value (simulating a previous partial run)\n    state_store = sqlite_store.create_state_store(\"run-nooverwrite-1\")\n    new_state = DictState()\n    new_state[\"my_key\"] = \"new_value\"\n    new_state_data = {\n        \"store_type\": \"in_memory\",\n        \"state_type\": \"DictState\",\n        \"state_module\": \"workflows.context.state_store\",\n        \"state_data\": serialize_dict_state_data(new_state, serializer, ()),\n    }\n    state_store._write_in_memory_state(new_state_data)\n\n    server = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", wf)\n\n    async with server.contextmanager():\n        handler = await wait_handler_status(sqlite_store, \"nooverwrite-1\", \"completed\")\n        # Should have the pre-seeded value, not the legacy ctx value\n        assert handler.result is not None\n        assert handler.result.result == \"my_key=new_value\"\n\n\n# --- Multi-step HITL broker state resumption tests ---\n\n\nclass Step1Done(Event):\n    value: str\n\n\nclass Step2Done(Event):\n    value: str\n\n\nclass HumanInput1(Event):\n    answer: str\n\n\nclass HumanInput2(Event):\n    answer: str\n\n\nclass MultiStepHITLWorkflow(Workflow):\n    \"\"\"Three-step workflow with two human-in-the-loop wait points.\"\"\"\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> Step1Done:\n        await ctx.store.set(\"step\", \"started\")\n        return Step1Done(value=\"step1_complete\")\n\n    @step\n    async def wait_for_human_1(self, ctx: Context, ev: Step1Done) -> HumanInput1:\n        await ctx.store.set(\"step\", \"waiting_for_human_1\")\n        await ctx.store.set(\"step1_value\", ev.value)\n        human = await ctx.wait_for_event(HumanInput1)\n        return human\n\n    @step\n    async def process_human_1(self, ctx: Context, ev: HumanInput1) -> Step2Done:\n        await ctx.store.set(\"step\", \"processed_human_1\")\n        await ctx.store.set(\"human1_answer\", ev.answer)\n        return Step2Done(value=\"step2_complete\")\n\n    @step\n    async def wait_for_human_2(self, ctx: Context, ev: Step2Done) -> HumanInput2:\n        await ctx.store.set(\"step\", \"waiting_for_human_2\")\n        await ctx.store.set(\"step2_value\", ev.value)\n        human = await ctx.wait_for_event(HumanInput2)\n        return human\n\n    @step\n    async def finalize(self, ctx: Context, ev: HumanInput2) -> StopEvent:\n        await ctx.store.set(\"step\", \"finalized\")\n        step1 = await ctx.store.get(\"step1_value\", \"\")\n        human1 = await ctx.store.get(\"human1_answer\", \"\")\n        step2 = await ctx.store.get(\"step2_value\", \"\")\n        return StopEvent(result=f\"{step1}|{human1}|{step2}|{ev.answer}\")\n\n\nHITL_EXTRA_EVENTS = [HumanInput1, HumanInput2]\n\n\n@pytest.mark.asyncio\nasync def test_simple_hitl_cross_server_restart(\n    sqlite_store: SqliteWorkflowStore,\n    waiting_workflow: WaitingWorkflow,\n) -> None:\n    \"\"\"Simple single-wait HITL workflow survives a full server restart.\"\"\"\n    handler_id = \"simple-restart-1\"\n\n    # Server 1: start workflow, let it idle\n    server1 = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n    server1.add_workflow(\"test\", WaitingWorkflow())\n\n    async with server1.contextmanager():\n        wf1 = server1._service._runtime.get_workflow(\"test\")\n        assert wf1 is not None\n        await server1._service.start_workflow(wf1, handler_id)\n\n        await wait_handler_idle(sqlite_store, handler_id)\n\n    # Server 2: send event, expect completion\n    server2 = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n    server2.add_workflow(\"test\", WaitingWorkflow())\n\n    async with server2.contextmanager():\n        await server2._service.send_event(\n            handler_id, WaitableExternalEvent(response=\"hello\")\n        )\n\n        await wait_handler_status(sqlite_store, handler_id, \"completed\")\n\n\n@pytest.mark.asyncio\nasync def test_multistep_hitl_broker_state_survives_restart(\n    sqlite_store: SqliteWorkflowStore,\n) -> None:\n    \"\"\"Multi-step HITL workflow interrupted at each wait point, server restarted,\n    resumes correctly with broker state and user state preserved.\"\"\"\n    handler_id = \"hitl-1\"\n\n    def _make_server() -> WorkflowServer:\n        wf = MultiStepHITLWorkflow()\n        server = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n        server.add_workflow(\"test\", wf, additional_events=HITL_EXTRA_EVENTS)\n        return server\n\n    # Phase 1: Start workflow, let it reach first wait point, then stop server\n    server1 = _make_server()\n\n    async with server1.contextmanager():\n        wf1 = server1._service._runtime.get_workflow(\"test\")\n        assert wf1 is not None\n        await server1._service.start_workflow(wf1, handler_id)\n\n        # Wait for handler to become idle (waiting for HumanInput1)\n        await wait_handler_idle(sqlite_store, handler_id)\n\n    # Verify state was persisted at first wait point\n    run_id = (await sqlite_store.query(HandlerQuery(handler_id_in=[handler_id])))[\n        0\n    ].run_id\n    assert run_id is not None\n    state_store = sqlite_store.create_state_store(run_id)\n    await wait_state_value(sqlite_store, run_id, \"step\", \"waiting_for_human_1\")\n    assert await state_store.get(\"step1_value\") == \"step1_complete\"\n\n    # Phase 2: Restart server, send first human input, let it reach second wait\n    server2 = _make_server()\n\n    async with server2.contextmanager():\n        await server2._service.send_event(handler_id, HumanInput1(answer=\"answer1\"))\n\n        async def handler_idle_2() -> None:\n            found = await sqlite_store.query(HandlerQuery(handler_id_in=[handler_id]))\n            assert len(found) == 1\n            assert found[0].idle_since is not None\n            # Verify we progressed past the first wait\n            ss = sqlite_store.create_state_store(run_id)\n            assert await ss.get(\"step\") == \"waiting_for_human_2\"\n\n        await wait_for_passing(handler_idle_2, max_duration=5.0, interval=0.05)\n\n    # Verify state after second wait\n    state_store2 = sqlite_store.create_state_store(run_id)\n    assert await state_store2.get(\"human1_answer\") == \"answer1\"\n    assert await state_store2.get(\"step2_value\") == \"step2_complete\"\n\n    # Phase 3: Restart server again, send second human input, verify completion\n    server3 = _make_server()\n\n    async with server3.contextmanager():\n        await server3._service.send_event(handler_id, HumanInput2(answer=\"answer2\"))\n\n        handler = await wait_handler_status(sqlite_store, handler_id, \"completed\")\n        assert handler.result is not None\n        assert handler.result.result == \"step1_complete|answer1|step2_complete|answer2\"\n\n\n@pytest.mark.asyncio\nasync def test_multistep_hitl_multiple_restarts_at_same_wait_point(\n    sqlite_store: SqliteWorkflowStore,\n) -> None:\n    \"\"\"Server can be restarted multiple times while idle at the same wait point\n    without losing state.\"\"\"\n    handler_id = \"hitl-multi-restart\"\n\n    def _make_server() -> WorkflowServer:\n        wf = MultiStepHITLWorkflow()\n        server = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n        server.add_workflow(\"test\", wf, additional_events=HITL_EXTRA_EVENTS)\n        return server\n\n    # Start workflow, let it reach first wait point\n    server1 = _make_server()\n\n    async with server1.contextmanager():\n        wf1 = server1._service._runtime.get_workflow(\"test\")\n        assert wf1 is not None\n        await server1._service.start_workflow(wf1, handler_id)\n\n        await wait_handler_idle(sqlite_store, handler_id)\n\n    run_id = (await sqlite_store.query(HandlerQuery(handler_id_in=[handler_id])))[\n        0\n    ].run_id\n    assert run_id is not None\n\n    # Restart server 3 times without sending any events - state should be preserved\n    for _ in range(3):\n        server_n = _make_server()\n\n        async with server_n.contextmanager():\n            persistence = _get_persistence(server_n)\n\n            # Wait for resume task to complete\n            async def resume_done() -> None:\n                assert persistence.resume_task is not None\n                assert persistence.resume_task.done()\n\n            await wait_for_passing(resume_done, max_duration=2.0, interval=0.05)\n\n            # Handler should still be idle (not resumed by _on_server_start).\n            handler = await wait_handler_idle(sqlite_store, handler_id)\n            assert handler.status == \"running\"\n\n    # State should still be intact after multiple restarts\n    await wait_state_value(sqlite_store, run_id, \"step\", \"waiting_for_human_1\")\n\n    # Now actually send the events and complete the workflow\n    server_final = _make_server()\n\n    async with server_final.contextmanager():\n        await server_final._service.send_event(handler_id, HumanInput1(answer=\"final1\"))\n\n        async def idle_at_2() -> None:\n            found = await sqlite_store.query(HandlerQuery(handler_id_in=[handler_id]))\n            assert found[0].idle_since is not None\n            ss = sqlite_store.create_state_store(run_id)\n            assert await ss.get(\"step\") == \"waiting_for_human_2\"\n\n        await wait_for_passing(idle_at_2, max_duration=5.0, interval=0.05)\n\n        await server_final._service.send_event(handler_id, HumanInput2(answer=\"final2\"))\n\n        handler = await wait_handler_status(sqlite_store, handler_id, \"completed\")\n        assert handler.result is not None\n        assert handler.result.result == \"step1_complete|final1|step2_complete|final2\"\n\n\n@pytest.mark.asyncio\nasync def test_tick_content_after_multistep_workflow(\n    sqlite_store: SqliteWorkflowStore,\n) -> None:\n    \"\"\"After a multi-step workflow reaches idle, ticks are stored with expected structure.\"\"\"\n    handler_id = \"tick-verify-1\"\n\n    wf = MultiStepHITLWorkflow()\n    server = WorkflowServer(workflow_store=sqlite_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", wf, additional_events=HITL_EXTRA_EVENTS)\n\n    async with server.contextmanager():\n        wf_ref = server._service._runtime.get_workflow(\"test\")\n        assert wf_ref is not None\n        handler_data = await server._service.start_workflow(wf_ref, handler_id)\n        run_id = handler_data.run_id\n        assert run_id is not None\n\n        # Wait for handler to become idle at first wait point\n        await wait_handler_idle(sqlite_store, handler_id)\n\n        # Verify ticks have expected structure\n        ticks = await sqlite_store.get_ticks(run_id)\n        assert len(ticks) > 0, \"Expected at least one tick after workflow steps run\"\n\n        for tick in ticks:\n            assert tick.run_id == run_id\n            assert isinstance(tick.sequence, int)\n            assert tick.sequence >= 0\n            assert tick.timestamp is not None\n            assert isinstance(tick.tick_data, dict)\n\n\n@pytest.mark.asyncio\nasync def test_concurrent_send_event_to_idle_handler(\n    memory_store: MemoryWorkflowStore, waiting_workflow: WaitingWorkflow\n) -> None:\n    \"\"\"Two concurrent send_event calls to the same idle handler cause no unhandled exceptions.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", waiting_workflow)\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(\n            waiting_workflow, \"concurrent-1\"\n        )\n        run_id = handler_data.run_id\n        assert run_id is not None\n\n        idle_release = _get_idle_release(server)\n\n        await wait_handler_idle_and_released(\n            memory_store, \"concurrent-1\", idle_release, run_id\n        )\n\n        # Fire two send_event calls concurrently\n        results = await asyncio.gather(\n            server._service.send_event(\n                \"concurrent-1\", WaitableExternalEvent(response=\"first\")\n            ),\n            server._service.send_event(\n                \"concurrent-1\", WaitableExternalEvent(response=\"second\")\n            ),\n            return_exceptions=True,\n        )\n\n        # At least one should succeed without error; the other may error or succeed\n        exceptions = [r for r in results if isinstance(r, Exception)]\n        successes = [r for r in results if not isinstance(r, Exception)]\n        assert len(successes) >= 1, (\n            f\"Expected at least one success, got exceptions: {exceptions}\"\n        )\n\n        # Handler should eventually complete (not hang or crash)\n        await wait_handler_status(memory_store, \"concurrent-1\", \"completed\")\n\n\nclass FailAfterWaitEvent(Event):\n    value: str\n\n\nclass FailAfterWaitWorkflow(Workflow):\n    \"\"\"Workflow that waits for an event then raises an error.\"\"\"\n\n    @step\n    async def start_and_wait(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        external = await ctx.wait_for_event(FailAfterWaitEvent)\n        if external.value == \"error\":\n            raise RuntimeError(\"Error response received\")\n        return StopEvent(result=f\"received: {external.value}\")\n\n\n@pytest.mark.asyncio\nasync def test_failed_workflow_after_reload(\n    memory_store: MemoryWorkflowStore,\n) -> None:\n    \"\"\"Workflow that raises after reload ends up with status='failed' and error message.\"\"\"\n    wf = FailAfterWaitWorkflow()\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"test\", wf, additional_events=[FailAfterWaitEvent])\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(wf, \"fail-reload-1\")\n        run_id = handler_data.run_id\n        assert run_id is not None\n\n        idle_release = _get_idle_release(server)\n\n        await wait_handler_idle_and_released(\n            memory_store, \"fail-reload-1\", idle_release, run_id\n        )\n\n        # Send error event to trigger RuntimeError in the workflow\n        await server._service.send_event(\n            \"fail-reload-1\", FailAfterWaitEvent(value=\"error\")\n        )\n\n        # Handler should end up failed with error message\n        handler = await wait_handler_status(memory_store, \"fail-reload-1\", \"failed\")\n        assert handler.error is not None\n        assert \"Error response received\" in handler.error\n\n\n# --- State persistence across handler runs (counter pattern) ---\n\n\nclass IncrementEvent(HumanResponseEvent):\n    pass\n\n\nclass CounterWorkflow(Workflow):\n    \"\"\"Workflow that increments a persistent counter each time it receives an event.\"\"\"\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> None:\n        count = await ctx.store.get(\"count\", 0)\n        await ctx.store.set(\"count\", count + 1)\n\n    @step\n    async def wait_and_increment(self, ctx: Context, ev: IncrementEvent) -> StopEvent:\n        count = await ctx.store.get(\"count\", 0)\n        new_count = count + 1\n        await ctx.store.set(\"count\", new_count)\n        return StopEvent(result=f\"count={new_count}\")\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\n    \"store_factory\",\n    [\n        pytest.param(lambda tmp: MemoryWorkflowStore(), id=\"memory\"),\n        pytest.param(\n            lambda tmp: SqliteWorkflowStore(str(tmp / \"test.db\")), id=\"sqlite\"\n        ),\n    ],\n)\nasync def test_counter_state_persists_across_idle_reload(\n    tmp_path: Path,\n    store_factory: Any,\n) -> None:\n    \"\"\"A counter workflow increments on start, goes idle, reloads on event,\n    and the count reflects both increments.\"\"\"\n    store = store_factory(tmp_path)\n    handler_id = \"counter-1\"\n\n    wf = CounterWorkflow()\n    server = WorkflowServer(workflow_store=store, idle_timeout=0.01)\n    server.add_workflow(\"counter\", wf, additional_events=[IncrementEvent])\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(wf, handler_id)\n        run_id = handler_data.run_id\n        assert run_id is not None\n\n        idle_release = _get_idle_release(server)\n        await wait_handler_idle_and_released(store, handler_id, idle_release, run_id)\n\n        # Verify count=1 after the start step\n        await wait_state_value(store, run_id, \"count\", 1)\n\n        # Send event to reload and increment again\n        await server._service.send_event(handler_id, IncrementEvent())\n\n        handler = await wait_handler_status(store, handler_id, \"completed\")\n        assert handler.result is not None\n        assert handler.result.result == \"count=2\"\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_error_handling.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom typing import AsyncGenerator\n\nimport pytest\nimport pytest_asyncio\nfrom httpx import ASGITransport, AsyncClient\nfrom llama_agents.server import MemoryWorkflowStore, WorkflowServer\nfrom llama_agents.server._store.abstract_workflow_store import (\n    HandlerQuery,\n    PersistentHandler,\n)\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\n\nclass BrokenWorkflow(Workflow):\n    @step\n    async def explode(self, ev: StartEvent) -> StopEvent:\n        raise RuntimeError(\"something went wrong internally\")\n\n\nclass OkWorkflow(Workflow):\n    @step\n    async def run_step(self, ev: StartEvent) -> StopEvent:\n        return StopEvent()\n\n\nclass CrashingStore(MemoryWorkflowStore):\n    \"\"\"Store that raises on specific methods to simulate persistence failures.\"\"\"\n\n    def __init__(self) -> None:\n        super().__init__()\n        self.fail_update: bool = False\n        self.fail_query: bool = False\n\n    async def update(self, handler: PersistentHandler) -> None:\n        if self.fail_update:\n            raise ConnectionError(\"database connection lost\")\n        return await super().update(handler)\n\n    async def query(self, query: HandlerQuery) -> list[PersistentHandler]:\n        if self.fail_query:\n            raise ConnectionError(\"database connection lost\")\n        return await super().query(query)\n\n\n@pytest.fixture\ndef server() -> WorkflowServer:\n    server = WorkflowServer(workflow_store=MemoryWorkflowStore())\n    server.add_workflow(\"broken\", BrokenWorkflow())\n    return server\n\n\n@pytest_asyncio.fixture\nasync def client(server: WorkflowServer) -> AsyncGenerator[AsyncClient, None]:\n    async with server.contextmanager():\n        transport = ASGITransport(app=server.app)\n        async with AsyncClient(transport=transport, base_url=\"http://test\") as client:\n            yield client\n\n\n@pytest.mark.asyncio\nasync def test_malformed_json_returns_400(client: AsyncClient) -> None:\n    # Start a workflow to get a handler_id\n    response = await client.post(\n        \"/workflows/broken/run-nowait\", json={\"start_event\": \"{}\"}\n    )\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    # Send malformed JSON body\n    response = await client.post(\n        f\"/events/{handler_id}\",\n        content=b\"not json\",\n        headers={\"content-type\": \"application/json\"},\n    )\n    assert response.status_code == 400, (\n        f\"Expected 400, got {response.status_code}: {response.text}\"\n    )\n    assert \"detail\" in response.json()\n\n\n@pytest.mark.asyncio\nasync def test_unhandled_exception_returns_json_500(\n    client: AsyncClient, caplog: pytest.LogCaptureFixture\n) -> None:\n    with caplog.at_level(logging.ERROR, logger=\"llama_agents.server._api\"):\n        response = await client.post(\n            \"/workflows/broken/run\", json={\"start_event\": \"{}\"}\n        )\n    assert response.status_code == 500, (\n        f\"Expected 500, got {response.status_code}: {response.text}\"\n    )\n    data = response.json()\n    # Workflow failure returns handler data with error field\n    assert data[\"error\"] == \"something went wrong internally\"\n    assert any(\"finished with status=failed\" in r.message for r in caplog.records)\n\n\n# -- Workflow not found errors --\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/workflows/nonexistent/run\", json={\"start_event\": \"{}\"}\n    )\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Workflow not found\"\n\n\n@pytest.mark.asyncio\nasync def test_run_nowait_workflow_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/workflows/nonexistent/run-nowait\", json={\"start_event\": \"{}\"}\n    )\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Workflow not found\"\n\n\n@pytest.mark.asyncio\nasync def test_workflow_events_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.get(\"/workflows/nonexistent/events\")\n    assert response.status_code == 404\n    assert \"not found\" in response.json()[\"detail\"].lower()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_schema_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.get(\"/workflows/nonexistent/schema\")\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Workflow not found\"\n\n\n@pytest.mark.asyncio\nasync def test_workflow_representation_not_found_returns_404(\n    client: AsyncClient,\n) -> None:\n    response = await client.get(\"/workflows/nonexistent/representation\")\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Workflow not found\"\n\n\n# -- Handler not found errors --\n\n\n@pytest.mark.asyncio\nasync def test_get_handler_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.get(\"/handlers/nonexistent-id\")\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Handler not found\"\n\n\n@pytest.mark.asyncio\nasync def test_get_result_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.get(\"/results/nonexistent-id\")\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Handler not found\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_handler_not_found_returns_404(\n    client: AsyncClient,\n) -> None:\n    response = await client.get(\"/events/nonexistent-id\")\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Handler not found\"\n\n\n@pytest.mark.asyncio\nasync def test_cancel_handler_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.post(\"/handlers/nonexistent-id/cancel\")\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Handler not found\"\n\n\n# -- Invalid request parameter errors --\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_invalid_after_sequence_returns_400(\n    client: AsyncClient,\n) -> None:\n    # Start a workflow to get a valid handler_id\n    response = await client.post(\n        \"/workflows/broken/run-nowait\", json={\"start_event\": \"{}\"}\n    )\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    response = await client.get(\n        f\"/events/{handler_id}\", params={\"after_sequence\": \"not-a-number\"}\n    )\n    assert response.status_code == 400\n    assert \"after_sequence\" in response.json()[\"detail\"]\n\n\n@pytest.mark.asyncio\nasync def test_post_event_missing_event_data_returns_400(\n    client: AsyncClient,\n) -> None:\n    response = await client.post(\n        \"/workflows/broken/run-nowait\", json={\"start_event\": \"{}\"}\n    )\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    response = await client.post(f\"/events/{handler_id}\", json={})\n    assert response.status_code == 400\n    assert response.json()[\"detail\"] == \"Event data is required\"\n\n\n@pytest.mark.asyncio\nasync def test_post_event_handler_not_found_returns_404(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/events/nonexistent-id\", json={\"event\": {\"type\": \"SomeEvent\"}}\n    )\n    assert response.status_code == 404\n    assert response.json()[\"detail\"] == \"Handler not found\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_invalid_json_returns_400(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/workflows/broken/run\",\n        content=b\"not json\",\n        headers={\"content-type\": \"application/json\"},\n    )\n    assert response.status_code == 400\n    assert \"Invalid JSON body\" in response.json()[\"detail\"]\n\n\n@pytest.mark.asyncio\nasync def test_run_nowait_invalid_json_returns_400(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/workflows/broken/run-nowait\",\n        content=b\"not json\",\n        headers={\"content-type\": \"application/json\"},\n    )\n    assert response.status_code == 400\n    assert \"Invalid JSON body\" in response.json()[\"detail\"]\n\n\n# -- Post event to completed workflow --\n\n\n@pytest.mark.asyncio\nasync def test_post_event_to_completed_workflow_returns_409(\n    client: AsyncClient,\n) -> None:\n    import asyncio\n\n    response = await client.post(\n        \"/workflows/broken/run-nowait\", json={\"start_event\": \"{}\"}\n    )\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    # Poll until the handler reaches terminal status\n    for _ in range(50):\n        result = await client.get(f\"/handlers/{handler_id}\")\n        if result.status_code == 200 and result.json().get(\"status\") in (\n            \"completed\",\n            \"failed\",\n        ):\n            break\n        await asyncio.sleep(0.1)\n\n    response = await client.post(\n        f\"/events/{handler_id}\",\n        json={\"event\": {\"type\": \"StartEvent\", \"data\": \"{}\"}},\n    )\n    assert response.status_code == 409\n    assert response.json()[\"detail\"] == \"Workflow already completed\"\n\n\n# -- 500 errors: store/runtime failures --\n\n\ndef _make_crashing_server(\n    store: CrashingStore,\n) -> WorkflowServer:\n    server = WorkflowServer(\n        workflow_store=store,\n        persistence_backoff=[],  # no retries, fail fast\n    )\n    server.add_workflow(\"ok\", OkWorkflow())\n    server.add_workflow(\"broken\", BrokenWorkflow())\n    return server\n\n\n@pytest_asyncio.fixture\nasync def crashing_store_and_client() -> AsyncGenerator[\n    tuple[CrashingStore, AsyncClient], None\n]:\n    store = CrashingStore()\n    server = _make_crashing_server(store)\n    async with server.contextmanager():\n        # raise_app_exceptions=False lets Starlette's exception handlers return\n        # JSON 500 responses instead of httpx re-raising the exception\n        transport = ASGITransport(app=server.app, raise_app_exceptions=False)\n        async with AsyncClient(transport=transport, base_url=\"http://test\") as client:\n            yield store, client\n\n\n_API_LOGGER = \"llama_agents.server._api\"\n_SERVICE_LOGGER = \"llama_agents.server._service\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_store_crash_returns_500(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    \"\"\"Store fails during initial handler persistence -> 500.\"\"\"\n    store, client = crashing_store_and_client\n    store.fail_update = True\n    with caplog.at_level(logging.ERROR, logger=_API_LOGGER):\n        response = await client.post(\"/workflows/ok/run\", json={\"start_event\": \"{}\"})\n    assert response.status_code == 500\n    assert \"detail\" in response.json()\n    assert any(\"Error running workflow\" in r.message for r in caplog.records)\n\n\n@pytest.mark.asyncio\nasync def test_run_nowait_store_crash_returns_500(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n) -> None:\n    \"\"\"Store fails during initial handler persistence on run-nowait -> 500.\"\"\"\n    store, client = crashing_store_and_client\n    store.fail_update = True\n    response = await client.post(\"/workflows/ok/run-nowait\", json={\"start_event\": \"{}\"})\n    assert response.status_code == 500\n    assert \"Initial persistence failed\" in response.json()[\"detail\"]\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_step_crash_returns_500_with_error(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    \"\"\"Workflow step raises -> sync run returns 500 with error in body.\"\"\"\n    _, client = crashing_store_and_client\n    with caplog.at_level(logging.ERROR):\n        response = await client.post(\n            \"/workflows/broken/run\", json={\"start_event\": \"{}\"}\n        )\n    assert response.status_code == 500\n    data = response.json()\n    assert data[\"status\"] == \"failed\"\n    assert data[\"error\"] == \"something went wrong internally\"\n    # The service layer logs the exception\n    assert any(\"raised an exception\" in r.message for r in caplog.records)\n    # The API layer logs the failed status\n    assert any(\"finished with status=failed\" in r.message for r in caplog.records)\n\n\n@pytest.mark.asyncio\nasync def test_run_nowait_step_crash_still_returns_200(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n) -> None:\n    \"\"\"run-nowait returns 200 immediately; failure is only visible via handler poll.\"\"\"\n    _, client = crashing_store_and_client\n    response = await client.post(\n        \"/workflows/broken/run-nowait\", json={\"start_event\": \"{}\"}\n    )\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    # Poll until the handler reaches terminal status\n    for _ in range(50):\n        result = await client.get(f\"/handlers/{handler_id}\")\n        if result.json().get(\"status\") == \"failed\":\n            break\n        await asyncio.sleep(0.1)\n    else:\n        pytest.fail(\"handler never reached 'failed' status\")\n    assert result.json()[\"status\"] == \"failed\"\n    assert result.json()[\"error\"] == \"something went wrong internally\"\n\n\n_RUNTIME_LOGGER = \"llama_agents.server._runtime.server_runtime\"\n\n\n@pytest.mark.asyncio\nasync def test_run_nowait_step_crash_logs_error(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    \"\"\"Async workflow failure is logged by the server runtime adapter.\"\"\"\n    _, client = crashing_store_and_client\n    with caplog.at_level(logging.ERROR, logger=_RUNTIME_LOGGER):\n        response = await client.post(\n            \"/workflows/broken/run-nowait\", json={\"start_event\": \"{}\"}\n        )\n        assert response.status_code == 200\n        handler_id = response.json()[\"handler_id\"]\n\n        # Wait for the workflow to fail\n        for _ in range(50):\n            result = await client.get(f\"/handlers/{handler_id}\")\n            if result.json().get(\"status\") == \"failed\":\n                break\n            await asyncio.sleep(0.1)\n        else:\n            pytest.fail(\"handler never reached 'failed' status\")\n\n    runtime_records = [r for r in caplog.records if r.name == _RUNTIME_LOGGER]\n    assert any(\n        \"something went wrong internally\" in r.message for r in runtime_records\n    ), f\"Expected runtime error log, got: {[r.message for r in runtime_records]}\"\n\n\n@pytest.mark.asyncio\nasync def test_get_handler_store_crash_returns_500(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    \"\"\"Store.query() fails when fetching handler -> unhandled exception -> 500.\"\"\"\n    store, client = crashing_store_and_client\n    response = await client.post(\"/workflows/ok/run-nowait\", json={\"start_event\": \"{}\"})\n    handler_id = response.json()[\"handler_id\"]\n\n    store.fail_query = True\n    with caplog.at_level(logging.ERROR, logger=_API_LOGGER):\n        response = await client.get(f\"/handlers/{handler_id}\")\n    assert response.status_code == 500\n    assert \"detail\" in response.json()\n    assert any(\"Unhandled exception\" in r.message for r in caplog.records)\n\n\n@pytest.mark.asyncio\nasync def test_cancel_handler_store_crash_returns_500(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    \"\"\"Store.query() fails when cancelling handler -> unhandled exception -> 500.\"\"\"\n    store, client = crashing_store_and_client\n    response = await client.post(\"/workflows/ok/run-nowait\", json={\"start_event\": \"{}\"})\n    handler_id = response.json()[\"handler_id\"]\n\n    store.fail_query = True\n    with caplog.at_level(logging.ERROR, logger=_API_LOGGER):\n        response = await client.post(f\"/handlers/{handler_id}/cancel\")\n    assert response.status_code == 500\n    assert \"detail\" in response.json()\n    assert any(\"Unhandled exception\" in r.message for r in caplog.records)\n\n\n@pytest.mark.asyncio\nasync def test_post_event_store_crash_returns_500(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    \"\"\"Store.query() fails when resolving handler for event send -> 500.\"\"\"\n    store, client = crashing_store_and_client\n    response = await client.post(\"/workflows/ok/run-nowait\", json={\"start_event\": \"{}\"})\n    handler_id = response.json()[\"handler_id\"]\n\n    store.fail_query = True\n    with caplog.at_level(logging.ERROR, logger=_API_LOGGER):\n        response = await client.post(\n            f\"/events/{handler_id}\",\n            json={\"event\": {\"type\": \"StartEvent\", \"data\": \"{}\"}},\n        )\n    assert response.status_code == 500\n    assert \"detail\" in response.json()\n    assert any(\"Unhandled exception\" in r.message for r in caplog.records)\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_store_crash_returns_500(\n    crashing_store_and_client: tuple[CrashingStore, AsyncClient],\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    \"\"\"Store.query() fails when resolving event stream -> 500.\"\"\"\n    store, client = crashing_store_and_client\n    response = await client.post(\"/workflows/ok/run-nowait\", json={\"start_event\": \"{}\"})\n    handler_id = response.json()[\"handler_id\"]\n\n    store.fail_query = True\n    with caplog.at_level(logging.ERROR, logger=_API_LOGGER):\n        response = await client.get(f\"/events/{handler_id}\")\n    assert response.status_code == 500\n    assert \"detail\" in response.json()\n    assert any(\"Unhandled exception\" in r.message for r in caplog.records)\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_event_interceptor.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for EventInterceptorDecorator.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\nfrom unittest.mock import MagicMock\n\nfrom llama_agents.server._runtime.event_interceptor import (\n    EventInterceptorDecorator,\n)\nfrom workflows.context.state_store import StateStore\nfrom workflows.events import Event, StopEvent\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    WaitResult,\n    WaitResultTimeout,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\nfrom workflows.workflow import Workflow\n\n\nclass _RecordingInternalAdapter(InternalRunAdapter):\n    \"\"\"Adapter that records calls for assertion.\"\"\"\n\n    def __init__(self) -> None:\n        self.events_written: list[Event] = []\n        self.ticks_sent: list[WorkflowTick] = []\n        self.closed = False\n\n    @property\n    def run_id(self) -> str:\n        return \"test-run\"\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        self.events_written.append(event)\n\n    async def get_now(self) -> float:\n        return 1.0\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        self.ticks_sent.append(tick)\n\n    async def wait_receive(self, timeout_seconds: float | None = None) -> WaitResult:\n        return WaitResultTimeout()\n\n    async def close(self) -> None:\n        self.closed = True\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return None\n\n\nclass _StubRuntime(Runtime):\n    def __init__(self, adapter: InternalRunAdapter) -> None:\n        super().__init__()\n        self._adapter = adapter\n\n    def register(self, workflow: Any) -> RegisteredWorkflow:\n        return RegisteredWorkflow(\n            workflow=workflow, workflow_run_fn=MagicMock(), steps={}\n        )\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Any,\n        init_state: Any,\n        start_event: Any = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: Any = None,\n    ) -> ExternalRunAdapter:\n        raise NotImplementedError\n\n    def get_internal_adapter(self, workflow: Any) -> InternalRunAdapter:\n        return self._adapter\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        raise NotImplementedError\n\n    async def launch(self) -> None:\n        pass\n\n    async def destroy(self) -> None:\n        pass\n\n\nasync def test_write_to_event_stream_is_blocked() -> None:\n    inner_adapter = _RecordingInternalAdapter()\n    runtime = _StubRuntime(inner_adapter)\n    decorator = EventInterceptorDecorator(runtime)\n\n    wf = MagicMock(spec=Workflow)\n    adapter = decorator.get_internal_adapter(wf)\n\n    await adapter.write_to_event_stream(StopEvent(result=\"hello\"))\n\n    assert inner_adapter.events_written == [], \"Events should not reach inner adapter\"\n\n\nasync def test_other_methods_pass_through() -> None:\n    inner_adapter = _RecordingInternalAdapter()\n    runtime = _StubRuntime(inner_adapter)\n    decorator = EventInterceptorDecorator(runtime)\n\n    wf = MagicMock(spec=Workflow)\n    adapter = decorator.get_internal_adapter(wf)\n\n    assert adapter.run_id == \"test-run\"\n    assert await adapter.get_now() == 1.0\n    await adapter.close()\n    assert inner_adapter.closed\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_handler_serialization.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport json\nfrom datetime import datetime, timezone\n\nimport pytest\nfrom llama_agents.client.protocol import HandlerData\nfrom llama_agents.server import PersistentHandler\nfrom llama_agents.server._service import handler_data_from_persistent\nfrom workflows.events import StopEvent\n\n\nclass MyStopEvent(StopEvent):\n    message: str\n\n\n@pytest.mark.asyncio\nasync def test_handler_data_from_persistent_json_roundtrip() -> None:\n    now = datetime.now(timezone.utc)\n    stop_event = MyStopEvent(message=\"ok\")\n\n    persistent = PersistentHandler(\n        handler_id=\"handler-1\",\n        workflow_name=\"wf\",\n        run_id=\"test-run-id\",\n        status=\"completed\",\n        started_at=now,\n        updated_at=now,\n        completed_at=now,\n        result=stop_event,\n    )\n\n    response_model = handler_data_from_persistent(persistent)\n    # JSON serialization should not error\n    s = json.dumps(response_model.model_dump())\n    reparsed_dict = json.loads(s)\n    reparsed = HandlerData.model_validate(reparsed_dict)\n\n    # Round-trip consistency\n    assert reparsed == response_model\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_idle_release_live_http.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Black box tests for idle release behavior over live HTTP.\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\nfrom llama_agents.client.client import WorkflowClient\nfrom llama_agents.server import MemoryWorkflowStore, WorkflowServer\nfrom server_test_fixtures import (\n    live_server,  # type: ignore[import]\n    wait_for_passing,  # type: ignore[import]\n)\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent, WorkflowIdleEvent\n\n\nclass WaitableExternalEvent(Event):\n    response: str\n\n\nclass WaitingWorkflow(Workflow):\n    @step\n    async def start_and_wait(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        external = await ctx.wait_for_event(WaitableExternalEvent)\n        return StopEvent(result=f\"received: {external.response}\")\n\n\n@pytest.mark.asyncio\nasync def test_fast_idle_timeout_does_not_drop_valid_event() -> None:\n    def make_server() -> WorkflowServer:\n        server = WorkflowServer(\n            workflow_store=MemoryWorkflowStore(),\n        )\n        server.add_workflow(\n            \"waiting\",\n            WaitingWorkflow(),\n            additional_events=[WaitableExternalEvent],\n        )\n        return server\n\n    async with live_server(make_server) as (base_url, _server):\n        client = WorkflowClient(base_url=base_url)\n        started = await client.run_workflow_nowait(\"waiting\")\n        handler_id = started.handler_id\n\n        async for env in client.get_workflow_events(\n            handler_id, include_internal_events=True\n        ):\n            event = env.load_event([WorkflowIdleEvent])\n            if isinstance(event, WorkflowIdleEvent):\n                send = await client.send_event(\n                    handler_id, WaitableExternalEvent(response=\"hello\")\n                )\n                assert send.status == \"sent\"\n                break\n\n        async def handler_completed() -> None:\n            data = await client.get_handler(handler_id)\n            assert data.status == \"completed\"\n            assert data.result is not None\n            assert data.result.value.get(\"result\") == \"received: hello\"\n\n        await wait_for_passing(handler_completed, max_duration=2.0, interval=0.01)\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_keyed_lock.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for KeyedLock utility.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\nimport pytest\nfrom llama_agents.server._keyed_lock import KeyedLock\n\n\n@pytest.fixture\ndef locks() -> KeyedLock:\n    return KeyedLock()\n\n\nasync def test_basic_locking(locks: KeyedLock) -> None:\n    \"\"\"Test that basic lock acquisition and release works.\"\"\"\n    async with locks(\"key1\"):\n        pass\n    # Lock should be reusable after release\n    async with locks(\"key1\"):\n        pass\n\n\nasync def test_mutual_exclusion(locks: KeyedLock) -> None:\n    \"\"\"Test that the lock provides mutual exclusion.\"\"\"\n    order: list[str] = []\n\n    async def task(name: str, delay: float) -> None:\n        async with locks(\"shared\"):\n            order.append(f\"{name}_enter\")\n            await asyncio.sleep(delay)\n            order.append(f\"{name}_exit\")\n\n    t1 = asyncio.create_task(task(\"first\", 0.01))\n    await asyncio.sleep(0.001)  # Let first task acquire lock\n    t2 = asyncio.create_task(task(\"second\", 0.01))\n\n    await asyncio.gather(t1, t2)\n\n    assert order == [\"first_enter\", \"first_exit\", \"second_enter\", \"second_exit\"]\n\n\nasync def test_exception_in_critical_section(locks: KeyedLock) -> None:\n    \"\"\"Test that lock is released even if exception occurs.\"\"\"\n    with pytest.raises(ValueError, match=\"test error\"):\n        async with locks(\"key\"):\n            raise ValueError(\"test error\")\n\n    # Lock should still be usable after exception\n    async with locks(\"key\"):\n        pass\n\n\nasync def test_cancellation_cleanup(locks: KeyedLock) -> None:\n    \"\"\"Test that lock is released on task cancellation.\"\"\"\n    started = asyncio.Event()\n\n    async def task() -> None:\n        async with locks(\"key\"):\n            started.set()\n            await asyncio.sleep(10)  # Long sleep to be cancelled\n\n    t = asyncio.create_task(task())\n    await started.wait()\n\n    t.cancel()\n    with pytest.raises(asyncio.CancelledError):\n        await t\n\n    # Lock should be usable after cancellation\n    async with locks(\"key\"):\n        pass\n\n\nasync def test_waiter_cancelled_while_waiting(locks: KeyedLock) -> None:\n    \"\"\"Test that cancellation while waiting cleans up properly.\"\"\"\n    holder_started = asyncio.Event()\n    waiter_started = asyncio.Event()\n\n    async def holder() -> None:\n        async with locks(\"key\"):\n            holder_started.set()\n            await asyncio.sleep(0.1)\n\n    async def waiter() -> None:\n        await holder_started.wait()\n        waiter_started.set()\n        async with locks(\"key\"):\n            pass  # Should never get here\n\n    t1 = asyncio.create_task(holder())\n    t2 = asyncio.create_task(waiter())\n\n    await waiter_started.wait()\n    await asyncio.sleep(0.01)  # Let waiter register\n\n    t2.cancel()\n    with pytest.raises(asyncio.CancelledError):\n        await t2\n\n    await t1\n    # Lock should be usable again after cancellation\n    async with locks(\"key\"):\n        pass\n\n\nasync def test_parallel_access_mutual_exclusion_with_race_detection(\n    locks: KeyedLock,\n) -> None:\n    \"\"\"Test mutual exclusion using race condition detection.\"\"\"\n    shared_value = 0\n    iterations = 50\n\n    async def increment_task() -> None:\n        nonlocal shared_value\n        async with locks(\"key\"):\n            current = shared_value\n            await asyncio.sleep(0)  # Yield to event loop\n            shared_value = current + 1\n\n    tasks = [asyncio.create_task(increment_task()) for _ in range(iterations)]\n    await asyncio.gather(*tasks)\n\n    assert shared_value == iterations\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_lru_cache.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom llama_agents.server._lru_cache import LRUCache\n\n\ndef test_put_and_get() -> None:\n    cache: LRUCache[str, int] = LRUCache()\n    cache.put(\"a\", 1)\n    assert cache.get(\"a\") == 1\n\n\ndef test_get_missing_returns_none() -> None:\n    cache: LRUCache[str, int] = LRUCache()\n    assert cache.get(\"missing\") is None\n\n\ndef test_delete() -> None:\n    cache: LRUCache[str, int] = LRUCache()\n    cache.put(\"a\", 1)\n    cache.delete(\"a\")\n    assert cache.get(\"a\") is None\n    assert len(cache) == 0\n\n\ndef test_delete_missing_key_is_noop() -> None:\n    cache: LRUCache[str, int] = LRUCache()\n    cache.delete(\"nope\")\n\n\ndef test_len() -> None:\n    cache: LRUCache[str, int] = LRUCache()\n    assert len(cache) == 0\n    cache.put(\"a\", 1)\n    cache.put(\"b\", 2)\n    assert len(cache) == 2\n\n\ndef test_evicts_lru_when_full() -> None:\n    cache: LRUCache[str, int] = LRUCache(maxsize=2)\n    cache.put(\"a\", 1)\n    cache.put(\"b\", 2)\n    cache.put(\"c\", 3)\n    assert cache.get(\"a\") is None\n    assert cache.get(\"b\") == 2\n    assert cache.get(\"c\") == 3\n\n\ndef test_get_refreshes_recency() -> None:\n    cache: LRUCache[str, int] = LRUCache(maxsize=2)\n    cache.put(\"a\", 1)\n    cache.put(\"b\", 2)\n    cache.get(\"a\")  # refresh \"a\"\n    cache.put(\"c\", 3)  # should evict \"b\"\n    assert cache.get(\"a\") == 1\n    assert cache.get(\"b\") is None\n\n\ndef test_put_existing_key_refreshes_recency() -> None:\n    cache: LRUCache[str, int] = LRUCache(maxsize=2)\n    cache.put(\"a\", 1)\n    cache.put(\"b\", 2)\n    cache.put(\"a\", 10)  # update and refresh \"a\"\n    cache.put(\"c\", 3)  # should evict \"b\"\n    assert cache.get(\"a\") == 10\n    assert cache.get(\"b\") is None\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_main.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\nfrom unittest.mock import patch\n\nimport pytest\nfrom llama_agents.server.__main__ import run_server\n\n\ndef test_no_file_path_argument(capsys: Any) -> None:\n    \"\"\"Test that the script exits with usage message when no file path is provided.\"\"\"\n    with patch(\"sys.argv\", [\"workflows.server\"]):\n        with pytest.raises(SystemExit) as exc_info:\n            run_server()\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        captured = capsys.readouterr()\n        assert (\n            \"Usage: python -m workflows.server <path_to_server_script>\" in captured.err\n        )\n\n\ndef test_nonexistent_file(capsys: Any) -> None:\n    \"\"\"Test that the script exits when file doesn't exist.\"\"\"\n    with patch(\"sys.argv\", [\"workflows.server\", \"/nonexistent/file.py\"]):\n        with pytest.raises(SystemExit) as exc_info:\n            run_server()\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        captured = capsys.readouterr()\n        assert \"Error: File '/nonexistent/file.py' not found\" in captured.err\n\n\ndef test_directory_instead_of_file(capsys: Any, tmp_path: Path) -> None:\n    \"\"\"Test that the script exits when a directory is provided instead of a file.\"\"\"\n    test_dir = tmp_path / \"test_dir\"\n    test_dir.mkdir()\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_dir)]):\n        with pytest.raises(SystemExit) as exc_info:\n            run_server()\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        captured = capsys.readouterr()\n        assert f\"Error: '{test_dir}' is not a file\" in captured.err\n\n\ndef test_no_workflow_server_instance(capsys: Any, tmp_path: Path) -> None:\n    \"\"\"Test that the script exits when no WorkflowServer instance is found.\"\"\"\n    # Create a test Python file without a WorkflowServer instance\n    test_file = tmp_path / \"no_server.py\"\n    test_file.write_text(\"\"\"\n# A file without WorkflowServer instance\nsome_variable = \"hello\"\nanother_variable = 42\n\"\"\")\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_file)]):\n        with pytest.raises(SystemExit) as exc_info:\n            run_server()\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        captured = capsys.readouterr()\n        assert (\n            f\"Error: No WorkflowServer instance found in '{test_file}'\" in captured.err\n        )\n\n\ndef test_workflow_server_with_custom_name(tmp_path: Path) -> None:\n    \"\"\"Test that the script finds WorkflowServer instance with any variable name.\"\"\"\n    # Create a test Python file with WorkflowServer instance named differently\n    test_file = tmp_path / \"custom_server.py\"\n    test_file.write_text(\"\"\"\nfrom llama_agents.server.server import WorkflowServer\n\n# WorkflowServer with custom name\nmy_app = WorkflowServer()\n\"\"\")\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_file)]):\n        with patch(\"uvicorn.run\") as mock_uvicorn:\n            run_server()\n\n            # Verify uvicorn.run was called with the server's app\n            mock_uvicorn.assert_called_once()\n            args, kwargs = mock_uvicorn.call_args\n            assert hasattr(args[0], \"routes\")  # Check it's a Starlette app\n            assert kwargs[\"host\"] == \"0.0.0.0\"\n            assert kwargs[\"port\"] == 8080\n\n\ndef test_multiple_workflow_servers_uses_first(tmp_path: Path) -> None:\n    \"\"\"Test that when multiple WorkflowServer instances exist, the first one is used.\"\"\"\n    test_file = tmp_path / \"multiple_servers.py\"\n    test_file.write_text(\"\"\"\nfrom llama_agents.server.server import WorkflowServer\n\n# Multiple WorkflowServer instances\nfirst_server = WorkflowServer()\nsecond_server = WorkflowServer()\n\"\"\")\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_file)]):\n        with patch(\"uvicorn.run\") as mock_uvicorn:\n            run_server()\n\n            # Should use the first server found\n            mock_uvicorn.assert_called_once()\n\n\ndef test_environment_variables(tmp_path: Path) -> None:\n    \"\"\"Test that environment variables are used for host and port.\"\"\"\n    test_file = tmp_path / \"env_test.py\"\n    test_file.write_text(\"\"\"\nfrom llama_agents.server.server import WorkflowServer\n\nserver = WorkflowServer()\n\"\"\")\n\n    test_host = \"127.0.0.1\"\n    test_port = \"9000\"\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_file)]):\n        with patch.dict(\n            os.environ,\n            {\n                \"WORKFLOWS_PY_SERVER_HOST\": test_host,\n                \"WORKFLOWS_PY_SERVER_PORT\": test_port,\n            },\n        ):\n            with patch(\"uvicorn.run\") as mock_uvicorn:\n                run_server()\n\n                mock_uvicorn.assert_called_once()\n                args, kwargs = mock_uvicorn.call_args\n                assert kwargs[\"host\"] == test_host\n                assert kwargs[\"port\"] == int(test_port)\n\n\ndef test_module_loading_error(capsys: Any, tmp_path: Path) -> None:\n    \"\"\"Test that script handles module loading errors gracefully.\"\"\"\n    # Create a file with syntax error\n    test_file = tmp_path / \"syntax_error.py\"\n    test_file.write_text(\"\"\"\nfrom llama_agents.server.server import WorkflowServer\n\n# Syntax error\ndef invalid_syntax(\n    server = WorkflowServer()\n\"\"\")\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_file)]):\n        with pytest.raises(SystemExit) as exc_info:\n            run_server()\n\n        assert isinstance(exc_info.value, SystemExit)\n        assert exc_info.value.code == 1\n        captured = capsys.readouterr()\n        assert \"Error loading or running server:\" in captured.err\n\n\ndef test_spec_creation_failure(capsys: Any, tmp_path: Path) -> None:\n    \"\"\"Test handling of spec creation failure.\"\"\"\n    test_file = tmp_path / \"test.py\"\n    test_file.write_text(\"server = 'test'\")\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_file)]):\n        with patch(\"importlib.util.spec_from_file_location\", return_value=None):\n            with pytest.raises(SystemExit) as exc_info:\n                run_server()\n\n            assert isinstance(exc_info.value, SystemExit)\n            assert exc_info.value.code == 1\n            captured = capsys.readouterr()\n            assert \"Unable to get spec from module\" in captured.err\n\n\ndef test_non_workflow_server_objects_ignored(tmp_path: Path) -> None:\n    \"\"\"Test that objects that aren't WorkflowServer instances are ignored.\"\"\"\n    test_file = tmp_path / \"mixed_objects.py\"\n    test_file.write_text(\"\"\"\nfrom llama_agents.server.server import WorkflowServer\n\n# Various non-WorkflowServer objects\nstring_var = \"not a server\"\nnumber_var = 42\nlist_var = [1, 2, 3]\ndict_var = {\"key\": \"value\"}\n\nclass FakeServer:\n    pass\n\nfake_server = FakeServer()\n\n# The actual WorkflowServer\nreal_server = WorkflowServer()\n\"\"\")\n\n    with patch(\"sys.argv\", [\"workflows.server\", str(test_file)]):\n        with patch(\"uvicorn.run\") as mock_uvicorn:\n            run_server()\n\n            # Should find and use the real WorkflowServer\n            mock_uvicorn.assert_called_once()\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_memory_workflow_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nimport pytest\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom llama_agents.server import (\n    AbstractWorkflowStore,\n    HandlerQuery,\n    MemoryWorkflowStore,\n    PersistentHandler,\n)\nfrom llama_agents.server._store.abstract_workflow_store import Status, StoredEvent\nfrom workflows.events import (\n    Event,\n    StopEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n)\n\nT0 = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)\n\n\ndef _ts(seconds: int) -> datetime:\n    return datetime(2024, 1, 1, 0, 0, seconds, tzinfo=timezone.utc)\n\n\ndef _handler(\n    handler_id: str = \"h1\",\n    workflow_name: str = \"wf\",\n    status: Status = \"running\",\n    **kwargs: Any,\n) -> PersistentHandler:\n    return PersistentHandler(\n        handler_id=handler_id,\n        workflow_name=workflow_name,\n        status=status,\n        **kwargs,\n    )\n\n\nasync def _insert(\n    store: MemoryWorkflowStore,\n    handler_id: str = \"h1\",\n    workflow_name: str = \"wf\",\n    status: Status = \"running\",\n    **kwargs: Any,\n) -> PersistentHandler:\n    h = _handler(\n        handler_id=handler_id, workflow_name=workflow_name, status=status, **kwargs\n    )\n    await store.update(h)\n    return h\n\n\nasync def _query_ids(store: MemoryWorkflowStore, **kwargs: Any) -> set[str]:\n    result = await store.query(HandlerQuery(**kwargs))\n    return {h.handler_id for h in result}\n\n\ndef _make_stored_event(event: Event, run_id: str = \"run-1\") -> StoredEvent:\n    return StoredEvent(\n        run_id=run_id,\n        sequence=0,\n        timestamp=datetime.now(timezone.utc),\n        event=EventEnvelopeWithMetadata.from_event(event),\n    )\n\n\n@pytest.fixture\ndef store() -> MemoryWorkflowStore:\n    return MemoryWorkflowStore()\n\n\n@pytest.mark.asyncio\nasync def test_update_and_query_returns_inserted_handler(\n    store: MemoryWorkflowStore,\n) -> None:\n    await _insert(store, handler_id=\"h1\", workflow_name=\"wf_a\")\n\n    result = await store.query(\n        HandlerQuery(workflow_name_in=[\"wf_a\"], status_in=[\"running\"])\n    )\n    assert len(result) == 1\n    found = result[0]\n    assert found.handler_id == \"h1\"\n    assert found.workflow_name == \"wf_a\"\n    assert found.status == \"running\"\n\n\n@pytest.mark.asyncio\nasync def test_update_on_conflict_overwrites_existing_row(\n    store: MemoryWorkflowStore,\n) -> None:\n    await _insert(store, handler_id=\"h2\", workflow_name=\"wf_b\")\n    await _insert(store, handler_id=\"h2\", workflow_name=\"wf_b\", status=\"completed\")\n\n    assert (\n        await _query_ids(store, workflow_name_in=[\"wf_b\"], status_in=[\"running\"])\n        == set()\n    )\n\n    result_completed = await store.query(\n        HandlerQuery(workflow_name_in=[\"wf_b\"], status_in=[\"completed\"])\n    )\n    assert len(result_completed) == 1\n    found = result_completed[0]\n    assert found.handler_id == \"h2\"\n    assert found.workflow_name == \"wf_b\"\n    assert found.status == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_delete_filters_by_query(store: MemoryWorkflowStore) -> None:\n    await _insert(\n        store, handler_id=\"delete-me\", workflow_name=\"wf_delete\", status=\"completed\"\n    )\n    await _insert(store, handler_id=\"keep-me\", workflow_name=\"wf_keep\")\n\n    deleted = await store.delete(HandlerQuery(handler_id_in=[\"delete-me\"]))\n    assert deleted == 1\n    assert await _query_ids(store) == {\"keep-me\"}\n\n\n@pytest.mark.asyncio\nasync def test_delete_noop_on_empty_filter(store: MemoryWorkflowStore) -> None:\n    await _insert(\n        store, handler_id=\"delete-me\", workflow_name=\"wf_delete\", status=\"completed\"\n    )\n\n    deleted = await store.delete(HandlerQuery(handler_id_in=[]))\n    assert deleted == 0\n    assert await _query_ids(store) == {\"delete-me\"}\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_handler_id_and_empty_lists(\n    store: MemoryWorkflowStore,\n) -> None:\n    for hid, wf in [(\"h1\", \"wf_a\"), (\"h2\", \"wf_a\"), (\"h3\", \"wf_b\")]:\n        await _insert(store, handler_id=hid, workflow_name=wf)\n\n    assert await _query_ids(store, handler_id_in=[\"h1\", \"h3\"]) == {\"h1\", \"h3\"}\n    assert await store.query(HandlerQuery(handler_id_in=[])) == []\n    assert await store.query(HandlerQuery(workflow_name_in=[])) == []\n    assert await _query_ids(store) == {\"h1\", \"h2\", \"h3\"}\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_multiple_statuses(store: MemoryWorkflowStore) -> None:\n    statuses: list[tuple[str, Status]] = [\n        (\"h1\", \"running\"),\n        (\"h2\", \"completed\"),\n        (\"h3\", \"failed\"),\n        (\"h4\", \"cancelled\"),\n    ]\n    for hid, status in statuses:\n        await _insert(store, handler_id=hid, status=status)\n\n    assert await _query_ids(store, status_in=[\"completed\", \"failed\"]) == {\"h2\", \"h3\"}\n    assert await store.query(HandlerQuery(status_in=[])) == []\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_workflow_name(store: MemoryWorkflowStore) -> None:\n    await _insert(store, handler_id=\"h1\", workflow_name=\"wf_a\")\n    await _insert(store, handler_id=\"h2\", workflow_name=\"wf_b\")\n    await _insert(store, handler_id=\"h3\", workflow_name=\"wf_a\", status=\"completed\")\n\n    assert await _query_ids(store, workflow_name_in=[\"wf_a\"]) == {\"h1\", \"h3\"}\n    assert await _query_ids(store, workflow_name_in=[\"wf_a\", \"wf_b\"]) == {\n        \"h1\",\n        \"h2\",\n        \"h3\",\n    }\n\n\n@pytest.mark.asyncio\nasync def test_query_combines_multiple_filters(store: MemoryWorkflowStore) -> None:\n    await _insert(store, handler_id=\"h1\", workflow_name=\"wf_a\")\n    await _insert(store, handler_id=\"h2\", workflow_name=\"wf_a\", status=\"completed\")\n    await _insert(store, handler_id=\"h3\", workflow_name=\"wf_b\")\n    await _insert(store, handler_id=\"h4\", workflow_name=\"wf_b\", status=\"completed\")\n\n    assert await _query_ids(\n        store, workflow_name_in=[\"wf_a\"], status_in=[\"running\"]\n    ) == {\"h1\"}\n    assert await _query_ids(\n        store,\n        handler_id_in=[\"h2\", \"h4\"],\n        workflow_name_in=[\"wf_a\"],\n        status_in=[\"completed\"],\n    ) == {\"h2\"}\n\n\n@pytest.mark.asyncio\nasync def test_delete_removes_multiple_matching_handlers(\n    store: MemoryWorkflowStore,\n) -> None:\n    for i in range(5):\n        await _insert(\n            store, handler_id=f\"h{i}\", status=\"completed\" if i % 2 == 0 else \"running\"\n        )\n\n    deleted = await store.delete(HandlerQuery(status_in=[\"completed\"]))\n    assert deleted == 3\n    assert await _query_ids(store) == {\"h1\", \"h3\"}\n\n\n@pytest.mark.asyncio\nasync def test_store_handles_all_datetime_fields(store: MemoryWorkflowStore) -> None:\n    now = datetime.now(timezone.utc)\n    stop = StopEvent(result={\"output\": \"success\"})\n    await _insert(\n        store,\n        status=\"completed\",\n        run_id=\"run123\",\n        error=None,\n        result=stop,\n        started_at=now,\n        updated_at=now,\n        completed_at=now,\n    )\n\n    result = await store.query(HandlerQuery(handler_id_in=[\"h1\"]))\n    assert len(result) == 1\n    found = result[0]\n    assert found.run_id == \"run123\"\n    assert found.result == stop\n    assert found.started_at == now\n    assert found.updated_at == now\n    assert found.completed_at == now\n\n\n@pytest.mark.asyncio\nasync def test_store_handles_error_field(store: MemoryWorkflowStore) -> None:\n    await _insert(store, status=\"failed\", error=\"Something went wrong\")\n\n    result = await store.query(HandlerQuery(handler_id_in=[\"h1\"]))\n    assert len(result) == 1\n    assert result[0].error == \"Something went wrong\"\n\n\n@pytest.mark.asyncio\nasync def test_empty_store_returns_empty_results(store: MemoryWorkflowStore) -> None:\n    assert await store.query(HandlerQuery()) == []\n    assert await store.delete(HandlerQuery(handler_id_in=[\"nonexistent\"])) == 0\n\n\n@pytest.mark.asyncio\nasync def test_update_handler_status_with_nonexistent_run_id(\n    store: MemoryWorkflowStore,\n) -> None:\n    await store.update_handler_status(\"nonexistent-run-id\", status=\"completed\")\n\n\n@pytest.mark.asyncio\nasync def test_update_handler_status_sets_status_and_completed_at(\n    store: MemoryWorkflowStore,\n) -> None:\n    await _insert(store, run_id=\"run-1\")\n\n    await store.update_handler_status(\"run-1\", status=\"completed\")\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert len(result) == 1\n    assert result[0].status == \"completed\"\n    assert result[0].updated_at is not None\n    assert result[0].completed_at is not None\n\n\n@pytest.mark.asyncio\nasync def test_update_handler_status_with_result(store: MemoryWorkflowStore) -> None:\n    await _insert(store, run_id=\"run-1\")\n\n    stop = StopEvent(result={\"answer\": 42})\n    await store.update_handler_status(\"run-1\", status=\"completed\", result=stop)\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert result[0].status == \"completed\"\n    assert result[0].result == stop\n\n\n@pytest.mark.asyncio\nasync def test_update_handler_status_with_error(store: MemoryWorkflowStore) -> None:\n    await _insert(store, run_id=\"run-1\")\n\n    await store.update_handler_status(\"run-1\", status=\"failed\", error=\"boom\")\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert result[0].status == \"failed\"\n    assert result[0].error == \"boom\"\n    assert result[0].completed_at is not None\n\n\n@pytest.mark.asyncio\nasync def test_update_handler_status_idle_since_explicit_none_clears(\n    store: MemoryWorkflowStore,\n) -> None:\n    now = datetime.now(timezone.utc)\n    await _insert(store, run_id=\"run-1\", idle_since=now)\n\n    await store.update_handler_status(\"run-1\", idle_since=None)\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert result[0].idle_since is None\n\n\n@pytest.mark.asyncio\nasync def test_update_handler_status_idle_since_unset_preserves(\n    store: MemoryWorkflowStore,\n) -> None:\n    now = datetime.now(timezone.utc)\n    await _insert(store, run_id=\"run-1\", idle_since=now)\n\n    await store.update_handler_status(\"run-1\", status=\"running\")\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert result[0].idle_since == now\n\n\n@pytest.mark.asyncio\nasync def test_update_handler_status_non_terminal_does_not_set_completed_at(\n    store: MemoryWorkflowStore,\n) -> None:\n    await _insert(store, run_id=\"run-1\")\n\n    await store.update_handler_status(\"run-1\", status=\"running\")\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert result[0].completed_at is None\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"terminal_status\", [\"completed\", \"failed\", \"cancelled\"])\nasync def test_update_handler_status_terminal_sets_completed_at(\n    store: MemoryWorkflowStore,\n    terminal_status: Status,\n) -> None:\n    await _insert(store, run_id=\"run-1\")\n\n    await store.update_handler_status(\"run-1\", status=terminal_status)\n\n    result = await store.query(HandlerQuery(run_id_in=[\"run-1\"]))\n    assert result[0].completed_at is not None\n\n\ndef test_is_terminal_event_stop_event() -> None:\n    stored = _make_stored_event(StopEvent(result=\"done\"))\n    assert AbstractWorkflowStore._is_terminal_event(stored) is True\n\n\ndef test_is_terminal_event_regular_event() -> None:\n    stored = _make_stored_event(Event())\n    assert AbstractWorkflowStore._is_terminal_event(stored) is False\n\n\ndef test_is_terminal_event_workflow_failed_event() -> None:\n    event = WorkflowFailedEvent(\n        step_name=\"my_step\",\n        exception=ValueError(\"bad value\"),\n        attempts=1,\n        elapsed_seconds=0.1,\n    )\n    stored = _make_stored_event(event)\n    assert AbstractWorkflowStore._is_terminal_event(stored) is True\n\n\ndef test_is_terminal_event_workflow_cancelled_event() -> None:\n    stored = _make_stored_event(WorkflowCancelledEvent())\n    assert AbstractWorkflowStore._is_terminal_event(stored) is True\n\n\n# --- max_completed history cap tests ---\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_default_is_1000() -> None:\n    assert MemoryWorkflowStore().max_completed == 1000\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_none_means_unlimited() -> None:\n    store = MemoryWorkflowStore(max_completed=None)\n    assert store.max_completed is None\n\n    for i in range(50):\n        await _insert(\n            store,\n            handler_id=f\"h{i}\",\n            status=\"completed\",\n            run_id=f\"run-{i}\",\n            completed_at=_ts(i),\n        )\n\n    assert len(await store.query(HandlerQuery())) == 50\n\n\ndef test_max_completed_negative_raises_value_error() -> None:\n    with pytest.raises(ValueError, match=\"max_completed must be >= 0 or None\"):\n        MemoryWorkflowStore(max_completed=-1)\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_evicts_oldest_when_exceeded() -> None:\n    store = MemoryWorkflowStore(max_completed=3)\n\n    for i in range(5):\n        await _insert(\n            store,\n            handler_id=f\"h{i}\",\n            status=\"completed\",\n            run_id=f\"run-{i}\",\n            completed_at=_ts(i),\n        )\n\n    assert await _query_ids(store) == {\"h2\", \"h3\", \"h4\"}\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_does_not_evict_running_handlers() -> None:\n    store = MemoryWorkflowStore(max_completed=2)\n\n    for i in range(3):\n        await _insert(store, handler_id=f\"running-{i}\", run_id=f\"run-r{i}\")\n\n    for i in range(3):\n        await _insert(\n            store,\n            handler_id=f\"done-{i}\",\n            status=\"completed\",\n            run_id=f\"run-d{i}\",\n            completed_at=_ts(i),\n        )\n\n    assert await _query_ids(store) == {\n        \"running-0\",\n        \"running-1\",\n        \"running-2\",\n        \"done-1\",\n        \"done-2\",\n    }\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_applies_to_all_terminal_statuses() -> None:\n    store = MemoryWorkflowStore(max_completed=2)\n\n    await _insert(\n        store, handler_id=\"h-completed\", status=\"completed\", completed_at=_ts(0)\n    )\n    await _insert(store, handler_id=\"h-failed\", status=\"failed\", completed_at=_ts(1))\n    await _insert(\n        store, handler_id=\"h-cancelled\", status=\"cancelled\", completed_at=_ts(2)\n    )\n\n    assert await _query_ids(store) == {\"h-failed\", \"h-cancelled\"}\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_cleans_up_events_ticks_and_state() -> None:\n    store = MemoryWorkflowStore(max_completed=1)\n\n    store.events[\"run-old\"] = []\n    store.ticks[\"run-old\"] = []\n    store.create_state_store(\"run-old\")\n\n    await _insert(\n        store,\n        handler_id=\"h-old\",\n        status=\"completed\",\n        run_id=\"run-old\",\n        completed_at=_ts(0),\n    )\n    assert \"run-old\" in store.events\n    assert \"run-old\" in store.ticks\n    assert \"run-old\" in store.state_stores\n\n    await _insert(\n        store,\n        handler_id=\"h-new\",\n        status=\"completed\",\n        run_id=\"run-new\",\n        completed_at=_ts(1),\n    )\n\n    assert \"run-old\" not in store.events\n    assert \"run-old\" not in store.ticks\n    assert \"run-old\" not in store.state_stores\n    remaining = await store.query(HandlerQuery())\n    assert len(remaining) == 1\n    assert remaining[0].handler_id == \"h-new\"\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_eviction_via_update_handler_status() -> None:\n    \"\"\"Eviction triggers when status changes to terminal via update_handler_status.\"\"\"\n    store = MemoryWorkflowStore(max_completed=2)\n\n    for i in range(3):\n        await _insert(store, handler_id=f\"h{i}\", run_id=f\"run-{i}\")\n\n    await store.update_handler_status(\"run-0\", status=\"completed\")\n    await store.update_handler_status(\"run-1\", status=\"completed\")\n    await store.update_handler_status(\"run-2\", status=\"completed\")\n\n    ids = await _query_ids(store)\n    assert len(ids) == 2\n    assert \"h0\" not in ids\n    assert \"h1\" in ids\n    assert \"h2\" in ids\n\n\n@pytest.mark.asyncio\nasync def test_max_completed_ignores_stale_terminal_queue_entries() -> None:\n    store = MemoryWorkflowStore(max_completed=1)\n\n    await _insert(\n        store,\n        handler_id=\"shared\",\n        status=\"completed\",\n        run_id=\"run-old\",\n        completed_at=_ts(0),\n    )\n    await _insert(store, handler_id=\"shared\", status=\"running\", run_id=\"run-active\")\n\n    await _insert(\n        store,\n        handler_id=\"done-2\",\n        status=\"completed\",\n        run_id=\"run-2\",\n        completed_at=_ts(1),\n    )\n\n    handlers = await store.query(HandlerQuery())\n    by_id = {handler.handler_id: handler for handler in handlers}\n    assert set(by_id) == {\"shared\", \"done-2\"}\n    assert by_id[\"shared\"].status == \"running\"\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_migrations.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport sqlite3\nimport sys\nfrom collections.abc import Generator\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.server._store import SQLITE_MIGRATION_SOURCE\nfrom llama_agents.server._store.sqlite.migrate import run_migrations\n\n_FAKE_MIGRATION_SQL = \"\"\"\\\n-- migration: 1\n\nCREATE TABLE IF NOT EXISTS fake_journal (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    run_id TEXT NOT NULL,\n    seq_num INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS fake_lifecycle (\n    run_id TEXT PRIMARY KEY,\n    state TEXT NOT NULL DEFAULT 'active',\n    updated_at TEXT NOT NULL\n);\n\"\"\"\n\n\n@pytest.fixture()\ndef fake_migrations_pkg(tmp_path: Path) -> Generator[str]:\n    \"\"\"Create a temporary importable migrations package and return its dotted name.\"\"\"\n    pkg_dir = tmp_path / \"fake_migrations\"\n    pkg_dir.mkdir()\n    (pkg_dir / \"__init__.py\").write_text(\"\")\n    (pkg_dir / \"0001_init.sql\").write_text(_FAKE_MIGRATION_SQL)\n    sys.path.insert(0, str(tmp_path))\n    yield \"fake_migrations\"\n    sys.path.remove(str(tmp_path))\n\n\ndef _get_tables(conn: sqlite3.Connection) -> set[str]:\n    rows = conn.execute(\"SELECT name FROM sqlite_master WHERE type='table'\").fetchall()\n    return {r[0] for r in rows}\n\n\ndef _get_table_columns(conn: sqlite3.Connection, table: str) -> list[str]:\n    cursor = conn.execute(f\"PRAGMA table_info({table})\")\n    return [row[1] for row in cursor.fetchall()]\n\n\ndef _max_version(conn: sqlite3.Connection, package: str = \"server\") -> int:\n    row = conn.execute(\n        \"SELECT MAX(version) FROM schema_migrations WHERE package = ?\",\n        (package,),\n    ).fetchone()\n    return int(row[0]) if row and row[0] is not None else 0\n\n\ndef test_run_migrations_on_empty_db(tmp_path: Path) -> None:\n    db_path = tmp_path / \"test.sqlite\"\n    conn = sqlite3.connect(str(db_path))\n    try:\n        run_migrations(conn)\n\n        # Should track versions in schema_migrations\n        assert _max_version(conn) >= 2\n\n        # Handlers table should have all extended columns\n        columns = _get_table_columns(conn, \"handlers\")\n        for expected in [\n            \"handler_id\",\n            \"workflow_name\",\n            \"status\",\n            \"ctx\",\n            \"run_id\",\n            \"error\",\n            \"result\",\n            \"started_at\",\n            \"updated_at\",\n            \"completed_at\",\n        ]:\n            assert expected in columns\n\n        # Running again should be idempotent\n        before = _max_version(conn)\n        run_migrations(conn)\n        after = _max_version(conn)\n        assert after == before\n    finally:\n        conn.close()\n\n\ndef test_migrate_from_version_1(tmp_path: Path) -> None:\n    \"\"\"Simulates upgrading an existing deployment with PRAGMA user_version=1.\"\"\"\n    db_path = tmp_path / \"test_v1.sqlite\"\n    conn = sqlite3.connect(str(db_path))\n    try:\n        conn.executescript(\n            \"\"\"\n            PRAGMA user_version=1;\n            CREATE TABLE IF NOT EXISTS handlers (\n                handler_id TEXT PRIMARY KEY,\n                workflow_name TEXT,\n                status TEXT,\n                ctx TEXT\n            );\n            \"\"\"\n        )\n\n        run_migrations(conn)\n\n        # Legacy user_version should have been bootstrapped into schema_migrations\n        assert _max_version(conn) >= 2\n\n        # New columns should be present\n        columns = _get_table_columns(conn, \"handlers\")\n        for expected in [\n            \"run_id\",\n            \"error\",\n            \"result\",\n            \"started_at\",\n            \"updated_at\",\n            \"completed_at\",\n        ]:\n            assert expected in columns\n    finally:\n        conn.close()\n\n\ndef test_per_package_migrations(tmp_path: Path, fake_migrations_pkg: str) -> None:\n    \"\"\"Test running migrations with multiple package sources.\"\"\"\n    db_path = tmp_path / \"test_multi.sqlite\"\n    conn = sqlite3.connect(str(db_path))\n    try:\n        sources = [\n            SQLITE_MIGRATION_SOURCE,\n            (\"fake\", fake_migrations_pkg),\n        ]\n        run_migrations(conn, sources=sources)\n\n        # Server tables should exist\n        tables = _get_tables(conn)\n        assert \"handlers\" in tables\n        assert \"ticks\" in tables\n\n        # Fake package tables should exist\n        assert \"fake_journal\" in tables\n        assert \"fake_lifecycle\" in tables\n\n        # Both packages should have version entries\n        assert _max_version(conn, \"server\") >= 1\n        assert _max_version(conn, \"fake\") >= 1\n\n        # Idempotent\n        run_migrations(conn, sources=sources)\n        assert _max_version(conn, \"server\") >= 1\n    finally:\n        conn.close()\n\n\ndef test_legacy_upgrade_with_extra_package(\n    tmp_path: Path, fake_migrations_pkg: str\n) -> None:\n    \"\"\"Simulate upgrading from released v3 deployment, then adding another package.\"\"\"\n    db_path = tmp_path / \"test_upgrade.sqlite\"\n    conn = sqlite3.connect(str(db_path))\n    try:\n        # Simulate existing v3 deployment\n        conn.executescript(\n            \"\"\"\n            PRAGMA user_version=3;\n            CREATE TABLE IF NOT EXISTS handlers (\n                handler_id TEXT PRIMARY KEY,\n                workflow_name TEXT,\n                status TEXT,\n                ctx TEXT,\n                run_id TEXT,\n                error TEXT,\n                result TEXT,\n                started_at TEXT,\n                updated_at TEXT,\n                completed_at TEXT,\n                idle_since TEXT\n            );\n            \"\"\"\n        )\n\n        sources = [\n            SQLITE_MIGRATION_SOURCE,\n            (\"fake\", fake_migrations_pkg),\n        ]\n        run_migrations(conn, sources=sources)\n\n        # Server should be seeded at 3 and advanced to 4+\n        assert _max_version(conn, \"server\") >= 4\n\n        # Fake package tables should be created\n        tables = _get_tables(conn)\n        assert \"fake_journal\" in tables\n        assert \"fake_lifecycle\" in tables\n    finally:\n        conn.close()\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_openapi_schema.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom llama_agents.server import WorkflowServer\nfrom workflows import Workflow\n\n\ndef test_openapi_schema_includes_all_routes(simple_test_workflow: Workflow) -> None:\n    server = WorkflowServer()\n    server.add_workflow(\"test\", simple_test_workflow)\n    schema = server.openapi_schema()\n\n    assert \"paths\" in schema\n    paths = schema[\"paths\"]\n\n    expected = {\n        \"/workflows\": {\"get\"},\n        \"/workflows/{name}/run\": {\"post\"},\n        \"/workflows/{name}/run-nowait\": {\"post\"},\n        \"/results/{handler_id}\": {\"get\"},\n        \"/handlers/{handler_id}\": {\"get\"},\n        \"/events/{handler_id}\": {\"get\"},\n        \"/handlers\": {\"get\"},\n        \"/handlers/{handler_id}/cancel\": {\"post\"},\n        \"/health\": {\"get\"},\n    }\n\n    # Validate each expected path and method is present\n    for path, methods in expected.items():\n        assert path in paths, f\"Missing path in schema: {path}\"\n        present_methods = {m.lower() for m in paths[path].keys()}\n        for method in methods:\n            assert method in present_methods, (\n                f\"Missing method for {path}: expected {method}, found {present_methods}\"\n            )\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_persistent_handler_serialization.py",
    "content": "# ty: ignore[invalid-argument-type, unknown-argument]\nfrom __future__ import annotations\n\nfrom typing import Any, cast\n\nfrom llama_agents.server import PersistentHandler\nfrom workflows.events import StopEvent\n\n\ndef _base_handler_kwargs() -> dict[str, Any]:\n    return {\n        \"handler_id\": \"h1\",\n        \"workflow_name\": \"wf\",\n        \"status\": cast(\"str\", \"completed\"),\n    }\n\n\ndef test_stop_event_round_trip() -> None:\n    handler = PersistentHandler(**_base_handler_kwargs(), result=StopEvent(result=1))\n\n    dumped = handler.model_dump(mode=\"python\")\n    restored = PersistentHandler(**dumped)\n    assert isinstance(restored.result, StopEvent)\n    assert restored.result.result == 1\n\n\ndef test_legacy_result_dict_is_coerced_to_stop_event() -> None:\n    legacy_payload = {\"foo\": 2}\n    handler = PersistentHandler(\n        **_base_handler_kwargs(), result=cast(Any, legacy_payload)\n    )\n\n    assert isinstance(handler.result, StopEvent)\n    assert handler.result.result == legacy_payload\n\n    dumped = handler.model_dump(mode=\"python\")\n\n    restored = PersistentHandler(**dumped)\n    assert isinstance(restored.result, StopEvent)\n    assert restored.result.result == legacy_payload\n\n\nclass MyStop(StopEvent):\n    pass\n\n\ndef test_stop_event_subclass_round_trip() -> None:\n    payload = {\"y\": 3}\n    handler = PersistentHandler(\n        **_base_handler_kwargs(),\n        result=MyStop(result=payload),  # type: ignore[call-arg]\n    )\n\n    dumped = handler.model_dump(mode=\"python\")\n\n    restored = PersistentHandler(**dumped)\n    assert isinstance(restored.result, MyStop)\n    assert restored.result.result == payload\n\n\ndef test_converts_to_stop_event() -> None:\n    handler = PersistentHandler(**_base_handler_kwargs(), result=123)  # type: ignore[arg-type]\n    assert isinstance(handler.result, StopEvent)\n    assert handler.result.result == 123\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_pool.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import cast\n\nimport asyncpg\nimport pytest\nfrom llama_agents.server._pool import PoolProvider\n\n\nclass FakePool:\n    def __init__(self) -> None:\n        self.close_calls = 0\n        self.terminate_calls = 0\n\n    async def close(self) -> None:\n        self.close_calls += 1\n\n    def terminate(self) -> None:\n        self.terminate_calls += 1\n\n\ndef pool_factory(pool: FakePool) -> asyncpg.Pool:\n    return cast(asyncpg.Pool, pool)\n\n\ndef create_owned_provider(\n    monkeypatch: pytest.MonkeyPatch,\n    pool: FakePool,\n    calls: list[tuple[str, int, int]] | None = None,\n) -> PoolProvider:\n    async def create_pool(\n        dsn: str,\n        *,\n        min_size: int,\n        max_size: int,\n    ) -> asyncpg.Pool:\n        if calls is not None:\n            calls.append((dsn, min_size, max_size))\n        return pool_factory(pool)\n\n    monkeypatch.setattr(asyncpg, \"create_pool\", create_pool)\n    return PoolProvider.create(\"postgresql://example/db\", min_size=2, max_size=7)\n\n\nasync def test_create_uses_asyncpg_create_pool(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    pool = FakePool()\n    calls: list[tuple[str, int, int]] = []\n\n    provider = create_owned_provider(monkeypatch, pool, calls)\n\n    assert await provider.get() is pool\n    assert calls == [(\"postgresql://example/db\", 2, 7)]\n\n\nasync def test_owned_provider_closes_cached_pool_once(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    pool = FakePool()\n    provider = create_owned_provider(monkeypatch, pool)\n\n    assert await provider.get() is pool\n    await provider.close()\n    await provider.close()\n\n    assert pool.close_calls == 1\n    assert pool.terminate_calls == 0\n\n\nasync def test_borrowed_provider_close_and_terminate_do_not_affect_pool() -> None:\n    pool = FakePool()\n    provider = PoolProvider.borrowed(\n        lambda: asyncio.sleep(0, result=pool_factory(pool))\n    )\n\n    assert await provider.get() is pool\n    await provider.close()\n    provider.terminate()\n\n    assert pool.close_calls == 0\n    assert pool.terminate_calls == 0\n    assert await provider.get() is pool\n\n\nasync def test_owned_provider_terminates_cached_pool_once(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    pool = FakePool()\n    provider = create_owned_provider(monkeypatch, pool)\n\n    assert await provider.get() is pool\n    provider.terminate()\n    provider.terminate()\n\n    assert pool.close_calls == 0\n    assert pool.terminate_calls == 1\n\n\nasync def test_close_and_terminate_before_get_do_not_call_factory(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    calls: list[tuple[str, int, int]] = []\n\n    closed = create_owned_provider(monkeypatch, FakePool(), calls)\n    await closed.close()\n\n    terminated = create_owned_provider(monkeypatch, FakePool(), calls)\n    terminated.terminate()\n\n    assert calls == []\n\n\nasync def test_close_after_terminate_is_safe(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    pool = FakePool()\n    provider = create_owned_provider(monkeypatch, pool)\n\n    assert await provider.get() is pool\n    provider.terminate()\n    await provider.close()\n\n    assert pool.close_calls == 0\n    assert pool.terminate_calls == 1\n\n\nasync def test_terminate_after_close_is_safe(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    pool = FakePool()\n    provider = create_owned_provider(monkeypatch, pool)\n\n    assert await provider.get() is pool\n    await provider.close()\n    provider.terminate()\n\n    assert pool.close_calls == 1\n    assert pool.terminate_calls == 0\n\n\nasync def test_get_after_close_raises() -> None:\n    provider = PoolProvider(\n        lambda: asyncio.sleep(0, result=pool_factory(FakePool())),\n        owns_pool=True,\n    )\n\n    await provider.close()\n\n    with pytest.raises(RuntimeError, match=\"closed\"):\n        await provider.get()\n\n\nasync def test_get_after_terminate_raises() -> None:\n    provider = PoolProvider(\n        lambda: asyncio.sleep(0, result=pool_factory(FakePool())),\n        owns_pool=True,\n    )\n\n    provider.terminate()\n\n    with pytest.raises(RuntimeError, match=\"terminated\"):\n        await provider.get()\n\n\nasync def test_factory_exception_is_not_cached() -> None:\n    pool = FakePool()\n    calls = 0\n\n    async def factory() -> asyncpg.Pool:\n        nonlocal calls\n        calls += 1\n        if calls == 1:\n            raise ValueError(\"failed\")\n        return pool_factory(pool)\n\n    provider = PoolProvider.borrowed(factory)\n\n    with pytest.raises(ValueError, match=\"failed\"):\n        await provider.get()\n\n    assert await provider.get() is pool\n    assert calls == 2\n\n\nasync def test_concurrent_get_invokes_factory_once() -> None:\n    pool = FakePool()\n    calls = 0\n    started = asyncio.Event()\n    release = asyncio.Event()\n\n    async def factory() -> asyncpg.Pool:\n        nonlocal calls\n        calls += 1\n        started.set()\n        await release.wait()\n        return pool_factory(pool)\n\n    provider = PoolProvider.borrowed(factory)\n\n    first = asyncio.create_task(provider.get())\n    await started.wait()\n    second = asyncio.create_task(provider.get())\n    release.set()\n\n    assert await asyncio.gather(first, second) == [pool, pool]\n    assert calls == 1\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_postgres_migrations.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\n\nimport asyncpg\nimport pytest\nfrom llama_agents.server._store import POSTGRES_MIGRATION_SOURCE\nfrom llama_agents.server._store.migration_utils import (\n    iter_migration_files,\n    parse_target_version,\n)\nfrom llama_agents.server._store.postgres.migrate import run_migrations\n\n# ── Unit tests (no DB) ──────────────────────────────────────────────\n\n\n_PG_MIGRATIONS_PKG = POSTGRES_MIGRATION_SOURCE[1]\n\n\ndef test_parse_target_version_valid() -> None:\n    assert parse_target_version(\"-- migration: 1\\nCREATE TABLE ...\") == 1\n    assert parse_target_version(\"-- migration: 42\\n\") == 42\n\n\ndef test_parse_target_version_missing() -> None:\n    assert parse_target_version(\"CREATE TABLE ...\") is None\n    assert parse_target_version(\"\") is None\n\n\ndef test_iter_migration_files_returns_sorted_sql() -> None:\n    files = iter_migration_files(_PG_MIGRATIONS_PKG)\n    assert len(files) >= 1\n    assert all(f.name.endswith(\".sql\") for f in files)\n    names = [f.name for f in files]\n    assert names == sorted(names)\n\n\ndef test_first_migration_has_version_1() -> None:\n    files = iter_migration_files(_PG_MIGRATIONS_PKG)\n    sql = files[0].read_text()\n    assert parse_target_version(sql) == 1\n\n\n# ── Integration tests (require Docker) ──────────────────────────────\n\nEXPECTED_VERSION = len(iter_migration_files(_PG_MIGRATIONS_PKG))\n\n\n@pytest.mark.docker\nasync def test_run_migrations_fresh_db(postgres_dsn: str) -> None:\n    conn = await asyncpg.connect(postgres_dsn)\n    schema = \"test_migrate_fresh\"\n    try:\n        await conn.execute(f\"DROP SCHEMA IF EXISTS {schema} CASCADE\")\n        await run_migrations(conn, schema=schema)\n\n        version = await conn.fetchval(\n            f\"SELECT MAX(version) FROM {schema}.schema_migrations WHERE package = 'server'\"\n        )\n        assert version == EXPECTED_VERSION\n\n        tables = await conn.fetch(\n            \"SELECT table_name FROM information_schema.tables WHERE table_schema = $1\",\n            schema,\n        )\n        table_names = {r[\"table_name\"] for r in tables}\n        assert \"wf_handlers\" in table_names\n        assert \"wf_events\" in table_names\n        assert \"wf_ticks\" in table_names\n        assert \"workflow_state\" in table_names\n        assert \"schema_migrations\" in table_names\n    finally:\n        await conn.execute(f\"DROP SCHEMA IF EXISTS {schema} CASCADE\")\n        await conn.close()\n\n\n@pytest.mark.docker\nasync def test_run_migrations_idempotent(postgres_dsn: str) -> None:\n    conn = await asyncpg.connect(postgres_dsn)\n    schema = \"test_migrate_idempotent\"\n    try:\n        await conn.execute(f\"DROP SCHEMA IF EXISTS {schema} CASCADE\")\n        await run_migrations(conn, schema=schema)\n        await run_migrations(conn, schema=schema)\n\n        version = await conn.fetchval(\n            f\"SELECT MAX(version) FROM {schema}.schema_migrations WHERE package = 'server'\"\n        )\n        assert version == EXPECTED_VERSION\n\n        count = await conn.fetchval(\n            f\"SELECT COUNT(*) FROM {schema}.schema_migrations WHERE package = 'server'\"\n        )\n        assert count == EXPECTED_VERSION\n    finally:\n        await conn.execute(f\"DROP SCHEMA IF EXISTS {schema} CASCADE\")\n        await conn.close()\n\n\n@pytest.mark.docker\nasync def test_run_migrations_no_schema(postgres_dsn: str) -> None:\n    conn = await asyncpg.connect(postgres_dsn)\n    try:\n        await conn.execute(\"DROP TABLE IF EXISTS schema_migrations CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS wf_handlers CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS wf_events CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS wf_ticks CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS workflow_state CASCADE\")\n\n        await run_migrations(conn, schema=None)\n\n        version = await conn.fetchval(\n            \"SELECT MAX(version) FROM schema_migrations WHERE package = 'server'\"\n        )\n        assert version == EXPECTED_VERSION\n    finally:\n        await conn.execute(\"DROP TABLE IF EXISTS schema_migrations CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS wf_handlers CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS wf_events CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS wf_ticks CASCADE\")\n        await conn.execute(\"DROP TABLE IF EXISTS workflow_state CASCADE\")\n        await conn.close()\n\n\n@pytest.mark.docker\n@pytest.mark.timeout(30)\nasync def test_concurrent_migrations_with_advisory_lock(\n    postgres_dsn: str,\n) -> None:\n    \"\"\"Run migrations from N concurrent connections — only one should win the\n    race; the rest should observe the lock and skip gracefully.\"\"\"\n    schema = \"test_concurrent_mig\"\n    concurrency = 8\n\n    conn = await asyncpg.connect(postgres_dsn)\n    try:\n        await conn.execute(f\"DROP SCHEMA IF EXISTS {schema} CASCADE\")\n    finally:\n        await conn.close()\n\n    async def migrate_once() -> None:\n        c = await asyncpg.connect(postgres_dsn)\n        try:\n            await run_migrations(c, schema=schema)\n        finally:\n            await c.close()\n\n    results = await asyncio.gather(\n        *[migrate_once() for _ in range(concurrency)],\n        return_exceptions=True,\n    )\n\n    for i, result in enumerate(results):\n        assert not isinstance(result, Exception), f\"Migration task {i} failed: {result}\"\n\n    conn = await asyncpg.connect(postgres_dsn)\n    try:\n        count = await conn.fetchval(\n            f\"SELECT COUNT(*) FROM {schema}.schema_migrations WHERE package = 'server'\"\n        )\n        assert count == EXPECTED_VERSION, (\n            f\"Expected {EXPECTED_VERSION} migration rows, got {count}\"\n        )\n\n        tables = await conn.fetch(\n            \"SELECT table_name FROM information_schema.tables WHERE table_schema = $1\",\n            schema,\n        )\n        table_names = {r[\"table_name\"] for r in tables}\n        assert \"wf_handlers\" in table_names\n        assert \"wf_events\" in table_names\n    finally:\n        await conn.execute(f\"DROP SCHEMA IF EXISTS {schema} CASCADE\")\n        await conn.close()\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_postgres_state_store.py",
    "content": "# ty: ignore[invalid-argument-type]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom typing import AsyncGenerator\n\nimport asyncpg\nimport pytest\nfrom llama_agents.server._store.postgres_state_store import (\n    PostgresStateStore,\n)\nfrom pydantic import BaseModel\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import DictState, InMemoryStateStore\n\nSCHEMA = \"test_pg_state\"\n\n\nclass CounterState(BaseModel):\n    count: int = 0\n    label: str = \"default\"\n\n\nclass ExtendedCounterState(CounterState):\n    extra: str = \"extra_default\"\n\n\n@pytest.fixture\nasync def pool(postgres_dsn: str) -> AsyncGenerator[asyncpg.Pool, None]:\n    p = await asyncpg.create_pool(postgres_dsn, min_size=1, max_size=5)\n    async with p.acquire() as conn:\n        await conn.execute(f\"CREATE SCHEMA IF NOT EXISTS {SCHEMA}\")\n        await conn.execute(f\"\"\"\n            CREATE TABLE IF NOT EXISTS {SCHEMA}.workflow_state (\n                run_id VARCHAR(255) PRIMARY KEY,\n                state_json TEXT NOT NULL,\n                state_type VARCHAR(255),\n                state_module VARCHAR(255),\n                created_at TIMESTAMPTZ,\n                updated_at TIMESTAMPTZ\n            )\n        \"\"\")\n        await conn.execute(f\"DELETE FROM {SCHEMA}.workflow_state\")\n    yield p\n    await p.close()\n\n\n@pytest.mark.docker\nasync def test_get_returns_default_dict_state(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-1\", schema=SCHEMA\n    )\n    state = await store.get_state()\n    assert isinstance(state, DictState)\n    assert dict(state) == {}\n\n\n@pytest.mark.docker\nasync def test_set_and_get_path(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-path\", schema=SCHEMA\n    )\n    await store.set(\"foo\", 42)\n    value = await store.get(\"foo\")\n    assert value == 42\n\n\n@pytest.mark.docker\nasync def test_set_nested_path(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-nested\", schema=SCHEMA\n    )\n    await store.set(\"a.b.c\", \"deep\")\n    value = await store.get(\"a.b.c\")\n    assert value == \"deep\"\n\n\n@pytest.mark.docker\nasync def test_get_missing_path_raises(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-missing\", schema=SCHEMA\n    )\n    with pytest.raises(ValueError, match=\"not found\"):\n        await store.get(\"nonexistent\")\n\n\n@pytest.mark.docker\nasync def test_get_missing_path_returns_default(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-default\", schema=SCHEMA\n    )\n    value = await store.get(\"nonexistent\", default=\"fallback\")\n    assert value == \"fallback\"\n\n\n@pytest.mark.docker\nasync def test_set_state_replaces_dict_state(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-replace\", schema=SCHEMA\n    )\n    await store.set(\"x\", 1)\n    new_state = DictState(y=2)\n    await store.set_state(new_state)\n    state = await store.get_state()\n    assert \"y\" in state\n    assert \"x\" not in state\n\n\n@pytest.mark.docker\nasync def test_typed_state_get_returns_default(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[CounterState] = PostgresStateStore(\n        pool=pool, run_id=\"run-typed\", state_type=CounterState, schema=SCHEMA\n    )\n    state = await store.get_state()\n    assert isinstance(state, CounterState)\n    assert state.count == 0\n    assert state.label == \"default\"\n\n\n@pytest.mark.docker\nasync def test_typed_state_set_and_get(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[CounterState] = PostgresStateStore(\n        pool=pool, run_id=\"run-typed-set\", state_type=CounterState, schema=SCHEMA\n    )\n    await store.set_state(CounterState(count=5, label=\"updated\"))\n    state = await store.get_state()\n    assert state.count == 5\n    assert state.label == \"updated\"\n\n\n@pytest.mark.docker\nasync def test_set_state_parent_type_merge(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[ExtendedCounterState] = PostgresStateStore(\n        pool=pool, run_id=\"run-merge\", state_type=ExtendedCounterState, schema=SCHEMA\n    )\n    await store.set_state(ExtendedCounterState(count=1, label=\"init\", extra=\"mine\"))\n    parent = CounterState(count=10, label=\"merged\")\n    await store.set_state(parent)  # type: ignore[arg-type]\n    state = await store.get_state()\n    assert state.count == 10\n    assert state.label == \"merged\"\n    assert state.extra == \"mine\"\n\n\n@pytest.mark.docker\nasync def test_edit_state_dict(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-edit\", schema=SCHEMA\n    )\n    await store.set(\"counter\", 0)\n    async with store.edit_state() as state:\n        state[\"counter\"] = state[\"counter\"] + 1\n    value = await store.get(\"counter\")\n    assert value == 1\n\n\n@pytest.mark.docker\nasync def test_edit_state_typed(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[CounterState] = PostgresStateStore(\n        pool=pool, run_id=\"run-edit-typed\", state_type=CounterState, schema=SCHEMA\n    )\n    async with store.edit_state() as state:\n        state.count += 10\n    result = await store.get_state()\n    assert result.count == 10\n\n\n@pytest.mark.docker\nasync def test_clear_resets_state(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-clear\", schema=SCHEMA\n    )\n    await store.set(\"x\", 99)\n    await store.clear()\n    state = await store.get_state()\n    assert dict(state) == {}\n\n\n@pytest.mark.docker\nasync def test_clear_resets_typed_state(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[CounterState] = PostgresStateStore(\n        pool=pool, run_id=\"run-clear-typed\", state_type=CounterState, schema=SCHEMA\n    )\n    await store.set_state(CounterState(count=100, label=\"dirty\"))\n    await store.clear()\n    state = await store.get_state()\n    assert state.count == 0\n    assert state.label == \"default\"\n\n\n@pytest.mark.docker\nasync def test_different_run_ids_are_isolated(pool: asyncpg.Pool) -> None:\n    store_a: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-a\", schema=SCHEMA\n    )\n    store_b: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-b\", schema=SCHEMA\n    )\n    await store_a.set(\"x\", \"from-a\")\n    await store_b.set(\"x\", \"from-b\")\n    assert await store_a.get(\"x\") == \"from-a\"\n    assert await store_b.get(\"x\") == \"from-b\"\n\n\n@pytest.mark.docker\nasync def test_to_dict_returns_metadata_only(pool: asyncpg.Pool) -> None:\n    store: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-todict\", schema=SCHEMA\n    )\n    await store.set(\"key\", \"value\")\n    serializer = JsonSerializer()\n    d = store.to_dict(serializer)\n    assert d[\"store_type\"] == \"postgres\"\n    assert d[\"run_id\"] == \"run-todict\"\n    assert \"state_data\" not in d\n\n\n@pytest.mark.docker\nasync def test_from_dict_postgres_format(pool: asyncpg.Pool) -> None:\n    store1: PostgresStateStore[DictState] = PostgresStateStore(\n        pool=pool, run_id=\"run-fromdict\", schema=SCHEMA\n    )\n    await store1.set(\"saved\", True)\n\n    serializer = JsonSerializer()\n    payload = store1.to_dict(serializer)\n\n    store2 = PostgresStateStore.from_dict(\n        payload, serializer, pool=pool, state_type=DictState, schema=SCHEMA\n    )\n    value = await store2.get(\"saved\")\n    assert value is True\n\n\n@pytest.mark.docker\nasync def test_from_dict_in_memory_format_migrates(pool: asyncpg.Pool) -> None:\n    serializer = JsonSerializer()\n    in_memory_store = InMemoryStateStore(DictState(migrated_key=\"migrated_value\"))\n    payload = in_memory_store.to_dict(serializer)\n\n    store = PostgresStateStore.from_dict(\n        payload,\n        serializer,\n        pool=pool,\n        state_type=DictState,\n        run_id=\"run-migrate\",\n        schema=SCHEMA,\n    )\n    await store._write_in_memory_state(payload)\n    value = await store.get(\"migrated_key\")\n    assert value == \"migrated_value\"\n\n\nasync def test_from_dict_empty_raises() -> None:\n    with pytest.raises(ValueError, match=\"Cannot restore\"):\n        PostgresStateStore.from_dict({}, JsonSerializer())\n\n\nasync def test_from_dict_no_pool_raises() -> None:\n    with pytest.raises(ValueError, match=\"pool is required\"):\n        PostgresStateStore.from_dict(\n            {\"store_type\": \"postgres\", \"run_id\": \"x\"}, JsonSerializer()\n        )\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_postgres_workflow_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime, timezone\nfrom typing import Any, cast\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom llama_agents.server._pool import PoolProvider\nfrom llama_agents.server._store.abstract_workflow_store import (\n    HandlerQuery,\n    PersistentHandler,\n    Status,\n)\nfrom llama_agents.server._store.postgres_workflow_store import PostgresWorkflowStore\nfrom server_test_fixtures import wait_for_passing  # type: ignore[import]\nfrom workflows.events import Event, StopEvent\n\n\ndef _make_event() -> EventEnvelopeWithMetadata:\n    class TestEvent(Event):\n        key: str = \"value\"\n\n    return EventEnvelopeWithMetadata.from_event(TestEvent())\n\n\ndef _make_stop_event() -> EventEnvelopeWithMetadata:\n    return EventEnvelopeWithMetadata.from_event(StopEvent(result=\"done\"))\n\n\ndef _make_handler(\n    handler_id: str = \"h1\",\n    workflow_name: str = \"test_workflow\",\n    status: Status = \"running\",\n    run_id: str = \"run-1\",\n    started_at: datetime | None = None,\n    updated_at: datetime | None = None,\n    completed_at: datetime | None = None,\n    idle_since: datetime | None = None,\n    error: str | None = None,\n) -> PersistentHandler:\n    now = datetime.now(timezone.utc)\n    return PersistentHandler(\n        handler_id=handler_id,\n        workflow_name=workflow_name,\n        status=status,\n        run_id=run_id,\n        started_at=started_at or now,\n        updated_at=updated_at or now,\n        completed_at=completed_at,\n        idle_since=idle_since,\n        error=error,\n    )\n\n\n# ── Unit tests (no Postgres needed, test logic with mocks) ──────────\n\n\nasync def test_create_state_store_without_pool_raises() -> None:\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n    with pytest.raises(RuntimeError, match=\"pool not initialized\"):\n        store.create_state_store(\"run-1\")\n\n\nasync def test_build_filters_empty_in_returns_none() -> None:\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n    assert store._build_filters(HandlerQuery(handler_id_in=[])) is None\n    assert store._build_filters(HandlerQuery(run_id_in=[])) is None\n    assert store._build_filters(HandlerQuery(status_in=[])) is None\n    assert store._build_filters(HandlerQuery(workflow_name_in=[])) is None\n\n\nasync def test_build_filters_produces_correct_clauses() -> None:\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n\n    result = store._build_filters(HandlerQuery(handler_id_in=[\"h1\", \"h2\"]))\n    assert result is not None\n    clauses, params = result\n    assert len(clauses) == 1\n    assert \"handler_id IN\" in clauses[0]\n    assert params == [\"h1\", \"h2\"]\n\n    result = store._build_filters(HandlerQuery(is_idle=True))\n    assert result is not None\n    clauses, params = result\n    assert \"idle_since IS NOT NULL\" in clauses[0]\n    assert params == []\n\n    result = store._build_filters(HandlerQuery(is_idle=False))\n    assert result is not None\n    clauses, params = result\n    assert \"idle_since IS NULL\" in clauses[0]\n\n\nasync def test_on_notify_wakes_condition() -> None:\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n    condition = store._get_or_create_condition(\"run-1\")\n\n    woken = False\n    waiting = asyncio.Event()\n\n    async def waiter() -> None:\n        nonlocal woken\n        async with condition:\n            waiting.set()\n            await condition.wait()\n            woken = True\n\n    task = asyncio.create_task(waiter())\n    await waiting.wait()\n    async with condition:\n        pass\n\n    # Simulate the NOTIFY callback\n    store._on_notify(MagicMock(), 0, \"wf_events\", \"run-1\")\n\n    await asyncio.wait_for(task, timeout=1.0)\n    assert woken\n\n\nasync def test_close_without_start_is_safe() -> None:\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n    await store.close()  # Should not raise\n\n\nasync def test_borrowed_pool_not_closed_on_close(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"When constructed with a borrowed provider, the borrowed pool is never closed.\"\"\"\n    fake_pool = MagicMock()\n    fake_pool.close = MagicMock()  # would be awaited if called\n    factory_calls = 0\n\n    async def factory() -> Any:\n        nonlocal factory_calls\n        factory_calls += 1\n        return fake_pool\n\n    # _setup_listener acquires from the pool — short-circuit it for this unit test.\n    async def noop_setup_listener(self: PostgresWorkflowStore) -> None:\n        return None\n\n    monkeypatch.setattr(PostgresWorkflowStore, \"_setup_listener\", noop_setup_listener)\n\n    store = PostgresWorkflowStore(\n        dsn=\"postgresql://localhost/test\",\n        pool=PoolProvider.borrowed(factory),\n        auto_migrate=False,\n    )\n\n    await store.start()\n    assert factory_calls == 1\n    assert store._pool is fake_pool\n\n    await store.close()\n    fake_pool.close.assert_not_called()\n    assert store._pool is None\n\n\nasync def test_listen_termination_callback_schedules_reconnect(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"When the LISTEN conn drops, _on_listen_termination must schedule reconnect.\"\"\"\n    reconnect_called = asyncio.Event()\n\n    async def fake_reconnect(self: PostgresWorkflowStore) -> None:\n        reconnect_called.set()\n\n    monkeypatch.setattr(PostgresWorkflowStore, \"_reconnect_listener\", fake_reconnect)\n\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n    store._on_listen_termination(MagicMock())\n\n    await asyncio.wait_for(reconnect_called.wait(), timeout=1.0)\n\n\nasync def test_listen_termination_callback_noops_when_closing(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"The closing flag suppresses reconnect attempts during teardown.\"\"\"\n    called = False\n\n    async def fake_reconnect(self: PostgresWorkflowStore) -> None:\n        nonlocal called\n        called = True\n\n    monkeypatch.setattr(PostgresWorkflowStore, \"_reconnect_listener\", fake_reconnect)\n\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n    store._closing = True\n    store._on_listen_termination(MagicMock())\n    await asyncio.sleep(0.01)\n    assert called is False\n\n\nasync def test_reconnect_listener_wakes_subscribers(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"After a successful reconnect, all active subscribe_events conditions are notified.\"\"\"\n\n    async def fake_setup_listener(self: PostgresWorkflowStore) -> None:\n        return None\n\n    monkeypatch.setattr(PostgresWorkflowStore, \"_setup_listener\", fake_setup_listener)\n\n    store = PostgresWorkflowStore(dsn=\"postgresql://localhost/test\")\n    # Pretend the pool exists so _reconnect_listener proceeds.\n    store._pool = cast(Any, MagicMock())\n\n    # Hold strong references to the conditions; the store keeps weak refs only.\n    cond_a = store._get_or_create_condition(\"run-a\")\n    cond_b = store._get_or_create_condition(\"run-b\")\n\n    woken = {\"a\": False, \"b\": False}\n\n    async def waiter(name: str, cond: asyncio.Condition) -> None:\n        async with cond:\n            await cond.wait()\n            woken[name] = True\n\n    task_a = asyncio.create_task(waiter(\"a\", cond_a))\n    task_b = asyncio.create_task(waiter(\"b\", cond_b))\n    await asyncio.sleep(0.01)\n\n    await store._reconnect_listener()\n\n    await asyncio.wait_for(asyncio.gather(task_a, task_b), timeout=1.0)\n    assert woken == {\"a\": True, \"b\": True}\n\n\n# ── Integration tests (require Docker) ──────────────────────────────\n\n\n@pytest.mark.docker\nasync def test_integration_migrations_idempotent(postgres_dsn: str) -> None:\n    store = PostgresWorkflowStore(dsn=postgres_dsn, schema=\"test_pg_store\")\n    try:\n        await store.start()\n        await store.run_migrations()\n        await store.run_migrations()  # Should be idempotent\n    finally:\n        await store.close()\n\n\n@pytest.mark.docker\nasync def test_integration_handler_crud(postgres_dsn: str) -> None:\n    store = PostgresWorkflowStore(dsn=postgres_dsn, schema=\"test_pg_store\")\n    try:\n        await store.start()\n        await store.run_migrations()\n\n        handler = _make_handler(handler_id=\"pg-h1\", run_id=\"pg-run-1\")\n        await store.update(handler)\n\n        results = await store.query(HandlerQuery(handler_id_in=[\"pg-h1\"]))\n        assert len(results) == 1\n        assert results[0].handler_id == \"pg-h1\"\n\n        count = await store.delete(HandlerQuery(handler_id_in=[\"pg-h1\"]))\n        assert count == 1\n\n        results = await store.query(HandlerQuery(handler_id_in=[\"pg-h1\"]))\n        assert len(results) == 0\n    finally:\n        await store.close()\n\n\n@pytest.mark.docker\nasync def test_integration_event_append_and_query(postgres_dsn: str) -> None:\n    store = PostgresWorkflowStore(dsn=postgres_dsn, schema=\"test_pg_store\")\n    try:\n        await store.start()\n        await store.run_migrations()\n\n        await store.append_event(\"pg-run-ev\", _make_event())\n        await store.append_event(\"pg-run-ev\", _make_event())\n        await store.append_event(\"pg-run-ev\", _make_event())\n\n        events = await store.query_events(\"pg-run-ev\")\n        assert len(events) == 3\n        assert events[0].sequence == 0\n        assert events[1].sequence == 1\n        assert events[2].sequence == 2\n\n        events = await store.query_events(\"pg-run-ev\", after_sequence=0, limit=1)\n        assert len(events) == 1\n        assert events[0].sequence == 1\n    finally:\n        await store.close()\n\n\n@pytest.mark.docker\nasync def test_integration_subscribe_events(postgres_dsn: str) -> None:\n    store = PostgresWorkflowStore(\n        dsn=postgres_dsn, schema=\"test_pg_store\", poll_interval=0.05\n    )\n    try:\n        await store.start()\n        await store.run_migrations()\n\n        run_id = \"pg-run-sub\"\n\n        async def subscribe() -> list[object]:\n            collected = []\n            async for event in store.subscribe_events(run_id):\n                collected.append(event)\n            return collected\n\n        subscribe_task = asyncio.create_task(subscribe())\n\n        async def subscribed() -> None:\n            assert run_id in store._conditions\n\n        await wait_for_passing(subscribed, max_duration=2.0, interval=0.01)\n\n        await store.append_event(run_id, _make_event())\n        await store.append_event(run_id, _make_event())\n        await store.append_event(run_id, _make_stop_event())\n\n        collected = await asyncio.wait_for(subscribe_task, timeout=5.0)\n\n        assert len(collected) == 3\n    finally:\n        await store.close()\n\n\n@pytest.mark.docker\nasync def test_integration_tick_append_and_get(postgres_dsn: str) -> None:\n    store = PostgresWorkflowStore(dsn=postgres_dsn, schema=\"test_pg_store\")\n    try:\n        await store.start()\n        await store.run_migrations()\n\n        run_id = \"pg-run-ticks\"\n        await store.append_tick(run_id, {\"type\": \"TickSendEvent\", \"event\": \"a\"})\n        await store.append_tick(run_id, {\"type\": \"TickSendEvent\", \"event\": \"b\"})\n        await store.append_tick(run_id, {\"type\": \"TickSendEvent\", \"event\": \"c\"})\n\n        ticks = await store.get_ticks(run_id)\n        assert len(ticks) == 3\n        assert ticks[0].sequence == 0\n        assert ticks[1].sequence == 1\n        assert ticks[2].sequence == 2\n        assert ticks[0].tick_data[\"event\"] == \"a\"\n        assert ticks[2].tick_data[\"event\"] == \"c\"\n\n        # Different run_id should be empty\n        assert await store.get_ticks(\"other-run\") == []\n    finally:\n        await store.close()\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_runtime_decorators.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for the base runtime decorator forwarding classes.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any, AsyncGenerator\nfrom unittest.mock import MagicMock\n\nfrom workflows.context.state_store import StateStore\nfrom workflows.events import (\n    Event,\n    StopEvent,\n)\nfrom workflows.runtime.runtime_decorators import (\n    BaseExternalRunAdapterDecorator,\n    BaseInternalRunAdapterDecorator,\n    BaseRuntimeDecorator,\n)\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    WaitResult,\n    WaitResultTimeout,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\n\n# -- Stubs -----------------------------------------------------------------\n\n\nclass StubInternalAdapter(InternalRunAdapter):\n    def __init__(self) -> None:\n        self.closed = False\n\n    @property\n    def run_id(self) -> str:\n        return \"r1\"\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        pass\n\n    async def get_now(self) -> float:\n        return 1.0\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        pass\n\n    async def wait_receive(self, timeout_seconds: float | None = None) -> WaitResult:\n        return WaitResultTimeout()\n\n    async def close(self) -> None:\n        self.closed = True\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return None\n\n\nclass StubExternalAdapter(ExternalRunAdapter):\n    def __init__(self) -> None:\n        self.closed = False\n\n    @property\n    def run_id(self) -> str:\n        return \"r1\"\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        pass\n\n    async def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        yield StopEvent(result=\"done\")\n\n    async def close(self) -> None:\n        self.closed = True\n\n    async def get_result(self) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return None\n\n\nclass StubRuntime(Runtime):\n    def __init__(self) -> None:\n        super().__init__()\n        self.launched = False\n\n    def register(self, workflow: Any) -> RegisteredWorkflow:\n        return RegisteredWorkflow(\n            workflow=workflow, workflow_run_fn=MagicMock(), steps={}\n        )\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Any,\n        init_state: Any,\n        start_event: Any = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: Any = None,\n    ) -> ExternalRunAdapter:\n        return StubExternalAdapter()\n\n    def get_internal_adapter(self, workflow: Any) -> InternalRunAdapter:\n        return StubInternalAdapter()\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        return StubExternalAdapter()\n\n    async def launch(self) -> None:\n        self.launched = True\n\n    async def destroy(self) -> None:\n        pass\n\n\n# -- Tests -----------------------------------------------------------------\n\n\ndef test_runtime_decorator_forwards() -> None:\n    inner = StubRuntime()\n    dec = BaseRuntimeDecorator(inner)\n    dec.launch_sync()\n    assert inner.launched\n\n\nasync def test_internal_adapter_decorator_forwards() -> None:\n    inner = StubInternalAdapter()\n    dec = BaseInternalRunAdapterDecorator(inner)\n    assert dec.run_id == \"r1\"\n    assert await dec.get_now() == 1.0\n    await dec.close()\n    assert inner.closed\n\n\nasync def test_external_adapter_decorator_forwards() -> None:\n    inner = StubExternalAdapter()\n    dec = BaseExternalRunAdapterDecorator(inner)\n    assert dec.run_id == \"r1\"\n    result = await dec.get_result()\n    assert result.result == \"done\"\n    await dec.close()\n    assert inner.closed\n\n\nasync def test_subclass_can_override_selectively() -> None:\n    \"\"\"Override one method; the rest still forward.\"\"\"\n\n    class Custom(BaseInternalRunAdapterDecorator):\n        async def get_now(self) -> float:\n            return 42.0\n\n    inner = StubInternalAdapter()\n    dec = Custom(inner)\n    assert await dec.get_now() == 42.0\n    assert dec.run_id == \"r1\"  # still forwarded\n\n\ndef test_runtime_decorator_forwards_untrack() -> None:\n    from workflows import Workflow, step\n    from workflows.events import StartEvent\n\n    class SimpleWorkflow(Workflow):\n        @step\n        async def start(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    inner = StubRuntime()\n    dec = BaseRuntimeDecorator(inner)\n    wf = SimpleWorkflow(runtime=dec)\n    assert wf in dec._pending\n    dec.untrack_workflow(wf)\n    assert wf not in dec._pending\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_server.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom typing import Any\nfrom unittest.mock import AsyncMock, Mock, patch\n\nimport pytest\nfrom llama_agents.server import WorkflowServer\nfrom starlette.middleware import Middleware\nfrom workflows.workflow import Workflow\n\n\ndef test_init_custom_middleware() -> None:\n    custom_middleware = [Mock(spec=Middleware)]\n    server = WorkflowServer(middleware=custom_middleware)  # type: ignore\n    assert server.app.user_middleware == custom_middleware\n\n\ndef test_add_workflow(simple_test_workflow: Workflow) -> None:\n    server = WorkflowServer()\n    server.add_workflow(\"test\", simple_test_workflow)\n    assert \"test\" in server.get_workflows()\n    assert server.get_workflows()[\"test\"] == simple_test_workflow\n\n\n@pytest.mark.asyncio\n@patch(\"llama_agents.server.server.uvicorn.Server\")\n@patch(\"llama_agents.server.server.uvicorn.Config\")\nasync def test_serve(mock_config: Any, mock_server: Any) -> None:\n    server = WorkflowServer()\n    mock_server_instance = AsyncMock()\n    mock_server.return_value = mock_server_instance\n\n    await server.serve(host=\"localhost\", port=8000)\n\n    mock_config.assert_called_once_with(server.app, host=\"localhost\", port=8000)\n    mock_server_instance.serve.assert_called_once()\n\n\n@pytest.mark.asyncio\n@patch(\"llama_agents.server.server.uvicorn.Server\")\n@patch(\"llama_agents.server.server.uvicorn.Config\")\nasync def test_serve_with_uvicorn_config(mock_config: Any, mock_server: Any) -> None:\n    server = WorkflowServer()\n    mock_server_instance = AsyncMock()\n    mock_server.return_value = mock_server_instance\n\n    uvicorn_config = {\"log_level\": \"debug\", \"reload\": True}\n    await server.serve(host=\"localhost\", port=8000, uvicorn_config=uvicorn_config)\n\n    mock_config.assert_called_once_with(\n        server.app, host=\"localhost\", port=8000, log_level=\"debug\", reload=True\n    )\n\n\ndef test_extract_workflow_success(simple_test_workflow: Workflow) -> None:\n    server = WorkflowServer()\n    server.add_workflow(\"test\", simple_test_workflow)\n\n    # Mocked request with path_params\n    mock_request = Mock()\n    mock_request.path_params = {\"name\": \"test\"}\n\n    assert server._api._extract_workflow(mock_request) is simple_test_workflow\n\n\ndef test_extract_workflow_missing_name() -> None:\n    server = WorkflowServer()\n    mock_request = Mock()\n    mock_request.path_params = {}\n\n    with pytest.raises(Exception) as exc_info:\n        server._api._extract_workflow(mock_request)\n    assert exc_info.value.status_code == 400  # type: ignore\n    assert \"name\" in exc_info.value.detail  # type: ignore\n\n\ndef test_extract_workflow_not_found() -> None:\n    server = WorkflowServer()\n    mock_request = Mock()\n    mock_request.path_params = {\"name\": \"nonexistent\"}\n\n    with pytest.raises(Exception) as exc_info:\n        server._api._extract_workflow(mock_request)\n    assert exc_info.value.status_code == 404  # type: ignore\n    assert \"not found\" in exc_info.value.detail  # type: ignore\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_server_endpoints.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nfrom collections import Counter\nfrom contextlib import asynccontextmanager\nfrom datetime import datetime\nfrom typing import Any, AsyncGenerator, AsyncIterator\n\nimport pytest\nimport pytest_asyncio\nfrom httpx import ASGITransport, AsyncClient, Response\nfrom llama_agents.server import (\n    HandlerQuery,\n    MemoryWorkflowStore,\n    PersistentHandler,\n    WorkflowServer,\n)\nfrom llama_index_instrumentation.dispatcher import active_instrument_tags\nfrom server_test_fixtures import (\n    ExternalEvent,  # type: ignore[import]\n    wait_for_passing,  # type: ignore[import]\n    wait_for_requested_external_event,  # type: ignore[import]\n)\nfrom workflows import Context, step\n\n# Prepare the event to send\nfrom workflows.context.context_types import SerializedContext\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import DictState, InMemoryStateStore\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\nclass CustomStopEvent(StopEvent):\n    message: str\n\n\nclass CustomStopWorkflow(Workflow):\n    @step\n    async def finish(self, ev: StartEvent) -> CustomStopEvent:\n        return CustomStopEvent(message=\"custom-completed\")\n\n\nasync def serialize_context(state_dict: dict[str, Any]) -> SerializedContext:\n    ser_context = SerializedContext()\n    state = InMemoryStateStore(DictState())\n    for key, value in state_dict.items():\n        await state.set(key, value)\n    ser_context.state = state.to_dict(JsonSerializer())\n    return ser_context\n\n\n@pytest.fixture\ndef server(\n    simple_test_workflow: Workflow,\n    error_workflow: Workflow,\n    streaming_workflow: Workflow,\n    interactive_workflow: Workflow,\n) -> WorkflowServer:\n    # Use MemoryWorkflowStore so get_handlers() can retrieve from persistence\n    server = WorkflowServer(workflow_store=MemoryWorkflowStore(), idle_timeout=0.01)\n    server.add_workflow(\"test\", simple_test_workflow)\n    server.add_workflow(\"error\", error_workflow)\n    server.add_workflow(\"streaming\", streaming_workflow)\n    server.add_workflow(\"interactive\", interactive_workflow)\n    return server\n\n\n@pytest.fixture\ndef context_server(\n    simple_test_workflow: Workflow,\n) -> WorkflowServer:\n    server = WorkflowServer(\n        workflow_store=MemoryWorkflowStore(),\n        idle_timeout=0.01,\n        accept_context_api=True,\n    )\n    server.add_workflow(\"test\", simple_test_workflow)\n    return server\n\n\n@pytest_asyncio.fixture\nasync def client(\n    server: WorkflowServer,\n) -> AsyncGenerator:\n    async with server.contextmanager():\n        transport = ASGITransport(app=server.app)\n\n        async with AsyncClient(transport=transport, base_url=\"http://test\") as client:\n            yield client\n\n\n@pytest_asyncio.fixture\nasync def context_client(\n    context_server: WorkflowServer,\n) -> AsyncGenerator:\n    async with context_server.contextmanager():\n        transport = ASGITransport(app=context_server.app)\n\n        async with AsyncClient(transport=transport, base_url=\"http://test\") as client:\n            yield client\n\n\n@asynccontextmanager\nasync def server_with_persisted_handlers(\n    interactive_workflow: Workflow,\n    *,\n    persisted_handlers: list[PersistentHandler] | None = None,\n) -> AsyncIterator[tuple[WorkflowServer, AsyncClient, MemoryWorkflowStore]]:\n    store = MemoryWorkflowStore()\n    if persisted_handlers is not None:\n        for handler in persisted_handlers:\n            await store.update(handler)\n\n    server_with_store = WorkflowServer(workflow_store=store, idle_timeout=0.01)\n    server_with_store.add_workflow(\"interactive\", interactive_workflow)\n\n    async with server_with_store.contextmanager():\n        transport = ASGITransport(app=server_with_store.app)\n        async with AsyncClient(transport=transport, base_url=\"http://test\") as client:\n            yield server_with_store, client, store\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_start_event_str_plain(\n    client: AsyncClient,\n) -> None:\n    # Provide start_event as a plain JSON string (no discriminators)\n    start_event_json = json.dumps({\"message\": \"plain string start\"})\n    response = await client.post(\n        \"/workflows/test/run\", json={\"start_event\": start_event_json}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: plain string start\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_start_event_dict_with_discriminators(\n    client: AsyncClient,\n) -> None:\n    # Provide start_event as a dict with pydantic discriminators\n    start_event_dict = {\n        \"__is_pydantic\": True,\n        \"value\": {\"_data\": {\"message\": \"dict with discriminators\"}},\n        \"qualified_name\": \"workflows.events.StartEvent\",\n    }\n    response = await client.post(\n        \"/workflows/test/run\", json={\"start_event\": start_event_dict}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: dict with discriminators\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_start_event_dict_plain(\n    client: AsyncClient,\n) -> None:\n    # Provide start_event as a plain dict (no discriminators)\n    start_event_dict = {\"message\": \"plain dict start\"}\n    response = await client.post(\n        \"/workflows/test/run\", json={\"start_event\": start_event_dict}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: plain dict start\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_nonconforming_start_event_type(\n    client: AsyncClient,\n) -> None:\n    # Provide start_event of a different event type than the workflow's StartEvent\n    wrong_event_dict = {\n        \"__is_pydantic\": True,\n        \"value\": {\"_data\": {\"message\": \"should fail\"}},\n        \"qualified_name\": \"workflows.events.StopEvent\",\n    }\n    response = await client.post(\n        \"/workflows/test/run\", json={\"start_event\": wrong_event_dict}\n    )\n    assert response.status_code == 400\n    assert \"Start event must be an instance of\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_health_check(client: AsyncClient) -> None:\n    response = await client.get(\"/health\")\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"status\"] == \"healthy\"\n\n\n@pytest.mark.asyncio\nasync def test_health_check_returns_503_when_not_launched(\n    server: WorkflowServer,\n) -> None:\n    \"\"\"Health endpoint returns 503 when runtime is not launched.\"\"\"\n    from unittest.mock import PropertyMock, patch\n\n    with patch.object(\n        type(server._service._runtime),\n        \"is_launched\",\n        new_callable=PropertyMock,\n        return_value=False,\n    ):\n        transport = ASGITransport(app=server.app)\n        async with AsyncClient(transport=transport, base_url=\"http://test\") as client:\n            response = await client.get(\"/health\")\n    assert response.status_code == 503\n    assert response.json() == {\"status\": \"unhealthy\"}\n\n\n@pytest.mark.asyncio\nasync def test_list_workflows(client: AsyncClient) -> None:\n    response = await client.get(\"/workflows\")\n    assert response.status_code == 200\n    data = response.json()\n    assert \"workflows\" in data\n    assert set(data[\"workflows\"]) == {\"test\", \"error\", \"streaming\", \"interactive\"}\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_success(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/workflows/test/run\", json={\"kwargs\": {\"message\": \"hello\"}}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert \"result\" in data\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: hello\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_no_kwargs(client: AsyncClient) -> None:\n    response = await client.post(\"/workflows/test/run\", json={})\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: default\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_context(context_client: AsyncClient) -> None:\n    ctx_dict = (\n        await serialize_context({\"test_param\": \"message from context\"})\n    ).model_dump(mode=\"python\")\n    response = await context_client.post(\n        \"/workflows/test/run\", json={\"context\": ctx_dict}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: message from context\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_context_rejected_by_default(client: AsyncClient) -> None:\n    ctx_dict = (\n        await serialize_context({\"test_param\": \"message from context\"})\n    ).model_dump(mode=\"python\")\n    response = await client.post(\"/workflows/test/run\", json={\"context\": ctx_dict})\n    assert response.status_code == 400\n    assert \"Context API is disabled\" in response.json()[\"detail\"]\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_start_event(client: AsyncClient) -> None:\n    # Test with simple start event containing message\n    start_event_json = '{\"__is_pydantic\": true, \"value\": {\"_data\": {\"message\": \"start event message\"}}, \"qualified_name\": \"workflows.events.StartEvent\"}'\n    response = await client.post(\n        \"/workflows/test/run\", json={\"start_event\": start_event_json}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: start event message\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_not_found(client: AsyncClient) -> None:\n    response = await client.post(\"/workflows/nonexistent/run\", json={})\n    assert response.status_code == 404\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_error(client: AsyncClient) -> None:\n    response = await client.post(\"/workflows/error/run\", json={})\n    assert response.status_code == 500\n    data = response.json()\n    assert data[\"error\"] is not None\n    assert \"Test error\" in data[\"error\"]\n    assert data[\"status\"] == \"failed\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_invalid_json(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/workflows/test/run\",\n        content=\"invalid json\",\n        headers={\"Content-Type\": \"application/json\"},\n    )\n    assert response.status_code == 400\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_invalid_start_event(client: AsyncClient) -> None:\n    # Test with invalid JSON for start_event\n    response = await client.post(\n        \"/workflows/test/run\", json={\"start_event\": \"invalid json\"}\n    )\n    assert response.status_code == 400\n    assert \"Validation error for 'start_event'\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_nowait_invalid_start_event(\n    client: AsyncClient,\n) -> None:\n    # Test with invalid JSON for start_event in nowait endpoint\n    response = await client.post(\n        \"/workflows/test/run-nowait\", json={\"start_event\": \"invalid json\"}\n    )\n    assert response.status_code == 400\n    assert \"Validation error for 'start_event'\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_structured_start_event_empty_object_validated(\n    client: AsyncClient,\n    server: WorkflowServer,\n    structured_start_workflow: Workflow,\n) -> None:\n    # Register workflow with required StartEvent fields\n    server.add_workflow(\"structured\", structured_start_workflow)\n\n    # Empty object should be validated and rejected with 400\n    response = await client.post(\n        \"/workflows/structured/run\",\n        json={\"start_event\": {}},\n    )\n    assert response.status_code == 400\n    assert \"Validation error for 'start_event'\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_structured_start_event_missing_value_treated_as_empty_and_validated(\n    client: AsyncClient,\n    server: WorkflowServer,\n    structured_start_workflow: Workflow,\n) -> None:\n    # Register workflow with required StartEvent fields\n    server.add_workflow(\"structured\", structured_start_workflow)\n\n    response = await client.post(\n        \"/workflows/structured/run\",\n        json={},  # testing no start_event whatsoever\n    )\n    assert response.status_code == 400\n    assert \"Validation error for 'start_event'\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_with_start_event_and_kwargs(\n    client: AsyncClient,\n) -> None:\n    # Test that start_event takes precedence over kwargs\n    start_event_json = '{\"__is_pydantic\": true, \"value\": {\"_data\": {\"message\": \"start event priority\"}}, \"qualified_name\": \"workflows.events.StartEvent\"}'\n    response = await client.post(\n        \"/workflows/test/run\",\n        json={\n            \"start_event\": start_event_json,\n            \"kwargs\": {\"message\": \"kwargs message\"},\n        },\n    )\n    assert response.status_code == 200\n    data = response.json()\n    # start_event should take precedence\n    assert data[\"result\"][\"value\"][\"result\"] == \"processed: start event priority\"\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_nowait_success(client: AsyncClient) -> None:\n    response = await client.post(\n        \"/workflows/test/run-nowait\", json={\"kwargs\": {\"message\": \"async\"}}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert \"handler_id\" in data\n    assert \"status\" in data\n    assert data[\"status\"] == \"running\"\n    assert len(data[\"handler_id\"]) == 10  # Default nanoid length\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_nowait_with_start_event(client: AsyncClient) -> None:\n    # Test with start event containing message\n    start_event_json = '{\"__is_pydantic\": true, \"value\": {\"_data\": {\"message\": \"async start event\"}}, \"qualified_name\": \"workflows.events.StartEvent\"}'\n    response = await client.post(\n        \"/workflows/test/run-nowait\", json={\"start_event\": start_event_json}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert \"handler_id\" in data\n    assert \"status\" in data\n    assert data[\"status\"] == \"running\"\n    assert len(data[\"handler_id\"]) == 10  # Default nanoid length\n\n\n@pytest.mark.asyncio\nasync def test_run_workflow_nowait_not_found(client: AsyncClient) -> None:\n    response = await client.post(\"/workflows/nonexistent/run-nowait\", json={})\n    assert response.status_code == 404\n\n\n@pytest.mark.asyncio\nasync def test_get_workflow_result(context_client: AsyncClient) -> None:\n    # Setup a context to test all the code paths\n    ctx_dict = (\n        await serialize_context({\"test_param\": \"message from context\"})\n    ).model_dump(mode=\"python\")\n    # run no-wait\n    response = await context_client.post(\n        \"/workflows/test/run-nowait\", json={\"context\": ctx_dict}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    assert \"handler_id\" in data\n    handler_id = data[\"handler_id\"]\n\n    await asyncio.sleep(0.1)\n\n    # get result\n    response = await context_client.get(f\"/handlers/{handler_id}\")\n    assert response.status_code == 200\n\n    # Verify the result content\n    result_data = response.json()\n    assert \"result\" in result_data\n    assert result_data[\"result\"][\"value\"][\"result\"] == \"processed: message from context\"\n\n\n@pytest.mark.asyncio\nasync def test_get_workflow_result_error(\n    client: AsyncClient, server: WorkflowServer\n) -> None:\n    # run no-wait\n    response = await client.post(\"/workflows/error/run-nowait\", json={})\n    assert response.status_code == 200\n    data = response.json()\n    assert \"handler_id\" in data\n    handler_id = data[\"handler_id\"]\n\n    # get result\n    async def _wait_failed() -> dict[str, Any]:\n        response = await client.get(f\"/handlers/{handler_id}\")\n        assert response.status_code == 500\n        return response.json()\n\n    data = await wait_for_passing(_wait_failed)\n    assert \"error\" in data\n    assert \"Test error\" in data[\"error\"]\n    assert data[\"status\"] == \"failed\"\n\n\n@pytest.mark.asyncio\nasync def test_get_workflow_result_not_found(client: AsyncClient) -> None:\n    response = await client.get(\"/handlers/nonexistent\")\n    assert response.status_code == 404\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_success(client: AsyncClient) -> None:\n    \"\"\"Test streaming events from a workflow.\"\"\"\n    # Start streaming workflow\n    response = await client.post(\n        \"/workflows/streaming/run-nowait\", json={\"kwargs\": {\"count\": 3}}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    handler_id = data[\"handler_id\"]\n\n    # Stream events (after_sequence=-1 to get all from beginning)\n    response = await client.get(f\"/events/{handler_id}?after_sequence=-1\")\n    assert response.status_code == 200\n    assert response.headers[\"content-type\"] == \"text/event-stream; charset=utf-8\"\n\n    # Collect streamed events\n    events: list[dict[str, Any]] = []\n    async for line in response.aiter_lines():\n        line = line.strip()\n        if line.startswith(\"data: \"):\n            event_data = json.loads(line.removeprefix(\"data: \"))\n            assert isinstance(event_data, dict)\n            if event_data:\n                events.append(event_data)\n\n    stream_events = [\n        e for e in events if e[\"qualified_name\"] == \"server_test_fixtures.StreamEvent\"\n    ]\n    assert len(stream_events) == 3\n    for i, event in enumerate(stream_events):\n        assert \"qualified_name\" in event\n        assert event[\"value\"][\"message\"] == f\"event_{i}\"\n        assert event[\"value\"][\"sequence\"] == i\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_sse(client: AsyncClient) -> None:\n    \"\"\"Test streaming events using Server-Sent Events format.\"\"\"\n    # Start streaming workflow\n    response = await client.post(\n        \"/workflows/streaming/run-nowait\", json={\"kwargs\": {\"count\": 2}}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    handler_id = data[\"handler_id\"]\n\n    # Stream events in SSE format (after_sequence=-1 to get all from beginning)\n    response = await client.get(f\"/events/{handler_id}?sse=true&after_sequence=-1\")\n    assert response.status_code == 200\n    assert response.headers[\"content-type\"].startswith(\"text/event-stream\")\n\n    # Collect streamed events\n    events = []\n    current_event = {}\n    async for line in response.aiter_lines():\n        line = line.strip()\n        if line.startswith(\"event: \"):\n            # Extract event type\n            current_event[\"event_type\"] = line.removeprefix(\"event: \")\n        elif line.startswith(\"data: \"):\n            # Extract JSON from SSE data line\n            event_json = line.removeprefix(\"data: \")\n            event_data = json.loads(event_json)\n            # Filter out empty events\n            if event_data:\n                current_event[\"data\"] = event_data\n                events.append(current_event.copy())\n                current_event = {}\n\n    # Verify we got event values (not full event objects)\n    # SSE format returns event data with event_type field\n    stream_events = [\n        e\n        for e in events\n        if e[\"data\"][\"qualified_name\"] == \"server_test_fixtures.StreamEvent\"\n    ]\n    assert len(stream_events) == 2\n\n    for i, event in enumerate(stream_events):\n        # SSE format returns event type and data separately\n        assert \"event_type\" not in event\n        assert event[\"data\"][\"value\"][\"message\"] == f\"event_{i}\"\n        assert event[\"data\"][\"value\"][\"sequence\"] == i\n\n    # reconnect with after_sequence beyond last event returns 204\n    response = await client.get(f\"/events/{handler_id}?sse=true&after_sequence=999999\")\n    assert response.status_code == 204\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_not_found(client: AsyncClient) -> None:\n    \"\"\"Test streaming events from non-existent handler.\"\"\"\n    response = await client.get(\"/events/nonexistent\")\n    assert response.status_code == 404\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_multiple_consumers(client: AsyncClient) -> None:\n    \"\"\"Multiple concurrent consumers can stream the same handler's events.\"\"\"\n    # Start a streaming workflow\n    response = await client.post(\n        \"/workflows/streaming/run-nowait\", json={\"kwargs\": {\"count\": 2}}\n    )\n    handler_id = response.json()[\"handler_id\"]\n\n    # Two concurrent stream requests (after_sequence=-1 to get all from beginning)\n    a = asyncio.create_task(client.get(f\"/events/{handler_id}?after_sequence=-1\"))\n    b = asyncio.create_task(client.get(f\"/events/{handler_id}?after_sequence=-1\"))\n\n    response_a, response_b = await asyncio.gather(a, b)\n\n    assert response_a.status_code == 200\n    assert response_b.status_code == 200\n\n    # Both consumers should receive the same events\n    def parse_events(text: str) -> list[dict[str, Any]]:\n        events = []\n        for line in text.strip().split(\"\\n\"):\n            line = line.strip()\n            if line.startswith(\"data: \"):\n                data = json.loads(line.removeprefix(\"data: \"))\n                if data:\n                    events.append(data)\n        return events\n\n    events_a = parse_events(response_a.text)\n    events_b = parse_events(response_b.text)\n\n    # Both should have the same event types\n    types_a = [e[\"type\"] for e in events_a]\n    types_b = [e[\"type\"] for e in events_b]\n    assert types_a == types_b\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_no_events_default_hides_internal(\n    client: AsyncClient,\n) -> None:\n    \"\"\"Test streaming from workflow that emits no events. Default excludes internal events.\"\"\"\n    # Start simple workflow that doesn't emit user events\n    response = await client.post(\n        \"/workflows/test/run-nowait\", json={\"kwargs\": {\"message\": \"test\"}}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    handler_id = data[\"handler_id\"]\n\n    # Stream without include_internal (after_sequence=-1 to get all from beginning)\n    response = await client.get(f\"/events/{handler_id}?after_sequence=-1\")\n    assert response.status_code == 200\n    assert response.headers[\"content-type\"] == \"text/event-stream; charset=utf-8\"\n\n    # Collect events\n    events = []\n    async for line in response.aiter_lines():\n        line = line.strip()\n        if line.startswith(\"data: \"):\n            event_data = json.loads(line.removeprefix(\"data: \"))\n            if event_data:\n                events.append(event_data)\n\n    # Only StopEvent should be present because internal events are hidden by default\n    event_types = [e[\"qualified_name\"] for e in events]\n    assert set(event_types) == {\"workflows.events.StopEvent\"}\n    assert event_types[-1] == \"workflows.events.StopEvent\"\n    assert Counter(event_types)[\"workflows.events.StopEvent\"] == 1\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_include_internal_true(client: AsyncClient) -> None:\n    \"\"\"When include_internal=true, internal events should be included.\"\"\"\n    # Start simple workflow that doesn't emit user events\n    response = await client.post(\n        \"/workflows/test/run-nowait\", json={\"kwargs\": {\"message\": \"test\"}}\n    )\n    assert response.status_code == 200\n    data = response.json()\n    handler_id = data[\"handler_id\"]\n\n    # Stream with include_internal=true (after_sequence=-1 to get all from beginning)\n    response = await client.get(\n        f\"/events/{handler_id}?include_internal=true&after_sequence=-1\"\n    )\n    assert response.status_code == 200\n    assert response.headers[\"content-type\"] == \"text/event-stream; charset=utf-8\"\n\n    # Collect events\n    events = []\n    async for line in response.aiter_lines():\n        line = line.strip()\n        if line.startswith(\"data: \"):\n            event_data = json.loads(line.removeprefix(\"data: \"))\n            if event_data:\n                events.append(event_data)\n\n    event_types = [e[\"qualified_name\"] for e in events]\n    # Expect internal event types to be present along with StopEvent\n    assert \"workflows.events.StopEvent\" in event_types\n    assert \"workflows.events.StepStateChanged\" in event_types\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_empty(client: AsyncClient) -> None:\n    response = await client.get(\"/handlers\")\n    assert response.status_code == 200\n    assert response.json() == {\"handlers\": []}\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_with_running_workflows(client: AsyncClient) -> None:\n    # Start multiple workflows\n    response1 = await client.post(\"/workflows/test/run-nowait\", json={})\n    handler_id1 = response1.json()[\"handler_id\"]\n\n    response2 = await client.post(\"/workflows/test/run-nowait\", json={})\n    handler_id2 = response2.json()[\"handler_id\"]\n\n    # Get handlers\n    response = await client.get(\"/handlers\")\n    assert response.status_code == 200\n    handlers = response.json()[\"handlers\"]\n\n    # Should have 2 handlers\n    assert len(handlers) == 2\n    handler_ids = {handler[\"handler_id\"] for handler in handlers}\n    assert handler_id1 in handler_ids\n    assert handler_id2 in handler_ids\n\n    # Check all required fields are present\n    for handler in handlers:\n        assert \"handler_id\" in handler\n        assert \"status\" in handler\n        assert \"result\" in handler\n        assert \"error\" in handler\n        assert handler[\"status\"] == \"running\"\n        assert handler[\"result\"] is None  # Running workflows don't have results yet\n        assert handler[\"error\"] is None  # Running workflows don't have errors\n\n    # Wait for workflows to complete to avoid warnings\n    for handler_id in [handler_id1, handler_id2]:\n        response = await client.get(f\"/handlers/{handler_id}\")\n        while response.status_code == 202:\n            await asyncio.sleep(0.01)\n            response = await client.get(f\"/handlers/{handler_id}\")\n\n\nasync def validate_result_response(\n    handler_id: str, client: AsyncClient, expected_status: int = 200\n) -> Any:\n    response = await client.get(f\"/handlers/{handler_id}\")\n    assert response.status_code == expected_status\n    return response.json() if expected_status == 200 else response.text\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_with_completed_workflow(client: AsyncClient) -> None:\n    # Start a workflow and wait for it to complete\n    response = await client.post(\"/workflows/test/run-nowait\", json={})\n    handler_id = response.json()[\"handler_id\"]\n\n    await wait_for_passing(lambda: validate_result_response(handler_id, client))\n    # Get handlers\n    response = await client.get(\"/handlers\")\n    assert response.status_code == 200\n    handlers = response.json()[\"handlers\"]\n\n    # Find our handler\n    handler = next(h for h in handlers if h[\"handler_id\"] == handler_id)\n    assert handler[\"status\"] == \"completed\"\n    assert handler[\"result\"] == {\n        \"type\": \"StopEvent\",\n        \"value\": {\"result\": \"processed: default\"},\n        \"qualified_name\": \"workflows.events.StopEvent\",\n        \"types\": None,\n    }\n    assert handler[\"error\"] is None\n\n\n@pytest.mark.asyncio\nasync def test_custom_stop_event_serialization_in_run_and_handlers(\n    client: AsyncClient, server: WorkflowServer\n) -> None:\n    # Register custom workflow that returns a CustomStopEvent\n    server.add_workflow(\"custom\", CustomStopWorkflow())\n\n    # Synchronous run returns a handler with result immediately\n    resp_run = await client.post(\"/workflows/custom/run\", json={})\n    assert resp_run.status_code == 200\n    run_data = resp_run.json()\n    assert run_data[\"status\"] == \"completed\"\n    assert isinstance(run_data.get(\"result\"), dict)\n    assert run_data[\"result\"][\"type\"] == \"CustomStopEvent\"\n    # Minimal value check\n    assert run_data[\"result\"][\"value\"][\"message\"] == \"custom-completed\"\n\n    # No-wait run then observe via handlers\n    resp_nowait = await client.post(\"/workflows/custom/run-nowait\", json={})\n    assert resp_nowait.status_code == 200\n    handler_id = resp_nowait.json()[\"handler_id\"]\n\n    # Wait for completion via results endpoint\n    async def _wait_done() -> dict[str, Any]:\n        r = await client.get(f\"/handlers/{handler_id}\")\n        if r.status_code == 200:\n            return r.json()\n        raise AssertionError(\"not done\")\n\n    result_data = await wait_for_passing(_wait_done)\n    assert result_data[\"result\"][\"type\"] == \"CustomStopEvent\"\n    assert result_data[\"result\"][\"value\"][\"message\"] == \"custom-completed\"\n\n    # Handlers list should reflect the same serialized result\n    resp_handlers = await client.get(\"/handlers\")\n    assert resp_handlers.status_code == 200\n    handlers = resp_handlers.json()[\"handlers\"]\n    custom = next(h for h in handlers if h[\"handler_id\"] == handler_id)\n    assert custom[\"status\"] == \"completed\"\n    assert custom[\"result\"][\"type\"] == \"CustomStopEvent\"\n    assert custom[\"result\"][\"value\"][\"message\"] == \"custom-completed\"\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_with_failed_workflow(client: AsyncClient) -> None:\n    # Start an error workflow\n    response = await client.post(\"/workflows/error/run-nowait\", json={})\n    handler_id = response.json()[\"handler_id\"]\n\n    # Wait a bit for workflow to fail\n    await asyncio.sleep(0.1)\n\n    result = await wait_for_passing(\n        lambda: validate_result_response(handler_id, client, 500)\n    )\n    assert \"Test error\" in result\n\n    # Get handlers\n    response = await client.get(\"/handlers\")\n    assert response.status_code == 200\n    handlers = response.json()[\"handlers\"]\n\n    # Find our handler\n    handler = next(h for h in handlers if h[\"handler_id\"] == handler_id)\n    assert handler[\"status\"] == \"failed\"\n    assert handler[\"error\"] is not None  # Should have an error\n    assert \"Test error\" in handler[\"error\"]  # Check error message\n    assert handler[\"result\"] is None  # Failed workflows don't have results\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_filters_status_and_workflow_name(\n    interactive_workflow: Workflow,\n) -> None:\n    # Seed persistence with mixed handlers\n    persisted = [\n        PersistentHandler(\n            handler_id=\"h1\", workflow_name=\"interactive\", status=\"running\"\n        ),\n        PersistentHandler(\n            handler_id=\"h2\", workflow_name=\"interactive\", status=\"completed\"\n        ),\n        PersistentHandler(handler_id=\"h3\", workflow_name=\"other\", status=\"failed\"),\n    ]\n\n    async with server_with_persisted_handlers(\n        interactive_workflow, persisted_handlers=persisted\n    ) as (_server, client, _store):\n        # Filter by single status\n        r1 = await client.get(\"/handlers?status=completed\")\n        assert r1.status_code == 200\n        ids1 = {h[\"handler_id\"] for h in r1.json()[\"handlers\"]}\n        assert ids1 == {\"h2\"}\n\n        # Filter by workflow name\n        r2 = await client.get(\"/handlers?workflow_name=interactive\")\n        assert r2.status_code == 200\n        ids2 = {h[\"handler_id\"] for h in r2.json()[\"handlers\"]}\n        assert ids2 == {\"h1\", \"h2\"}\n\n        # Filter by both\n        r3 = await client.get(\"/handlers?workflow_name=interactive&status=running\")\n        assert r3.status_code == 200\n        ids3 = {h[\"handler_id\"] for h in r3.json()[\"handlers\"]}\n        assert ids3 == {\"h1\"}\n\n\n@pytest.mark.asyncio\nasync def test_get_handlers_filters_multiple_status_params(\n    interactive_workflow: Workflow,\n) -> None:\n    persisted = [\n        PersistentHandler(\n            handler_id=\"ha\", workflow_name=\"interactive\", status=\"completed\"\n        ),\n        PersistentHandler(\n            handler_id=\"hb\", workflow_name=\"interactive\", status=\"failed\"\n        ),\n        PersistentHandler(\n            handler_id=\"hc\", workflow_name=\"interactive\", status=\"running\"\n        ),\n    ]\n\n    async with server_with_persisted_handlers(\n        interactive_workflow, persisted_handlers=persisted\n    ) as (_server, client, _store):\n        r = await client.get(\"/handlers?status=completed&status=failed\")\n        assert r.status_code == 200\n        ids = {h[\"handler_id\"] for h in r.json()[\"handlers\"]}\n        assert ids == {\"ha\", \"hb\"}\n\n\n@pytest.mark.asyncio\nasync def test_post_event_to_running_workflow(\n    client: AsyncClient, server: WorkflowServer\n) -> None:\n    # Start an interactive workflow\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    await wait_for_requested_external_event(server._service.store, handler_id)\n\n    serializer = JsonSerializer()\n    event = ExternalEvent(response=\"Hello from test\")\n    event_str = serializer.serialize(event)\n\n    # Send the event\n    response = await client.post(f\"/events/{handler_id}\", json={\"event\": event_str})\n    assert response.status_code == 200\n    assert response.json() == {\"status\": \"sent\"}\n\n    result = await wait_for_passing(\n        lambda: validate_result_response(handler_id, client)\n    )\n\n    assert result[\"result\"][\"value\"][\"result\"] == \"received: Hello from test\"\n\n\n@pytest.mark.asyncio\nasync def test_post_event_simple_schema_to_running_workflow(\n    client: AsyncClient, server: WorkflowServer\n) -> None:\n    # Start an interactive workflow\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    await wait_for_requested_external_event(server._service.store, handler_id)\n\n    # Send the event using type/data dict format\n    event_str = '{\"type\": \"ExternalEvent\", \"data\": {\"response\": \"Hello from test\"}}'\n    response = await client.post(f\"/events/{handler_id}\", json={\"event\": event_str})\n    assert response.status_code == 200\n    assert response.json() == {\"status\": \"sent\"}\n\n    result = await wait_for_passing(\n        lambda: validate_result_response(handler_id, client)\n    )\n\n    assert result[\"result\"][\"value\"][\"result\"] == \"received: Hello from test\"\n\n\n@pytest.mark.asyncio\nasync def test_post_event_with_discriminators_to_running_workflow(\n    client: AsyncClient, server: WorkflowServer\n) -> None:\n    \"\"\"Test posting event using JSON serializer dict format with discriminators.\"\"\"\n    # Start an interactive workflow\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    await wait_for_requested_external_event(server._service.store, handler_id)\n\n    # Send event as a dict with discriminators (not as a string)\n    # This is the format returned by JsonSerializer().serialize_value()\n    serializer = JsonSerializer()\n    event = ExternalEvent(response=\"Hello with discriminators\")\n    event_dict = serializer.serialize_value(event)\n\n    response = await client.post(f\"/events/{handler_id}\", json={\"event\": event_dict})\n    assert response.status_code == 200\n    assert response.json() == {\"status\": \"sent\"}\n\n    result = await wait_for_passing(\n        lambda: validate_result_response(handler_id, client)\n    )\n\n    assert result[\"result\"][\"value\"][\"result\"] == \"received: Hello with discriminators\"\n\n\n@pytest.mark.asyncio\nasync def test_get_workflow_result_returns_202_when_pending(\n    client: AsyncClient,\n) -> None:\n    # Start workflow that waits for an external event and thus remains pending\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    response = await client.get(f\"/handlers/{handler_id}\")\n    assert response.status_code == 202\n\n\n@pytest.mark.asyncio\nasync def test_get_workflow_result_multiple_times(\n    client: AsyncClient,\n) -> None:\n    # Start and wait for completion\n    response = await client.post(\n        \"/workflows/test/run-nowait\", json={\"kwargs\": {\"message\": \"cache-me\"}}\n    )\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    # First fetch populates cache\n    first = await wait_for_passing(lambda: validate_result_response(handler_id, client))\n    assert first[\"result\"][\"value\"][\"result\"] == \"processed: cache-me\"\n\n    second = await validate_result_response(handler_id, client)\n    assert second == first\n\n\n@pytest.mark.asyncio\nasync def test_post_event_handler_not_found(client: AsyncClient) -> None:\n    response = await client.post(\"/events/nonexistent_handler\", json={\"event\": \"{}\"})\n    assert response.status_code == 404\n    assert \"Handler not found\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_post_event_to_completed_workflow(client: AsyncClient) -> None:\n    # Start and wait for a simple workflow to complete\n    response = await client.post(\"/workflows/test/run-nowait\", json={})\n    handler_id = response.json()[\"handler_id\"]\n\n    # Wait for workflow to complete\n    response = await client.get(f\"/handlers/{handler_id}\")\n    while response.status_code == 202:\n        await asyncio.sleep(0.01)\n        response = await client.get(f\"/handlers/{handler_id}\")\n\n    # Try to send event to completed workflow\n    response = await client.post(f\"/events/{handler_id}\", json={\"event\": \"{}\"})\n    assert response.status_code == 409\n    assert \"Workflow already completed\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_post_event_invalid_event_data(client: AsyncClient) -> None:\n    # Start an interactive workflow\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    handler_id = response.json()[\"handler_id\"]\n\n    # Send invalid event data\n    response = await client.post(\n        f\"/events/{handler_id}\", json={\"event\": \"invalid json\"}\n    )\n    assert response.status_code == 400\n    assert \"Failed to deserialize event\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_post_event_body_parsing_error(client: AsyncClient) -> None:\n    # Start interactive workflow which waits for an event (keeps running)\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    # Send invalid JSON body (not JSON), triggers 400 from body parsing\n    response = await client.post(\n        f\"/events/{handler_id}\",\n        content=\"not json\",\n        headers={\"Content-Type\": \"application/json\"},\n    )\n    assert response.status_code == 400\n    assert \"Error processing request\" in response.json()[\"detail\"]\n\n\n@pytest.mark.asyncio\nasync def test_post_event_missing_event_data(client: AsyncClient) -> None:\n    # Start an interactive workflow\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    handler_id = response.json()[\"handler_id\"]\n\n    # Send request without event data\n    response = await client.post(f\"/events/{handler_id}\", json={})\n    assert response.status_code == 400\n    assert \"Event data is required\" in response.text\n\n\n@pytest.mark.asyncio\nasync def test_handler_datetime_fields_progress(\n    client: AsyncClient, server: WorkflowServer\n) -> None:\n    # Start interactive workflow which waits for an external event\n    response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    # Snapshot initial times\n    resp = await client.get(\"/handlers\")\n    assert resp.status_code == 200\n    handlers = resp.json()[\"handlers\"]\n    item = next(h for h in handlers if h[\"handler_id\"] == handler_id)\n    started_at_1 = datetime.fromisoformat(item[\"started_at\"])  # ISO 8601\n    updated_at_1 = datetime.fromisoformat(item[\"updated_at\"])  # ISO 8601\n    assert started_at_1 <= updated_at_1\n    assert item[\"completed_at\"] is None\n\n    await wait_for_requested_external_event(server._service.store, handler_id)\n\n    # Send an external event to progress the workflow and update timestamps\n    serializer = JsonSerializer()\n    event = ExternalEvent(response=\"ts-check\")\n    event_str = serializer.serialize(event)\n    send = await client.post(f\"/events/{handler_id}\", json={\"event\": event_str})\n    assert send.status_code == 200\n\n    # Check updated_at increased\n    resp2 = await client.get(\"/handlers\")\n    assert resp2.status_code == 200\n    item2 = next(h for h in resp2.json()[\"handlers\"] if h[\"handler_id\"] == handler_id)\n    updated_at_2 = datetime.fromisoformat(item2[\"updated_at\"])  # ISO 8601\n    assert updated_at_2 >= updated_at_1\n\n    # Wait for completion and check completed_at\n    async def _wait_done() -> Response:\n        r = await client.get(f\"/handlers/{handler_id}\")\n        if r.status_code == 200:\n            return r\n        raise AssertionError(\"not done\")\n\n    await wait_for_passing(_wait_done)\n\n    resp3 = await client.get(\"/handlers\")\n    item3 = next(h for h in resp3.json()[\"handlers\"] if h[\"handler_id\"] == handler_id)\n    assert item3[\"status\"] in {\"completed\", \"failed\"}\n    if item3[\"status\"] == \"completed\":\n        assert item3[\"completed_at\"] is not None\n        completed_at = datetime.fromisoformat(item3[\"completed_at\"])  # ISO 8601\n        assert completed_at >= updated_at_2\n\n\n@pytest.mark.asyncio\nasync def test_cancel_handler_persists_cancelled_status(\n    interactive_workflow: Workflow,\n) -> None:\n    async with server_with_persisted_handlers(interactive_workflow) as (\n        _server,\n        client,\n        store,\n    ):\n        response = await client.post(\n            \"/workflows/interactive/run-nowait\",\n            json={},\n        )\n        handler_id = response.json()[\"handler_id\"]\n        resp_cancel = await client.post(f\"/handlers/{handler_id}/cancel?purge=false\")\n        assert resp_cancel.status_code == 200\n        assert resp_cancel.json() == {\"status\": \"cancelled\"}\n        persisted_cancelled = await store.query(\n            HandlerQuery(handler_id_in=[handler_id])\n        )\n        assert len(persisted_cancelled) == 1\n        assert persisted_cancelled[0].status == \"cancelled\"\n\n        resp_delete2 = await client.post(f\"/handlers/{handler_id}/cancel?purge=false\")\n        assert resp_delete2.status_code == 404\n\n\n@pytest.mark.asyncio\nasync def test_delete_persisted_handler_removes_from_store(\n    interactive_workflow: Workflow,\n) -> None:\n    async with server_with_persisted_handlers(\n        interactive_workflow,\n        persisted_handlers=[\n            PersistentHandler(\n                handler_id=\"persist-only\",\n                workflow_name=\"interactive\",\n                status=\"completed\",\n            )\n        ],\n    ) as (_server, client, store):\n        resp_delete_store = await client.post(\n            \"/handlers/persist-only/cancel?purge=true\"\n        )\n        assert resp_delete_store.status_code == 200\n        assert resp_delete_store.json() == {\"status\": \"deleted\"}\n\n        persisted_handlers = await store.query(\n            HandlerQuery(handler_id_in=[\"persist-only\"])\n        )\n        assert persisted_handlers == []\n\n\n@pytest.mark.asyncio\nasync def test_stop_only_persisted_handler_without_removal_returns_not_found(\n    interactive_workflow: Workflow,\n) -> None:\n    async with server_with_persisted_handlers(\n        interactive_workflow,\n        persisted_handlers=[\n            PersistentHandler(\n                handler_id=\"store-only\",\n                workflow_name=\"interactive\",\n                status=\"completed\",\n            )\n        ],\n    ) as (_server, client, store):\n        resp_cancel_store_only = await client.post(\"/handlers/store-only/cancel\")\n        assert resp_cancel_store_only.status_code == 404\n\n        resp_cancel_store_only = await client.post(\"/handlers/store-only/cancel\")\n        assert resp_cancel_store_only.status_code == 404\n\n        persisted = await store.query(HandlerQuery(handler_id_in=[\"store-only\"]))\n        assert persisted and persisted[0].status == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_legacy_results_endpoint_still_works(client: AsyncClient) -> None:\n    # Start a workflow without waiting\n    response = await client.post(\"/workflows/test/run-nowait\", json={})\n    assert response.status_code == 200\n    handler_id = response.json()[\"handler_id\"]\n\n    # Poll the deprecated endpoint until completion\n    async def _wait_done() -> Response:\n        r = await client.get(f\"/results/{handler_id}\")\n        if r.status_code == 200:\n            return r\n        raise AssertionError(\"not done\")\n\n    r = await wait_for_passing(_wait_done)\n    data = r.json()\n    # Ensure it returns an object and includes an untyped result field\n    assert isinstance(data, dict)\n    assert data.get(\"handler_id\") == handler_id\n    assert data.get(\"result\") is not None\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_after_completion_should_return_unconsumed_events(\n    client: AsyncClient,\n) -> None:\n    # Start streaming workflow that emits 3 events and completes\n    start_resp = await client.post(\n        \"/workflows/streaming/run-nowait\", json={\"kwargs\": {\"count\": 3}}\n    )\n    assert start_resp.status_code == 200\n    handler_id = start_resp.json()[\"handler_id\"]\n\n    # Wait for completion via results endpoint\n    async def _wait_done() -> Response:\n        r = await client.get(f\"/handlers/{handler_id}\")\n        if r.status_code == 200:\n            return r\n        raise AssertionError(\"not done\")\n\n    await wait_for_passing(_wait_done)\n\n    # Now fetch events AFTER completion. Expect all events to be retrievable.\n    # Use NDJSON for easier parsing. after_sequence=-1 to get all from beginning.\n    resp = await client.get(f\"/events/{handler_id}?sse=false&after_sequence=-1\")\n    assert resp.status_code == 200\n    assert resp.headers[\"content-type\"].startswith(\"application/x-ndjson\")\n\n    # Collect NDJSON lines\n    lines: list[str] = []\n    async for line in resp.aiter_lines():\n        data = line.strip()\n        if data:\n            lines.append(data)\n\n    assert len(lines) == 4\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_sse_includes_id_field(client: AsyncClient) -> None:\n    \"\"\"SSE events include an id: field with the event sequence number.\"\"\"\n    response = await client.post(\n        \"/workflows/streaming/run-nowait\", json={\"kwargs\": {\"count\": 2}}\n    )\n    handler_id = response.json()[\"handler_id\"]\n\n    response = await client.get(f\"/events/{handler_id}?sse=true&after_sequence=-1\")\n    assert response.status_code == 200\n\n    # Parse raw SSE frames and extract id fields\n    ids: list[int] = []\n    for line in response.text.strip().split(\"\\n\"):\n        line = line.strip()\n        if line.startswith(\"id: \"):\n            ids.append(int(line.removeprefix(\"id: \")))\n\n    # Every SSE event should have an id\n    assert len(ids) >= 2\n    # Ids should be monotonically increasing\n    assert ids == sorted(ids)\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_last_event_id_header(client: AsyncClient) -> None:\n    \"\"\"SSE Last-Event-ID header takes priority over after_sequence query param.\"\"\"\n    response = await client.post(\n        \"/workflows/streaming/run-nowait\", json={\"kwargs\": {\"count\": 3}}\n    )\n    handler_id = response.json()[\"handler_id\"]\n\n    # First, stream all events to get the sequence numbers\n    response = await client.get(f\"/events/{handler_id}?sse=true&after_sequence=-1\")\n    assert response.status_code == 200\n\n    ids: list[int] = []\n    for line in response.text.strip().split(\"\\n\"):\n        line = line.strip()\n        if line.startswith(\"id: \"):\n            ids.append(int(line.removeprefix(\"id: \")))\n    assert len(ids) >= 3\n\n    # Reconnect with Last-Event-ID header set to skip past all events.\n    # The query param says after_sequence=-1 (from beginning), but the header\n    # should override it.\n    response = await client.get(\n        f\"/events/{handler_id}?sse=true&after_sequence=-1\",\n        headers={\"last-event-id\": str(ids[-1])},\n    )\n    # Should get 204 because Last-Event-ID is past the last event and the run\n    # is complete.\n    assert response.status_code == 204\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_after_sequence_now(client: AsyncClient) -> None:\n    \"\"\"after_sequence=now skips historical events, only receives new ones.\"\"\"\n    # Start a streaming workflow\n    response = await client.post(\n        \"/workflows/streaming/run-nowait\", json={\"kwargs\": {\"count\": 3}}\n    )\n    handler_id = response.json()[\"handler_id\"]\n\n    # Wait for completion so all events are stored\n    async def _wait_done() -> None:\n        r = await client.get(f\"/handlers/{handler_id}\")\n        if r.status_code != 200:\n            raise AssertionError(\"not done\")\n\n    await wait_for_passing(_wait_done)\n\n    # Now request with after_sequence=now. Since the workflow is already complete,\n    # \"now\" resolves to the last sequence, and there are no remaining events, so\n    # we should get 204.\n    response = await client.get(f\"/events/{handler_id}?after_sequence=now\")\n    assert response.status_code == 204\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_after_sequence_now_receives_future_events(\n    interactive_workflow: Workflow,\n) -> None:\n    \"\"\"after_sequence=now on a running workflow receives only events appended after the request.\"\"\"\n    async with server_with_persisted_handlers(interactive_workflow) as (\n        _server,\n        client,\n        store,\n    ):\n        # Start the interactive workflow (it waits for an external event)\n        start_resp = await client.post(\"/workflows/interactive/run-nowait\", json={})\n        handler_id = start_resp.json()[\"handler_id\"]\n\n        await wait_for_requested_external_event(store, handler_id)\n\n        # Count events currently in the store\n        found = await store.query(HandlerQuery(handler_id_in=[handler_id]))\n        run_id = found[0].run_id\n        assert run_id is not None\n        events_before = await store.query_events(run_id)\n        assert len(events_before) > 0\n\n        # Start streaming with after_sequence=now — should skip all existing events\n        stream_task = asyncio.create_task(\n            client.get(f\"/events/{handler_id}?sse=false&after_sequence=now\")\n        )\n\n        # Give the streaming request time to start\n        await asyncio.sleep(0.05)\n\n        # Send an external event to progress the workflow\n        serializer = JsonSerializer()\n        event = ExternalEvent(response=\"after-now\")\n        event_str = serializer.serialize(event)\n        await client.post(f\"/events/{handler_id}\", json={\"event\": event_str})\n\n        response = await stream_task\n        assert response.status_code == 200\n\n        # Parse NDJSON lines\n        events = []\n        for line in response.text.strip().split(\"\\n\"):\n            line = line.strip()\n            if line:\n                events.append(json.loads(line))\n\n        # The events should include at minimum the StopEvent from completion.\n        # They should NOT include any of the events that existed before \"now\".\n        event_types = [e[\"type\"] for e in events]\n        assert \"StopEvent\" in event_types\n\n        # Verify we got fewer events than the total stored — the historical ones were skipped\n        all_events = await store.query_events(run_id)\n        assert len(events) < len(all_events)\n\n\n@pytest.mark.asyncio\nasync def test_instrument_tags_contains_handler_id_in_server_context() -> None:\n    seen_handler_id: dict[str, str | None] = {\"handler_id\": None}\n\n    class TagReadingWorkflow(Workflow):\n        @step\n        async def read_tags(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            # Read handler_id set by the server while streaming events\n            hid = active_instrument_tags.get().get(\"llamaindex.handler_id\")\n            seen_handler_id[\"handler_id\"] = hid\n            return StopEvent()\n\n    server = WorkflowServer(workflow_store=MemoryWorkflowStore(), idle_timeout=0.01)\n    server.add_workflow(\"tags\", TagReadingWorkflow())\n\n    async with server.contextmanager():\n        transport = ASGITransport(app=server.app)\n        async with AsyncClient(transport=transport, base_url=\"http://test\") as client:\n            # Start without waiting\n            start = await client.post(\"/workflows/tags/run-nowait\", json={})\n            assert start.status_code == 200\n            handler_id = start.json()[\"handler_id\"]\n\n            # Wait for completion and fetch result\n            async def _wait_done() -> dict[str, Any]:\n                r = await client.get(f\"/handlers/{handler_id}\")\n                if r.status_code == 200:\n                    return r.json()\n                raise AssertionError(\"not done\")\n\n            data = await wait_for_passing(_wait_done)\n            assert data[\"status\"] == \"completed\"\n            assert seen_handler_id[\"handler_id\"] is not None\n            assert seen_handler_id[\"handler_id\"] == handler_id\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_server_live_http.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport contextlib\nfrom typing import AsyncGenerator\n\nimport pytest\nfrom llama_agents.client.client import WorkflowClient\nfrom llama_agents.server import WorkflowServer\nfrom server_test_fixtures import (  # type: ignore[import]\n    ExternalEvent,\n    RequestedExternalEvent,\n    wait_for_passing,  # type: ignore[import]\n)\nfrom server_test_fixtures import live_server as live_server_ctx  # type: ignore[import]\nfrom workflows import Workflow\nfrom workflows.events import StopEvent\n\n\n@pytest.fixture\nasync def live_server(\n    simple_test_workflow: Workflow,\n    streaming_workflow: Workflow,\n    interactive_workflow: Workflow,\n    error_workflow: Workflow,\n) -> AsyncGenerator[tuple[str, WorkflowServer], None]:\n    def make_server() -> WorkflowServer:\n        server = WorkflowServer()\n        server.add_workflow(\"test\", simple_test_workflow)\n        server.add_workflow(\"streaming\", streaming_workflow)\n        server.add_workflow(\"interactive\", interactive_workflow)\n        server.add_workflow(\"error\", error_workflow)\n        return server\n\n    async with live_server_ctx(make_server) as (base_url, server):\n        yield base_url, server\n\n\n@pytest.mark.asyncio\nasync def test_streaming_over_real_http(\n    live_server: tuple[str, WorkflowServer],\n) -> None:\n    base_url, _server = live_server\n\n    client = WorkflowClient(base_url=base_url)\n\n    # 1) Start interactive workflow (no-wait)\n    started = await client.run_workflow_nowait(\"interactive\")\n    handler_id = started.handler_id\n\n    # Stream until we see the RequestedExternalEvent, then respond and stop streaming\n    saw_prompt = False\n    async for ev in client.get_workflow_events(handler_id):\n        event = ev.load_event()\n        if isinstance(event, RequestedExternalEvent):\n            saw_prompt = True\n            sent = await client.send_event(handler_id, ExternalEvent(response=\"pong\"))\n            assert sent.status == \"sent\"\n            break\n    assert saw_prompt\n\n    data = await client.get_handler(handler_id)\n    assert data.status == \"completed\"\n    assert data.result is not None\n    assert data.result.value.get(\"result\") == \"received: pong\"\n\n\n@pytest.mark.asyncio\nasync def test_reconnect_stream_and_send_event_succeeds(\n    live_server: tuple[str, WorkflowServer],\n) -> None:\n    base_url, _server = live_server\n\n    client = WorkflowClient(base_url=base_url)\n\n    # Start the interactive workflow (no-wait)\n    started = await client.run_workflow_nowait(\"interactive\")\n    handler_id = started.handler_id\n\n    # 1) Connect and read until the first RequestedExternalEvent, then disconnect\n    saw_prompt = False\n    async for ev in client.get_workflow_events(handler_id):\n        event = ev.load_event()\n        if isinstance(event, RequestedExternalEvent):\n            saw_prompt = True\n            break  # simulate client disconnect\n    assert saw_prompt\n\n    # 2) Reconnect to stream; this should succeed and allow streaming again\n    stop_seen = asyncio.Event()\n\n    async def _consume_again() -> None:\n        async for ev in client.get_workflow_events(handler_id):\n            event = ev.load_event()\n            if isinstance(event, StopEvent):\n                stop_seen.set()\n                break\n\n    consumer_task = asyncio.create_task(_consume_again())\n    try:\n        # 3) After reconnect, post the human response; this should succeed\n        sent = await client.send_event(handler_id, ExternalEvent(response=\"pong\"))\n        assert sent.status == \"sent\"\n\n        # 4) Wait for completion\n        async def validate_result_response() -> None:\n            data = await client.get_handler(handler_id)\n            assert data.status == \"completed\"\n\n        await wait_for_passing(validate_result_response)\n\n        await asyncio.wait_for(stop_seen.wait(), timeout=2.0)\n    finally:\n        if not consumer_task.done():\n            consumer_task.cancel()\n            with contextlib.suppress(Exception):\n                await consumer_task\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_server_runtime.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ServerRuntimeDecorator and _ServerInternalRunAdapter.\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom typing import Any, AsyncGenerator\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom llama_agents.server import (\n    HandlerQuery,\n    MemoryWorkflowStore,\n    PersistentHandler,\n    WorkflowServer,\n)\nfrom llama_agents.server._runtime.idle_release_runtime import IdleReleaseDecorator\nfrom llama_agents.server._runtime.server_runtime import (\n    ServerRuntimeDecorator,\n    _ServerInternalRunAdapter,\n)\nfrom workflows import Workflow, step\nfrom workflows.context.state_store import StateStore\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n    WorkflowTimedOutEvent,\n)\nfrom workflows.runtime.runtime_decorators import BaseRuntimeDecorator\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    WaitResult,\n    WaitResultTimeout,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\n\n# -- Stubs -----------------------------------------------------------------\n\n\nclass StubInternalAdapter(InternalRunAdapter):\n    def __init__(self) -> None:\n        self.closed = False\n\n    @property\n    def run_id(self) -> str:\n        return \"r1\"\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        pass\n\n    async def get_now(self) -> float:\n        return 1.0\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        pass\n\n    async def wait_receive(self, timeout_seconds: float | None = None) -> WaitResult:\n        return WaitResultTimeout()\n\n    async def close(self) -> None:\n        self.closed = True\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return None\n\n\nclass StubExternalAdapter(ExternalRunAdapter):\n    def __init__(self) -> None:\n        self.closed = False\n\n    @property\n    def run_id(self) -> str:\n        return \"r1\"\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        pass\n\n    async def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        yield StopEvent(result=\"done\")\n\n    async def close(self) -> None:\n        self.closed = True\n\n    async def get_result(self) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return None\n\n\nclass StubRuntime(Runtime):\n    def __init__(self) -> None:\n        super().__init__()\n        self.launched = False\n\n    def register(self, workflow: Any) -> RegisteredWorkflow:\n        return RegisteredWorkflow(\n            workflow=workflow, workflow_run_fn=MagicMock(), steps={}\n        )\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Any,\n        init_state: Any,\n        start_event: Any = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: Any = None,\n    ) -> ExternalRunAdapter:\n        return StubExternalAdapter()\n\n    def get_internal_adapter(self, workflow: Any) -> InternalRunAdapter:\n        return StubInternalAdapter()\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        return StubExternalAdapter()\n\n    async def launch(self) -> None:\n        self.launched = True\n\n    async def destroy(self) -> None:\n        pass\n\n\nclass SimpleWorkflow(Workflow):\n    @step\n    async def start(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\n# -- Tests -----------------------------------------------------------------\n\n\ndef test_add_workflow_sets_workflow_name() -> None:\n    server = WorkflowServer()\n    wf = SimpleWorkflow(runtime=StubRuntime())\n    server.add_workflow(\"greeting\", wf)\n    assert wf.workflow_name == \"greeting\"\n\n\ndef test_add_workflow_wraps_runtime_with_decorator() -> None:\n    server = WorkflowServer()\n    wf = SimpleWorkflow(runtime=StubRuntime())\n    server.add_workflow(\"greeting\", wf)\n    assert isinstance(wf.runtime, BaseRuntimeDecorator)\n\n\ndef test_add_workflow_no_double_wrap() -> None:\n    server = WorkflowServer()\n    wf = SimpleWorkflow(runtime=StubRuntime())\n    server.add_workflow(\"greeting\", wf)\n    server.add_workflow(\"greeting\", wf)\n    assert isinstance(wf.runtime, ServerRuntimeDecorator)\n    # Inner should be IdleReleaseDecorator, not another ServerRuntimeDecorator\n    assert isinstance(wf.runtime._decorated, IdleReleaseDecorator)\n    assert not isinstance(wf.runtime._decorated, ServerRuntimeDecorator)\n\n\ndef test_server_runtime_decorator_wraps_internal_adapter() -> None:\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n    wf = SimpleWorkflow(runtime=decorator)\n    adapter = decorator.get_internal_adapter(wf)\n    assert isinstance(adapter, _ServerInternalRunAdapter)\n\n\nasync def test_server_internal_adapter_records_events_to_store() -> None:\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n    wf = SimpleWorkflow(runtime=decorator)\n    adapter = decorator.get_internal_adapter(wf)\n\n    await adapter.write_to_event_stream(StopEvent(result=\"hello\"))\n    await adapter.write_to_event_stream(StopEvent(result=\"world\"))\n\n    events = await store.query_events(adapter.run_id)\n    assert len(events) == 2\n    assert events[0].sequence == 0\n    assert events[1].sequence == 1\n    assert events[0].event.type == \"StopEvent\"\n    assert events[1].event.type == \"StopEvent\"\n\n\nasync def test_server_internal_adapter_forwards_to_inner() -> None:\n    \"\"\"The adapter should forward write_to_event_stream to the inner adapter.\n\n    Events are recorded to the store AND forwarded so that inner decorators\n    (e.g. _DurableInternalRunAdapter) can process them for idle detection.\n    \"\"\"\n\n    class RecordingAdapter(StubInternalAdapter):\n        def __init__(self) -> None:\n            super().__init__()\n            self.recorded_events: list[Event] = []\n\n        async def write_to_event_stream(self, event: Event) -> None:\n            self.recorded_events.append(event)\n\n    inner = RecordingAdapter()\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n    adapter = _ServerInternalRunAdapter(inner, decorator)\n\n    stop = StopEvent(result=\"forwarded\")\n    await adapter.write_to_event_stream(stop)\n\n    # Event is forwarded to inner adapter for decorator chain processing\n    assert len(inner.recorded_events) == 1\n    # And also recorded in the store\n    events = await store.query_events(adapter.run_id)\n    assert len(events) == 1\n\n\ndef test_add_workflow_uses_server_runtime_decorator() -> None:\n    server = WorkflowServer()\n    wf = SimpleWorkflow(runtime=StubRuntime())\n    server.add_workflow(\"test\", wf)\n    assert isinstance(wf.runtime, ServerRuntimeDecorator)\n\n\nasync def test_concurrent_runs_get_independent_sequences() -> None:\n    \"\"\"Two adapters from the same decorator should have independent sequences.\"\"\"\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n\n    adapter_a = _ServerInternalRunAdapter(StubInternalAdapterWithId(\"run-a\"), decorator)\n    adapter_b = _ServerInternalRunAdapter(StubInternalAdapterWithId(\"run-b\"), decorator)\n\n    # Interleave writes from both adapters\n    await adapter_a.write_to_event_stream(StopEvent(result=\"a1\"))\n    await adapter_b.write_to_event_stream(StopEvent(result=\"b1\"))\n    await adapter_a.write_to_event_stream(StopEvent(result=\"a2\"))\n    await adapter_b.write_to_event_stream(StopEvent(result=\"b2\"))\n    await adapter_b.write_to_event_stream(StopEvent(result=\"b3\"))\n\n    events_a = await store.query_events(\"run-a\")\n    events_b = await store.query_events(\"run-b\")\n\n    assert len(events_a) == 2\n    assert [e.sequence for e in events_a] == [0, 1]\n\n    assert len(events_b) == 3\n    assert [e.sequence for e in events_b] == [0, 1, 2]\n\n\nclass StubInternalAdapterWithId(StubInternalAdapter):\n    def __init__(self, run_id: str) -> None:\n        super().__init__()\n        self._run_id = run_id\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n\n@pytest.mark.parametrize(\n    \"event, expected_status, expected_error, expected_has_result\",\n    [\n        pytest.param(\n            StopEvent(result=\"done\"),\n            \"completed\",\n            None,\n            True,\n            id=\"stop-event\",\n        ),\n        pytest.param(\n            WorkflowFailedEvent(\n                step_name=\"s\",\n                exception=RuntimeError(\"boom\"),\n                attempts=1,\n                elapsed_seconds=0.0,\n            ),\n            \"failed\",\n            \"boom\",\n            False,\n            id=\"failed-event\",\n        ),\n        pytest.param(\n            WorkflowTimedOutEvent(\n                timeout=10.0,\n                active_steps=[\"s\"],\n            ),\n            \"failed\",\n            \"Workflow timed out after 10.0s\",\n            False,\n            id=\"timed-out-event\",\n        ),\n        pytest.param(\n            WorkflowCancelledEvent(),\n            \"cancelled\",\n            None,\n            False,\n            id=\"cancelled-event\",\n        ),\n    ],\n)\nasync def test_terminal_event_status_transitions(\n    event: Event,\n    expected_status: str,\n    expected_error: str | None,\n    expected_has_result: bool,\n) -> None:\n    \"\"\"Writing a terminal event updates handler status in the store.\"\"\"\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n    decorator._persistence_backoff = [0, 0]\n\n    run_id = \"run-terminal\"\n    # Seed a handler record so update_handler_status can find it\n    await store.update(\n        PersistentHandler(\n            handler_id=\"h1\",\n            workflow_name=\"test\",\n            status=\"running\",\n            run_id=run_id,\n            started_at=datetime.now(timezone.utc),\n        )\n    )\n\n    inner = StubInternalAdapterWithId(run_id)\n    adapter = _ServerInternalRunAdapter(inner, decorator)\n\n    await adapter.write_to_event_stream(event)\n\n    found = await store.query(HandlerQuery(run_id_in=[run_id]))\n    assert len(found) == 1\n    handler = found[0]\n    assert handler.status == expected_status\n    assert handler.error == expected_error\n    if expected_has_result:\n        assert handler.result is not None\n    else:\n        assert handler.result is None\n\n\nasync def test_run_workflow_handler_persists_initial_record() -> None:\n    \"\"\"run_workflow_handler creates a running handler record in the store.\"\"\"\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n    decorator._persistence_backoff = [0, 0]\n\n    await decorator.run_workflow_handler(\"h-init\", \"my_workflow\", \"test-run\")\n\n    found = await store.query(HandlerQuery(handler_id_in=[\"h-init\"]))\n    assert len(found) == 1\n    handler = found[0]\n    assert handler.handler_id == \"h-init\"\n    assert handler.workflow_name == \"my_workflow\"\n    assert handler.status == \"running\"\n    assert handler.run_id == \"test-run\"\n    assert handler.started_at is not None\n\n\nasync def test_retry_store_write_succeeds_after_failures() -> None:\n    \"\"\"_retry_store_write retries and eventually succeeds.\"\"\"\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n    decorator._persistence_backoff = [0, 0]\n\n    call_count = 0\n\n    async def flaky() -> None:\n        nonlocal call_count\n        call_count += 1\n        if call_count < 3:\n            raise RuntimeError(\"transient\")\n\n    await decorator._retry_store_write(flaky)\n    assert call_count == 3\n\n\nasync def test_retry_store_write_raises_after_exhaustion() -> None:\n    \"\"\"_retry_store_write raises when all retries are exhausted.\"\"\"\n    store = MemoryWorkflowStore()\n    decorator = ServerRuntimeDecorator(StubRuntime(), store=store)\n    decorator._persistence_backoff = [0, 0]\n\n    async def always_fail() -> None:\n        raise RuntimeError(\"permanent\")\n\n    with pytest.raises(RuntimeError, match=\"permanent\"):\n        await decorator._retry_store_write(always_fail)\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_sqlite_state_store.py",
    "content": "# ty: ignore[invalid-argument-type]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport sqlite3\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.server import SqliteWorkflowStore\nfrom llama_agents.server._store.sqlite.migrate import run_migrations\nfrom llama_agents.server._store.sqlite.sqlite_state_store import (\n    SqliteStateStore,\n)\nfrom pydantic import BaseModel\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import DictState, InMemoryStateStore\n\n# -- Typed state models for testing --\n\n\nclass CounterState(BaseModel):\n    count: int = 0\n    label: str = \"default\"\n\n\nclass ExtendedCounterState(CounterState):\n    extra: str = \"extra_default\"\n\n\n# -- Fixtures --\n\n\n@pytest.fixture\ndef db_path(tmp_path: Path) -> str:\n    path = str(tmp_path / \"test_state.db\")\n    conn = sqlite3.connect(path)\n    try:\n        run_migrations(conn)\n        conn.commit()\n    finally:\n        conn.close()\n    return path\n\n\n@pytest.fixture\ndef store(db_path: str) -> SqliteStateStore[DictState]:\n    return SqliteStateStore(db_path=db_path, run_id=\"run-1\")\n\n\n@pytest.fixture\ndef typed_store(db_path: str) -> SqliteStateStore[CounterState]:\n    return SqliteStateStore(\n        db_path=db_path,\n        run_id=\"run-typed\",\n        state_type=CounterState,\n    )\n\n\n# -- Basic get/set tests --\n\n\n@pytest.mark.asyncio\nasync def test_get_returns_default_dict_state(\n    store: SqliteStateStore[DictState],\n) -> None:\n    state = await store.get_state()\n    assert isinstance(state, DictState)\n    assert dict(state) == {}\n\n\n@pytest.mark.asyncio\nasync def test_set_and_get_path(store: SqliteStateStore[DictState]) -> None:\n    await store.set(\"foo\", 42)\n    value = await store.get(\"foo\")\n    assert value == 42\n\n\n@pytest.mark.asyncio\nasync def test_set_nested_path(store: SqliteStateStore[DictState]) -> None:\n    await store.set(\"a.b.c\", \"deep\")\n    value = await store.get(\"a.b.c\")\n    assert value == \"deep\"\n\n\n@pytest.mark.asyncio\nasync def test_get_missing_path_raises(store: SqliteStateStore[DictState]) -> None:\n    with pytest.raises(ValueError, match=\"not found\"):\n        await store.get(\"nonexistent\")\n\n\n@pytest.mark.asyncio\nasync def test_get_missing_path_returns_default(\n    store: SqliteStateStore[DictState],\n) -> None:\n    value = await store.get(\"nonexistent\", default=\"fallback\")\n    assert value == \"fallback\"\n\n\n@pytest.mark.asyncio\nasync def test_set_empty_path_raises(store: SqliteStateStore[DictState]) -> None:\n    with pytest.raises(ValueError, match=\"cannot be empty\"):\n        await store.set(\"\", 42)\n\n\n# -- get_state / set_state --\n\n\n@pytest.mark.asyncio\nasync def test_set_state_replaces_dict_state(\n    store: SqliteStateStore[DictState],\n) -> None:\n    await store.set(\"x\", 1)\n    new_state = DictState(y=2)\n    await store.set_state(new_state)\n    state = await store.get_state()\n    assert \"y\" in state\n    assert \"x\" not in state\n\n\n@pytest.mark.asyncio\nasync def test_typed_state_get_returns_default(\n    typed_store: SqliteStateStore[CounterState],\n) -> None:\n    state = await typed_store.get_state()\n    assert isinstance(state, CounterState)\n    assert state.count == 0\n    assert state.label == \"default\"\n\n\n@pytest.mark.asyncio\nasync def test_typed_state_set_and_get(\n    typed_store: SqliteStateStore[CounterState],\n) -> None:\n    await typed_store.set_state(CounterState(count=5, label=\"updated\"))\n    state = await typed_store.get_state()\n    assert state.count == 5\n    assert state.label == \"updated\"\n\n\n@pytest.mark.asyncio\nasync def test_set_state_parent_type_merge(db_path: str) -> None:\n    \"\"\"Setting a parent type state merges fields, preserving child-specific fields.\"\"\"\n    store: SqliteStateStore[ExtendedCounterState] = SqliteStateStore(\n        db_path=db_path,\n        run_id=\"run-merge\",\n        state_type=ExtendedCounterState,\n    )\n    await store.set_state(ExtendedCounterState(count=1, label=\"init\", extra=\"mine\"))\n\n    # Set parent type — should merge\n    parent = CounterState(count=10, label=\"merged\")\n    await store.set_state(parent)  # type: ignore[arg-type]\n\n    state = await store.get_state()\n    assert state.count == 10\n    assert state.label == \"merged\"\n    assert state.extra == \"mine\"  # child field preserved\n\n\n# -- edit_state --\n\n\n@pytest.mark.asyncio\nasync def test_edit_state_dict(store: SqliteStateStore[DictState]) -> None:\n    await store.set(\"counter\", 0)\n    async with store.edit_state() as state:\n        state[\"counter\"] = state[\"counter\"] + 1\n    value = await store.get(\"counter\")\n    assert value == 1\n\n\n@pytest.mark.asyncio\nasync def test_edit_state_typed(typed_store: SqliteStateStore[CounterState]) -> None:\n    async with typed_store.edit_state() as state:\n        state.count += 10\n    result = await typed_store.get_state()\n    assert result.count == 10\n\n\n# -- clear --\n\n\n@pytest.mark.asyncio\nasync def test_clear_resets_state(store: SqliteStateStore[DictState]) -> None:\n    await store.set(\"x\", 99)\n    await store.clear()\n    state = await store.get_state()\n    assert dict(state) == {}\n\n\n@pytest.mark.asyncio\nasync def test_clear_resets_typed_state(\n    typed_store: SqliteStateStore[CounterState],\n) -> None:\n    await typed_store.set_state(CounterState(count=100, label=\"dirty\"))\n    await typed_store.clear()\n    state = await typed_store.get_state()\n    assert state.count == 0\n    assert state.label == \"default\"\n\n\n# -- Persistence across instances --\n\n\n@pytest.mark.asyncio\nasync def test_state_persists_across_instances(db_path: str) -> None:\n    \"\"\"State set by one store instance is readable by a new instance pointing at the same DB.\"\"\"\n    store1: SqliteStateStore[DictState] = SqliteStateStore(\n        db_path=db_path, run_id=\"run-persist\"\n    )\n    await store1.set(\"key\", \"value\")\n\n    store2: SqliteStateStore[DictState] = SqliteStateStore(\n        db_path=db_path, run_id=\"run-persist\"\n    )\n    value = await store2.get(\"key\")\n    assert value == \"value\"\n\n\n@pytest.mark.asyncio\nasync def test_typed_state_persists_across_instances(db_path: str) -> None:\n    store1: SqliteStateStore[CounterState] = SqliteStateStore(\n        db_path=db_path,\n        run_id=\"run-typed-persist\",\n        state_type=CounterState,\n    )\n    await store1.set_state(CounterState(count=42, label=\"persisted\"))\n\n    store2: SqliteStateStore[CounterState] = SqliteStateStore(\n        db_path=db_path,\n        run_id=\"run-typed-persist\",\n        state_type=CounterState,\n    )\n    state = await store2.get_state()\n    assert state.count == 42\n    assert state.label == \"persisted\"\n\n\n@pytest.mark.asyncio\nasync def test_different_run_ids_are_isolated(db_path: str) -> None:\n    store_a: SqliteStateStore[DictState] = SqliteStateStore(\n        db_path=db_path, run_id=\"run-a\"\n    )\n    store_b: SqliteStateStore[DictState] = SqliteStateStore(\n        db_path=db_path, run_id=\"run-b\"\n    )\n    await store_a.set(\"x\", \"from-a\")\n    await store_b.set(\"x\", \"from-b\")\n\n    assert await store_a.get(\"x\") == \"from-a\"\n    assert await store_b.get(\"x\") == \"from-b\"\n\n\n# -- to_dict / from_dict --\n\n\n@pytest.mark.asyncio\nasync def test_to_dict_returns_metadata_only(\n    store: SqliteStateStore[DictState],\n) -> None:\n    await store.set(\"key\", \"value\")\n    serializer = JsonSerializer()\n    d = store.to_dict(serializer)\n    assert d[\"store_type\"] == \"sqlite\"\n    assert d[\"run_id\"] == \"run-1\"\n    assert \"state_data\" not in d\n\n\n@pytest.mark.asyncio\nasync def test_from_dict_sqlite_format(db_path: str) -> None:\n    \"\"\"from_dict with sqlite format reconnects to existing row.\"\"\"\n    store1: SqliteStateStore[DictState] = SqliteStateStore(\n        db_path=db_path, run_id=\"run-fromdict\"\n    )\n    await store1.set(\"saved\", True)\n\n    serializer = JsonSerializer()\n    payload = store1.to_dict(serializer)\n\n    store2 = SqliteStateStore.from_dict(\n        payload, serializer, db_path=db_path, state_type=DictState\n    )\n    value = await store2.get(\"saved\")\n    assert value is True\n\n\n@pytest.mark.asyncio\nasync def test_from_dict_in_memory_format_migrates(db_path: str) -> None:\n    \"\"\"from_dict with InMemorySerializedState format stores data on first DB access.\"\"\"\n    serializer = JsonSerializer()\n    in_memory_store = InMemoryStateStore(DictState(migrated_key=\"migrated_value\"))\n    payload = in_memory_store.to_dict(serializer)\n\n    store = SqliteStateStore.from_dict(\n        payload,\n        serializer,\n        db_path=db_path,\n        state_type=DictState,\n        run_id=\"run-migrate\",\n    )\n    value = await store.get(\"migrated_key\")\n    assert value == \"migrated_value\"\n\n\n@pytest.mark.asyncio\nasync def test_from_dict_empty_raises() -> None:\n    with pytest.raises(ValueError, match=\"Cannot restore\"):\n        SqliteStateStore.from_dict({}, JsonSerializer())\n\n\n# -- Migration applies cleanly --\n\n\n@pytest.mark.asyncio\nasync def test_migration_applies_on_existing_db(tmp_path: Path) -> None:\n    \"\"\"Verify the state table can be created on a DB that already has other tables.\"\"\"\n    db_path = str(tmp_path / \"existing.db\")\n    # Create DB with workflow store tables first\n    SqliteWorkflowStore(db_path)\n\n    # Now create state store on same DB — should work\n    store: SqliteStateStore[DictState] = SqliteStateStore(\n        db_path=db_path, run_id=\"run-coexist\"\n    )\n    await store.set(\"coexist\", True)\n    value = await store.get(\"coexist\")\n    assert value is True\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_sqlite_workflow_store.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.server import (\n    HandlerQuery,\n    PersistentHandler,\n    SqliteWorkflowStore,\n)\nfrom workflows.events import StopEvent\n\n\n@pytest.mark.asyncio\nasync def test_update_and_query_returns_inserted_handler(tmp_path: Path) -> None:\n    db_path: str = str(tmp_path / \"handlers.db\")\n    store = SqliteWorkflowStore(db_path)\n\n    handler = PersistentHandler(\n        handler_id=\"h1\",\n        workflow_name=\"wf_a\",\n        status=\"running\",\n    )\n\n    await store.update(handler)\n\n    # Filter by workflow_name list\n    result = await store.query(\n        HandlerQuery(workflow_name_in=[\"wf_a\"], status_in=[\"running\"])\n    )\n\n    assert len(result) == 1\n    found = result[0]\n    assert found.handler_id == \"h1\"\n    assert found.workflow_name == \"wf_a\"\n    assert found.status == \"running\"\n\n\n@pytest.mark.asyncio\nasync def test_update_on_conflict_overwrites_existing_row(tmp_path: Path) -> None:\n    db_path: str = str(tmp_path / \"handlers.db\")\n    store = SqliteWorkflowStore(db_path)\n\n    # Initial insert (in-progress)\n    await store.update(\n        PersistentHandler(\n            handler_id=\"h2\",\n            workflow_name=\"wf_b\",\n            status=\"running\",\n        )\n    )\n\n    # Update same handler_id (completed)\n    await store.update(\n        PersistentHandler(\n            handler_id=\"h2\",\n            workflow_name=\"wf_b\",\n            status=\"completed\",\n        )\n    )\n\n    # Should not be returned for completed=False\n    result_in_progress = await store.query(\n        HandlerQuery(workflow_name_in=[\"wf_b\"], status_in=[\"running\"])\n    )\n    assert result_in_progress == []\n\n    # Should be returned for completed=True with latest values\n    result_completed = await store.query(\n        HandlerQuery(workflow_name_in=[\"wf_b\"], status_in=[\"completed\"])\n    )\n    assert len(result_completed) == 1\n    found = result_completed[0]\n    assert found.handler_id == \"h2\"\n    assert found.workflow_name == \"wf_b\"\n    assert found.status == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_delete_filters_by_query(tmp_path: Path) -> None:\n    db_path: str = str(tmp_path / \"handlers.db\")\n    store = SqliteWorkflowStore(db_path)\n\n    await store.update(\n        PersistentHandler(\n            handler_id=\"delete-me\",\n            workflow_name=\"wf_delete\",\n            status=\"completed\",\n        )\n    )\n    await store.update(\n        PersistentHandler(\n            handler_id=\"keep-me\",\n            workflow_name=\"wf_keep\",\n            status=\"running\",\n        )\n    )\n\n    deleted = await store.delete(HandlerQuery(handler_id_in=[\"delete-me\"]))\n\n    assert deleted == 1\n    remaining = await store.query(HandlerQuery())\n    ids = {handler.handler_id for handler in remaining}\n    assert ids == {\"keep-me\"}\n\n\n@pytest.mark.asyncio\nasync def test_delete_noop_on_empty_filter(tmp_path: Path) -> None:\n    db_path: str = str(tmp_path / \"handlers.db\")\n    store = SqliteWorkflowStore(db_path)\n\n    await store.update(\n        PersistentHandler(\n            handler_id=\"delete-me\",\n            workflow_name=\"wf_delete\",\n            status=\"completed\",\n        )\n    )\n\n    deleted = await store.delete(HandlerQuery(handler_id_in=[]))\n\n    assert deleted == 0\n    remaining = await store.query(HandlerQuery())\n    assert len(remaining) == 1\n    assert remaining[0].handler_id == \"delete-me\"\n\n\n@pytest.mark.asyncio\nasync def test_query_filters_by_handler_id_and_empty_lists(tmp_path: Path) -> None:\n    db_path: str = str(tmp_path / \"handlers.db\")\n    store = SqliteWorkflowStore(db_path)\n\n    # Seed three handlers\n    for hid, wf in [(\"h1\", \"wf_a\"), (\"h2\", \"wf_a\"), (\"h3\", \"wf_b\")]:\n        await store.update(\n            PersistentHandler(\n                handler_id=hid,\n                workflow_name=wf,\n                status=\"running\",\n            )\n        )\n\n    # Filter by specific handler ids\n    result = await store.query(HandlerQuery(handler_id_in=[\"h1\", \"h3\"]))\n    ids = {h.handler_id for h in result}\n    assert ids == {\"h1\", \"h3\"}\n\n    # Empty handler_id list short-circuits to []\n    result_empty_ids = await store.query(HandlerQuery(handler_id_in=[]))\n    assert result_empty_ids == []\n\n    # Empty workflow_name list short-circuits to []\n    result_empty_wf = await store.query(HandlerQuery(workflow_name_in=[]))\n    assert result_empty_wf == []\n\n    # No filters returns all\n    all_rows = await store.query(HandlerQuery())\n    assert {h.handler_id for h in all_rows} == {\"h1\", \"h2\", \"h3\"}\n\n\nclass CustomStopEvent(StopEvent):\n    x: int\n    y: list[int]\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\n    \"event\",\n    [StopEvent(result={\"meta\": {\"x\": 1, \"y\": [2, 3]}}), CustomStopEvent(x=1, y=[2, 3])],\n)\nasync def test_update_pydantic_result_serialization(\n    tmp_path: Path, event: StopEvent\n) -> None:\n    \"\"\"\n    Ensures that a Pydantic BaseModel (StopEvent) stored in `result` is properly\n    serialized using model_dump_json() and does not raise TypeError as with json.dumps.\n    Also validates round-trip deserialization shape.\n    \"\"\"\n    db_path: str = str(tmp_path / \"handlers.db\")\n    store = SqliteWorkflowStore(db_path)\n\n    handler = PersistentHandler(\n        handler_id=\"pydantic-result\",\n        workflow_name=\"wf_pyd\",\n        status=\"completed\",\n        result=event,\n    )\n\n    # This would raise TypeError if the store used json.dumps(handler.result)\n    await store.update(handler)\n\n    rows = await store.query(HandlerQuery(handler_id_in=[\"pydantic-result\"]))\n    assert len(rows) == 1\n    found = rows[0]\n    assert found.handler_id == \"pydantic-result\"\n\n    # The row's result should deserialize to a StopEvent\n    assert found.result == event\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_sse_heartbeat.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\n\nimport httpx\nimport pytest\nfrom llama_agents.server import MemoryWorkflowStore, WorkflowServer\nfrom server_test_fixtures import (  # type: ignore[import]\n    InteractiveWorkflow,\n    StreamingWorkflow,\n    live_server,\n)\n\n\ndef _server_factory(*, sse_heartbeat_interval: float | None) -> WorkflowServer:\n    server = WorkflowServer(\n        workflow_store=MemoryWorkflowStore(),\n        idle_timeout=0.01,\n        sse_heartbeat_interval=sse_heartbeat_interval,\n    )\n    server.add_workflow(\"interactive\", InteractiveWorkflow())\n    server.add_workflow(\"streaming\", StreamingWorkflow())\n    return server\n\n\n@pytest.mark.asyncio\nasync def test_sse_heartbeat_during_idle() -> None:\n    \"\"\"Heartbeat comments appear when no real events are flowing.\"\"\"\n    async with live_server(lambda: _server_factory(sse_heartbeat_interval=0.05)) as (\n        base_url,\n        _server,\n    ):\n        async with httpx.AsyncClient(base_url=base_url, timeout=10.0) as client:\n            response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n            assert response.status_code == 200\n            handler_id = response.json()[\"handler_id\"]\n\n            collected: list[str] = []\n\n            async def read_stream() -> None:\n                async with client.stream(\n                    \"GET\",\n                    f\"/events/{handler_id}?sse=true&after_sequence=-1\",\n                ) as resp:\n                    async for line in resp.aiter_lines():\n                        collected.append(line)\n                        hb_count = sum(1 for x in collected if x == \": heartbeat\")\n                        if hb_count >= 2:\n                            return\n\n            await asyncio.wait_for(read_stream(), timeout=5.0)\n\n            heartbeat_lines = [x for x in collected if x == \": heartbeat\"]\n            assert len(heartbeat_lines) >= 2\n\n\n@pytest.mark.asyncio\nasync def test_sse_heartbeat_interspersed_with_events() -> None:\n    \"\"\"Real events and heartbeat comments both appear on the same stream.\"\"\"\n    async with live_server(lambda: _server_factory(sse_heartbeat_interval=0.05)) as (\n        base_url,\n        _server,\n    ):\n        async with httpx.AsyncClient(base_url=base_url, timeout=10.0) as client:\n            response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n            assert response.status_code == 200\n            handler_id = response.json()[\"handler_id\"]\n\n            collected: list[str] = []\n\n            async def read_stream() -> None:\n                async with client.stream(\n                    \"GET\",\n                    f\"/events/{handler_id}?sse=true&after_sequence=-1\",\n                ) as resp:\n                    async for line in resp.aiter_lines():\n                        collected.append(line)\n                        has_hb = any(x == \": heartbeat\" for x in collected)\n                        has_data = any(x.startswith(\"data: \") for x in collected)\n                        if has_hb and has_data:\n                            return\n\n            await asyncio.wait_for(read_stream(), timeout=5.0)\n\n            heartbeat_lines = [x for x in collected if x == \": heartbeat\"]\n            data_lines = [x for x in collected if x.startswith(\"data: \")]\n            assert len(heartbeat_lines) >= 1\n            assert len(data_lines) >= 1\n\n\n@pytest.mark.asyncio\nasync def test_ndjson_no_heartbeat() -> None:\n    \"\"\"NDJSON mode does not receive heartbeat comments even when configured.\"\"\"\n    async with live_server(lambda: _server_factory(sse_heartbeat_interval=0.05)) as (\n        base_url,\n        _server,\n    ):\n        async with httpx.AsyncClient(base_url=base_url, timeout=10.0) as client:\n            response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n            assert response.status_code == 200\n            handler_id = response.json()[\"handler_id\"]\n\n            collected: list[str] = []\n\n            async def read_stream() -> None:\n                async with client.stream(\n                    \"GET\",\n                    f\"/events/{handler_id}?sse=false&after_sequence=-1\",\n                ) as resp:\n                    async for line in resp.aiter_lines():\n                        collected.append(line)\n\n            # Let it run briefly — should NOT get any heartbeat\n            try:\n                await asyncio.wait_for(read_stream(), timeout=0.3)\n            except (TimeoutError, asyncio.TimeoutError):\n                pass\n\n            heartbeat_lines = [x for x in collected if \": heartbeat\" in x]\n            assert len(heartbeat_lines) == 0\n\n\n@pytest.mark.asyncio\nasync def test_no_heartbeat_when_disabled() -> None:\n    \"\"\"Default None heartbeat produces no heartbeat comments.\"\"\"\n    async with live_server(lambda: _server_factory(sse_heartbeat_interval=None)) as (\n        base_url,\n        _server,\n    ):\n        async with httpx.AsyncClient(base_url=base_url, timeout=10.0) as client:\n            response = await client.post(\"/workflows/interactive/run-nowait\", json={})\n            assert response.status_code == 200\n            handler_id = response.json()[\"handler_id\"]\n\n            collected: list[str] = []\n\n            async def read_stream() -> None:\n                async with client.stream(\n                    \"GET\",\n                    f\"/events/{handler_id}?sse=true&after_sequence=-1\",\n                ) as resp:\n                    async for line in resp.aiter_lines():\n                        collected.append(line)\n\n            # Let it run briefly — should NOT get any heartbeat\n            try:\n                await asyncio.wait_for(read_stream(), timeout=0.3)\n            except (TimeoutError, asyncio.TimeoutError):\n                pass\n\n            heartbeat_lines = [x for x in collected if x == \": heartbeat\"]\n            assert len(heartbeat_lines) == 0\n\n\n@pytest.mark.asyncio\nasync def test_heartbeat_with_completed_workflow() -> None:\n    \"\"\"Heartbeat doesn't prevent stream from ending when workflow completes.\"\"\"\n    async with live_server(lambda: _server_factory(sse_heartbeat_interval=0.05)) as (\n        base_url,\n        _server,\n    ):\n        async with httpx.AsyncClient(base_url=base_url, timeout=10.0) as client:\n            response = await client.post(\n                \"/workflows/streaming/run-nowait\",\n                json={\"kwargs\": {\"count\": 2}},\n            )\n            assert response.status_code == 200\n            handler_id = response.json()[\"handler_id\"]\n\n            # Wait for workflow to finish, then consume the full stream\n            await asyncio.sleep(0.3)\n\n            response = await client.get(\n                f\"/events/{handler_id}?sse=true&after_sequence=-1\"\n            )\n            assert response.status_code == 200\n\n            data_events = []\n            for line in response.text.splitlines():\n                if line.startswith(\"data: \"):\n                    data_events.append(json.loads(line.removeprefix(\"data: \")))\n\n            # Should have streaming events + StopEvent\n            assert len(data_events) >= 2\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_streaming_replay_memory.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport tracemalloc\nfrom collections.abc import AsyncIterator\nfrom datetime import datetime, timezone\n\nimport pytest\nfrom llama_agents.server._store.abstract_workflow_store import (\n    StoredTick,\n)\nfrom workflows.events import Event\nfrom workflows.runtime.control_loop import rebuild_state_from_ticks_stream\nfrom workflows.runtime.types.internal_state import BrokerConfig, BrokerState\nfrom workflows.runtime.types.ticks import TickPublishEvent, WorkflowTickAdapter\n\n\nclass _BigEvent(Event):\n    payload: str\n\n\nclass _FakeStreamingStore:\n    def __init__(self, count: int, payload_size: int) -> None:\n        self._count = count\n        self._payload_size = payload_size\n\n    async def stream_ticks(self, run_id: str) -> AsyncIterator[StoredTick]:\n        for i in range(self._count):\n            yield self._make_stored_tick(i)\n\n    def _make_stored_tick(self, seq: int) -> StoredTick:\n        payload = f\"{seq:08d}\" + (\"x\" * (self._payload_size - 8))\n        tick = TickPublishEvent(event=_BigEvent(payload=payload))\n        return StoredTick(\n            run_id=\"r\",\n            sequence=seq,\n            timestamp=datetime.now(timezone.utc),\n            tick_data=tick.model_dump(),\n        )\n\n\ndef _empty_state() -> BrokerState:\n    return BrokerState(\n        is_running=True, config=BrokerConfig(steps={}, timeout=None), workers={}\n    )\n\n\n@pytest.mark.asyncio\nasync def test_stream_replay_peak_memory_bounded() -> None:\n    payload_size = 200_000  # ~200 KB per tick\n    total_ticks = 200\n\n    store = _FakeStreamingStore(count=total_ticks, payload_size=payload_size)\n\n    # Warm up caches (Pydantic TypeAdapter, class tables, etc.) before measuring.\n    async for stored in store.stream_ticks(\"r\"):\n        WorkflowTickAdapter.validate_python(stored.tick_data)\n        break\n\n    async def _stream() -> AsyncIterator:\n        async for stored in store.stream_ticks(\"r\"):\n            yield WorkflowTickAdapter.validate_python(stored.tick_data)\n\n    tracemalloc.start()\n    try:\n        await rebuild_state_from_ticks_stream(_empty_state(), _stream())\n        _current, peak = tracemalloc.get_traced_memory()\n    finally:\n        tracemalloc.stop()\n\n    # Bound: a few ticks worth, not the full history.\n    bound = payload_size * 10\n    assert peak < bound, (\n        f\"Peak memory {peak} bytes exceeded bound {bound} bytes \"\n        f\"(total_ticks={total_ticks}, payload_size={payload_size}).\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_stream_replay_memory_bound_detects_full_materialization() -> None:\n    payload_size = 200_000\n    total_ticks = 200\n\n    store = _FakeStreamingStore(count=total_ticks, payload_size=payload_size)\n\n    async def _all_then_stream() -> AsyncIterator:\n        materialized = [\n            WorkflowTickAdapter.validate_python(s.tick_data)\n            async for s in store.stream_ticks(\"r\")\n        ]\n        for t in materialized:\n            yield t\n\n    tracemalloc.start()\n    try:\n        await rebuild_state_from_ticks_stream(_empty_state(), _all_then_stream())\n        _current, peak = tracemalloc.get_traced_memory()\n    finally:\n        tracemalloc.stop()\n\n    bound = payload_size * 10\n    assert peak >= bound, (\n        f\"Expected full materialization to blow the bound ({peak} < {bound}).\"\n    )\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_workflow_service.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nimport pytest\nfrom llama_agents.server import (\n    HandlerQuery,\n    MemoryWorkflowStore,\n    PersistentHandler,\n    WorkflowServer,\n)\nfrom llama_agents.server._service import EventSendError, HandlerCompletedError\nfrom server_test_fixtures import (  # type: ignore[import]\n    ErrorWorkflow,\n    ExternalEvent,\n    wait_for_passing,\n    wait_for_requested_external_event,\n)\nfrom workflows import Workflow\n\n\n@pytest.mark.asyncio\nasync def test_cancel_running_handler(\n    memory_store: MemoryWorkflowStore, interactive_workflow: Workflow\n) -> None:\n    \"\"\"Start an interactive workflow, cancel it, and verify status becomes cancelled.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\n        \"interactive\", interactive_workflow, additional_events=[ExternalEvent]\n    )\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(\n            interactive_workflow, \"cancel-test-1\"\n        )\n        assert handler_data.run_id is not None\n\n        result = await server._service.cancel_handler(\"cancel-test-1\")\n        assert result == \"cancelled\"\n\n        async def status_is_cancelled() -> None:\n            persisted = await memory_store.query(\n                HandlerQuery(handler_id_in=[\"cancel-test-1\"])\n            )\n            assert len(persisted) == 1\n            assert persisted[0].status == \"cancelled\"\n\n        await wait_for_passing(status_is_cancelled, max_duration=2.0, interval=0.01)\n\n\n@pytest.mark.asyncio\nasync def test_cancel_handler_with_purge(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"Start and complete a workflow, then purge it from the store.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"simple\", simple_test_workflow)\n\n    async with server.contextmanager():\n        await server._service.start_workflow(simple_test_workflow, \"purge-test-1\")\n\n        # Wait for completion\n        async def handler_completed() -> None:\n            persisted = await memory_store.query(\n                HandlerQuery(handler_id_in=[\"purge-test-1\"])\n            )\n            assert len(persisted) == 1\n            assert persisted[0].status == \"completed\"\n\n        await wait_for_passing(handler_completed, max_duration=2.0, interval=0.01)\n\n        result = await server._service.cancel_handler(\"purge-test-1\", purge=True)\n        assert result == \"deleted\"\n\n        # Handler should be gone from store\n        persisted = await memory_store.query(\n            HandlerQuery(handler_id_in=[\"purge-test-1\"])\n        )\n        assert len(persisted) == 0\n\n\n@pytest.mark.asyncio\nasync def test_cancel_handler_not_found(memory_store: MemoryWorkflowStore) -> None:\n    \"\"\"Cancelling a nonexistent handler returns None.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n\n    async with server.contextmanager():\n        result = await server._service.cancel_handler(\"nonexistent\")\n        assert result is None\n\n\n@pytest.mark.asyncio\nasync def test_send_event_workflow_not_registered(\n    memory_store: MemoryWorkflowStore,\n) -> None:\n    \"\"\"Sending an event to a handler whose workflow is not registered raises EventSendError.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n\n    # Seed store with a handler for an unregistered workflow\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=\"orphan-handler\",\n            workflow_name=\"unregistered\",\n            status=\"running\",\n            run_id=\"some-run-id\",\n            started_at=datetime.now(timezone.utc),\n        )\n    )\n\n    async with server.contextmanager():\n        with pytest.raises(EventSendError, match=\"not registered\"):\n            await server._service.send_event(\n                \"orphan-handler\", ExternalEvent(response=\"hello\")\n            )\n\n\n@pytest.mark.asyncio\nasync def test_send_event_no_run_id(\n    memory_store: MemoryWorkflowStore, interactive_workflow: Workflow\n) -> None:\n    \"\"\"Sending an event to a handler with no run_id raises EventSendError.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\n        \"interactive\", interactive_workflow, additional_events=[ExternalEvent]\n    )\n\n    # Seed store with a handler that has no run_id\n    await memory_store.update(\n        PersistentHandler(\n            handler_id=\"no-run-handler\",\n            workflow_name=\"interactive\",\n            status=\"running\",\n            run_id=None,\n            started_at=datetime.now(timezone.utc),\n        )\n    )\n\n    async with server.contextmanager():\n        with pytest.raises(EventSendError, match=\"no run ID\"):\n            await server._service.send_event(\n                \"no-run-handler\", ExternalEvent(response=\"hello\")\n            )\n\n\n@pytest.mark.asyncio\nasync def test_start_workflow_happy_path(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"start_workflow returns HandlerData with correct initial fields.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"simple\", simple_test_workflow)\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(\n            simple_test_workflow, \"start-hp-1\"\n        )\n        assert handler_data.handler_id == \"start-hp-1\"\n        assert handler_data.workflow_name == \"simple\"\n        assert handler_data.run_id is not None\n        assert handler_data.status == \"running\"\n\n\n@pytest.mark.asyncio\nasync def test_await_workflow_happy_path(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"await_workflow returns completed HandlerData.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"simple\", simple_test_workflow)\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(\n            simple_test_workflow, \"await-hp-1\"\n        )\n        result = await server._service.await_workflow(handler_data)\n        assert result.status == \"completed\"\n        assert result.handler_id == \"await-hp-1\"\n\n\n@pytest.mark.asyncio\nasync def test_await_workflow_error_returns_failed(\n    memory_store: MemoryWorkflowStore,\n) -> None:\n    \"\"\"await_workflow on an ErrorWorkflow returns failed status, not an exception.\"\"\"\n    error_wf = ErrorWorkflow()\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"error\", error_wf)\n\n    async with server.contextmanager():\n        handler_data = await server._service.start_workflow(error_wf, \"await-err-1\")\n        result = await server._service.await_workflow(handler_data)\n        assert result.status == \"failed\"\n\n\n@pytest.mark.asyncio\nasync def test_resolve_handler_raises_on_completed(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"resolve_handler raises HandlerCompletedError for a terminal handler.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"simple\", simple_test_workflow)\n\n    async with server.contextmanager():\n        await server._service.start_workflow(simple_test_workflow, \"resolve-done-1\")\n\n        async def handler_completed() -> None:\n            persisted = await memory_store.query(\n                HandlerQuery(handler_id_in=[\"resolve-done-1\"])\n            )\n            assert len(persisted) == 1\n            assert persisted[0].status == \"completed\"\n\n        await wait_for_passing(handler_completed, max_duration=2.0, interval=0.01)\n\n        with pytest.raises(HandlerCompletedError):\n            await server._service.resolve_handler(\"resolve-done-1\")\n\n\n@pytest.mark.asyncio\nasync def test_send_event_happy_path(\n    memory_store: MemoryWorkflowStore, interactive_workflow: Workflow\n) -> None:\n    \"\"\"send_event delivers an event and the workflow completes.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\n        \"interactive\", interactive_workflow, additional_events=[ExternalEvent]\n    )\n\n    async with server.contextmanager():\n        await server._service.start_workflow(interactive_workflow, \"send-hp-1\")\n\n        await wait_for_requested_external_event(memory_store, \"send-hp-1\")\n\n        await server._service.send_event(\"send-hp-1\", ExternalEvent(response=\"pong\"))\n\n        async def handler_completed() -> None:\n            persisted = await memory_store.query(\n                HandlerQuery(handler_id_in=[\"send-hp-1\"])\n            )\n            assert len(persisted) == 1\n            assert persisted[0].status == \"completed\"\n\n        await wait_for_passing(handler_completed, max_duration=2.0, interval=0.01)\n\n\n@pytest.mark.asyncio\nasync def test_cancel_terminal_handler_without_purge(\n    memory_store: MemoryWorkflowStore, simple_test_workflow: Workflow\n) -> None:\n    \"\"\"cancel_handler on an already-completed handler without purge returns None.\"\"\"\n    server = WorkflowServer(workflow_store=memory_store, idle_timeout=0.01)\n    server.add_workflow(\"simple\", simple_test_workflow)\n\n    async with server.contextmanager():\n        await server._service.start_workflow(simple_test_workflow, \"cancel-term-1\")\n\n        async def handler_completed() -> None:\n            persisted = await memory_store.query(\n                HandlerQuery(handler_id_in=[\"cancel-term-1\"])\n            )\n            assert len(persisted) == 1\n            assert persisted[0].status == \"completed\"\n\n        await wait_for_passing(handler_completed, max_duration=2.0, interval=0.01)\n\n        result = await server._service.cancel_handler(\"cancel-term-1\", purge=False)\n        assert result is None\n\n        # Handler should still exist unchanged\n        persisted = await memory_store.query(\n            HandlerQuery(handler_id_in=[\"cancel-term-1\"])\n        )\n        assert len(persisted) == 1\n        assert persisted[0].status == \"completed\"\n"
  },
  {
    "path": "packages/llama-agents-server/tests/server/test_workflow_store_events.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.client.protocol.serializable_events import EventEnvelopeWithMetadata\nfrom llama_agents.server import (\n    AbstractWorkflowStore,\n    MemoryWorkflowStore,\n    SqliteWorkflowStore,\n)\nfrom llama_agents.server._store.abstract_workflow_store import StoredEvent\nfrom llama_agents_integration_tests.fake_agent_data import (\n    FakeAgentDataBackend,\n    create_agent_data_store,\n)\nfrom server_test_fixtures import wait_for_passing  # type: ignore[import]\nfrom workflows.events import (\n    Event,\n    StopEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n)\n\n\ndef make_envelope(\n    event: Event | None = None,\n    seq_label: int = 0,\n) -> EventEnvelopeWithMetadata:\n    \"\"\"Create an EventEnvelopeWithMetadata by serializing a real Event.\"\"\"\n    if event is None:\n        event = Event(data=f\"seq-{seq_label}\")\n    return EventEnvelopeWithMetadata.from_event(event, include_qualified_name=False)\n\n\ndef _stop(seq_label: int = 0) -> StopEvent:\n    return StopEvent(data=f\"seq-{seq_label}\")\n\n\ndef _failed() -> WorkflowFailedEvent:\n    return WorkflowFailedEvent(\n        step_name=\"test_step\",\n        exception=ValueError(\"boom\"),\n        attempts=1,\n        elapsed_seconds=0.0,\n    )\n\n\ndef _cancelled() -> WorkflowCancelledEvent:\n    return WorkflowCancelledEvent()\n\n\nasync def _subscribe_and_collect(\n    store: AbstractWorkflowStore,\n    run_id: str,\n    after_sequence: int | None = None,\n) -> tuple[list[StoredEvent], asyncio.Task[None]]:\n    \"\"\"Subscribe to events, returning the collected list and the consumer task.\"\"\"\n    collected: list[StoredEvent] = []\n\n    async def consumer() -> None:\n        kwargs = {} if after_sequence is None else {\"after_sequence\": after_sequence}\n        async for event in store.subscribe_events(run_id, **kwargs):\n            collected.append(event)\n\n    subscriber_queues = getattr(store, \"_subscriber_queues\", None)\n    existing_queue_count = (\n        len(subscriber_queues.get(run_id, ()))\n        if subscriber_queues is not None\n        else None\n    )\n    task = asyncio.create_task(consumer())\n\n    async def registered() -> None:\n        conditions = getattr(store, \"_conditions\", None)\n        if conditions is not None:\n            assert run_id in conditions\n            return\n\n        if subscriber_queues is not None:\n            assert existing_queue_count is not None\n            assert len(subscriber_queues.get(run_id, ())) > existing_queue_count\n            return\n\n        raise AssertionError(\"store does not expose subscription registration state\")\n\n    await wait_for_passing(registered, max_duration=2.0, interval=0.01)\n    return collected, task\n\n\n@pytest.fixture(params=[\"memory\", \"sqlite\", \"agent_data\"])\ndef store(\n    request: pytest.FixtureRequest, tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> AbstractWorkflowStore:\n    if request.param == \"memory\":\n        return MemoryWorkflowStore()\n    elif request.param == \"sqlite\":\n        return SqliteWorkflowStore(str(tmp_path / \"test.sqlite\"), poll_interval=0.05)\n    else:\n        return create_agent_data_store(FakeAgentDataBackend(), monkeypatch)\n\n\n@pytest.mark.asyncio\nasync def test_append_single_event_and_query_it_back(\n    store: AbstractWorkflowStore,\n) -> None:\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n\n    result = await store.query_events(\"run-1\")\n    assert len(result) == 1\n    assert result[0].run_id == \"run-1\"\n    assert result[0].sequence == 0\n    assert result[0].event.type == \"Event\"\n\n\n@pytest.mark.asyncio\nasync def test_append_multiple_events_and_query_all(\n    store: AbstractWorkflowStore,\n) -> None:\n    for i in range(5):\n        await store.append_event(\"run-1\", make_envelope(seq_label=i))\n\n    result = await store.query_events(\"run-1\")\n    assert len(result) == 5\n    assert [e.sequence for e in result] == [0, 1, 2, 3, 4]\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\n    \"seed_count, after_sequence, limit, expected_sequences\",\n    [\n        pytest.param(5, 2, None, [3, 4], id=\"after_sequence_only\"),\n        pytest.param(5, None, 3, [0, 1, 2], id=\"limit_only\"),\n        pytest.param(10, 3, 2, [4, 5], id=\"after_sequence_and_limit\"),\n    ],\n)\nasync def test_query_events_with_filters(\n    store: AbstractWorkflowStore,\n    seed_count: int,\n    after_sequence: int | None,\n    limit: int | None,\n    expected_sequences: list[int],\n) -> None:\n    for i in range(seed_count):\n        await store.append_event(\"run-1\", make_envelope(seq_label=i))\n\n    result = await store.query_events(\n        \"run-1\", after_sequence=after_sequence, limit=limit\n    )\n    assert len(result) == len(expected_sequences)\n    assert [e.sequence for e in result] == expected_sequences\n\n\n@pytest.mark.asyncio\nasync def test_query_events_for_nonexistent_run_id_returns_empty(\n    store: AbstractWorkflowStore,\n) -> None:\n    result = await store.query_events(\"nonexistent-run\")\n    assert result == []\n\n\n@pytest.mark.asyncio\nasync def test_events_from_different_run_ids_are_isolated(\n    store: AbstractWorkflowStore,\n) -> None:\n    for i in range(3):\n        await store.append_event(\"run-a\", make_envelope(seq_label=i))\n    for i in range(2):\n        await store.append_event(\"run-b\", make_envelope(seq_label=i))\n\n    result_a = await store.query_events(\"run-a\")\n    result_b = await store.query_events(\"run-b\")\n\n    assert len(result_a) == 3\n    assert all(e.run_id == \"run-a\" for e in result_a)\n\n    assert len(result_b) == 2\n    assert all(e.run_id == \"run-b\" for e in result_b)\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_receives_appended_events(\n    store: AbstractWorkflowStore,\n) -> None:\n    \"\"\"Appending events wakes a waiting subscriber.\"\"\"\n    collected, task = await _subscribe_and_collect(store, \"run-1\")\n\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n    await store.append_event(\"run-1\", make_envelope(event=_stop(seq_label=2)))\n\n    await asyncio.wait_for(task, timeout=5.0)\n\n    assert len(collected) == 3\n    assert [e.sequence for e in collected] == [0, 1, 2]\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\n    \"terminal_event, expected_type\",\n    [\n        pytest.param(_failed(), \"WorkflowFailedEvent\", id=\"failed\"),\n        pytest.param(_cancelled(), \"WorkflowCancelledEvent\", id=\"cancelled\"),\n    ],\n)\nasync def test_subscribe_events_terminates_on_terminal_event(\n    store: AbstractWorkflowStore,\n    terminal_event: Event,\n    expected_type: str,\n) -> None:\n    \"\"\"Subscriber terminates after receiving a terminal event.\"\"\"\n    collected, task = await _subscribe_and_collect(store, \"run-1\")\n\n    if expected_type == \"WorkflowFailedEvent\":\n        await store.append_event(\"run-1\", make_envelope())\n    await store.append_event(\"run-1\", make_envelope(event=terminal_event))\n\n    await asyncio.wait_for(task, timeout=5.0)\n\n    assert collected[-1].event.type == expected_type\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_multiple_concurrent_subscribers(\n    store: AbstractWorkflowStore,\n) -> None:\n    \"\"\"Multiple concurrent subscribers on the same run each receive all events.\"\"\"\n    collected_a, task_a = await _subscribe_and_collect(store, \"run-1\")\n    collected_b, task_b = await _subscribe_and_collect(store, \"run-1\")\n\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n    await store.append_event(\"run-1\", make_envelope(event=_stop(seq_label=2)))\n\n    await asyncio.wait_for(asyncio.gather(task_a, task_b), timeout=5.0)\n\n    assert len(collected_a) == 3\n    assert len(collected_b) == 3\n    assert [e.sequence for e in collected_a] == [0, 1, 2]\n    assert [e.sequence for e in collected_b] == [0, 1, 2]\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_with_after_sequence(\n    store: AbstractWorkflowStore,\n) -> None:\n    \"\"\"Subscriber can resume from a specific sequence position.\"\"\"\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(seq_label=1))\n    await store.append_event(\"run-1\", make_envelope(seq_label=2))\n\n    collected, task = await _subscribe_and_collect(store, \"run-1\", after_sequence=1)\n\n    await store.append_event(\"run-1\", make_envelope(event=_stop(seq_label=3)))\n\n    await asyncio.wait_for(task, timeout=5.0)\n\n    # Should have events 2, 3 (skipping 0 and 1)\n    assert len(collected) == 2\n    assert [e.sequence for e in collected] == [2, 3]\n\n\n@pytest.mark.asyncio\nasync def test_subscribe_events_already_terminated(\n    store: AbstractWorkflowStore,\n) -> None:\n    \"\"\"If events already contain a terminal event, subscriber terminates immediately.\"\"\"\n    await store.append_event(\"run-1\", make_envelope(seq_label=0))\n    await store.append_event(\"run-1\", make_envelope(event=_stop(seq_label=1)))\n\n    collected: list[StoredEvent] = []\n    async for event in store.subscribe_events(\"run-1\"):\n        collected.append(event)\n\n    assert len(collected) == 2\n    assert collected[-1].event.type == \"StopEvent\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_ticks_yields_all_rows_in_order(\n    store: AbstractWorkflowStore,\n) -> None:\n    for i in range(13):\n        await store.append_tick(\"run-1\", {\"type\": \"TickSendEvent\", \"i\": i})\n\n    yielded: list[int] = []\n    async for tick in store.stream_ticks(\"run-1\"):\n        yielded.append(tick.sequence)\n\n    assert yielded == list(range(13))\n\n\n@pytest.mark.asyncio\nasync def test_stream_ticks_empty_history(\n    store: AbstractWorkflowStore,\n) -> None:\n    yielded: list[object] = []\n    async for tick in store.stream_ticks(\"empty-run\"):\n        yielded.append(tick)\n    assert yielded == []\n"
  },
  {
    "path": "packages/llama-index-utils-workflow/.gitignore",
    "content": "test*.html\n"
  },
  {
    "path": "packages/llama-index-utils-workflow/CHANGELOG.md",
    "content": "# llama-index-utils-workflow\n\n## 0.10.1\n\n### Patch Changes\n\n- 979d68b: Fix draw_all_possible_flows to accept workflow classes\n\n## 0.10.0\n\n### Minor Changes\n\n- b32ec53: Drop python 3.9 support\n\n## 0.9.5\n\n### Patch Changes\n\n- Updated dependencies [5e7f9e5]\n- Updated dependencies [9f26314]\n  - llama-index-workflows@2.16.0\n\n## 0.9.4\n\n### Patch Changes\n\n- Updated dependencies [6ec262c]\n  - llama-index-workflows@2.15.1\n\n## 0.9.3\n\n### Patch Changes\n\n- Updated dependencies [77a3f9c]\n- Updated dependencies [707a254]\n- Updated dependencies [05f5f4e]\n- Updated dependencies [3c22216]\n- Updated dependencies [96e437e]\n  - llama-index-workflows@2.15.0\n\n## 0.9.3-rc.1\n\n### Patch Changes\n\n- Updated dependencies [3720c61]\n- Updated dependencies [a2aad32]\n  - llama-index-workflows@2.15.0-rc.1\n\n## 0.9.3-rc.0\n\n### Patch Changes\n\n- Updated dependencies [e981f73]\n- Updated dependencies [b515a46]\n- Updated dependencies [7433d4c]\n  - llama-index-workflows@2.15.0-rc.0\n\n## 0.9.2\n\n### Patch Changes\n\n- Updated dependencies [3590913]\n- Updated dependencies [7433d4c]\n  - llama-index-workflows@2.14.2\n\n## 0.9.1\n\n### Patch Changes\n\n- Updated dependencies [6ece797]\n  - llama-index-workflows@2.14.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 73c1254: refactor: expand runtime plugin architecture\n\n  - Refactoring to better support alternate distributed backends\n  - Some `Context` methods may now raise errors if used in an unexpected context\n  - `WorkflowHandler` is no longer a future. Retains compatibility methods for main use cases (exception, cancel, etc)\n\n### Patch Changes\n\n- db90f89: Separate server/client to their own packages under a llama_agents namespace\n- 33bbd23: Read workflows from globals rather than sys modules to facilitate more robust/correct class loading\n- 0e826b1: Added in the utility function draw_all_possible_flows_nested_mermaid. This draws the possible flows (not execution) of nested workflows\n- Updated dependencies [73c1254]\n- Updated dependencies [45e7614]\n- Updated dependencies [45e7614]\n- Updated dependencies [2900f58]\n- Updated dependencies [6fdc45c]\n  - llama-index-workflows@2.14.0\n\n## 0.8.0\n\n### Minor Changes\n\n- 6dd7fc0: Add resource config node support to workflow representation\n\n## 0.7.1\n\n### Patch Changes\n\n- 40be1c7: add workflow class name to WorkflowGraph representation\n\n## 0.7.0\n\n### Minor Changes\n\n- e53c654: Add further detail to workflow graph, mainly adding `Resource` nodes to workflow graph and visualizations\n- 0d72b4d: reorganize workflow graph representation types\n\n## 0.6.0\n\n### Minor Changes\n\n- 96fd9c9: Added new function draw_most_recent_execution_mermaid\n\n  To draw the most recent workflow run in a mermaid format\n\n## 0.5.2\n\n### Patch Changes\n\n- 8e84276: Increase minimum llama-index-workflows version\n\n## 0.5.1\n\n### Patch Changes\n\n- f307253: Update typechecking to support ty\n- 91159d7: Moving `_extract_workflow_structure` to its own module in workflow core\n- 300fd05: Add stricter ruff formatting checks\n- 32ae78a: Switch build backend to uv\n"
  },
  {
    "path": "packages/llama-index-utils-workflow/README.md",
    "content": "# LlamaIndex Utils: Workflow\n\nUtilities for LlamaIndex workflows, including visualization tools.\n\n```bash\npip install llama-index-utils-workflow\n```\n\n## Features\n\n- **Workflow visualization** with `draw_all_possible_flows()`\n- **Latest execution visualization** with `draw_most_recent_execution()`\n- **Label truncation support** for better readability with long event names\n\n## Usage\n\n```python\nfrom llama_index.utils.workflow import draw_all_possible_flows\n\n# Basic workflow visualization\ndraw_all_possible_flows(my_workflow, \"workflow.html\")\n\n# With label truncation for long event names (v0.4.0+)\ndraw_all_possible_flows(my_workflow, \"workflow.html\", max_label_length=15)\n\n# Latest execution visualization\nhandler = my_workflow.run()\nawait handler\ndraw_most_recent_execution(handler, \"workflow.html\")\n```\n"
  },
  {
    "path": "packages/llama-index-utils-workflow/package.json",
    "content": "{\n  \"name\": \"llama-index-utils-workflow\",\n  \"version\": \"0.10.1\",\n  \"private\": false,\n  \"license\": \"MIT\",\n  \"scripts\": {},\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "packages/llama-index-utils-workflow/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.9.10,<0.10.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"pre-commit>=4.3.0\",\n  \"pytest>=8.4.2\",\n  \"pytest-asyncio>=1.0.0\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\"\n]\n\n[project]\nname = \"llama-index-utils-workflow\"\nversion = \"0.10.1\"\ndescription = \"llama-index utils for workflows\"\nreadme = \"README.md\"\nauthors = [{name = \"Adrian Lyjak\", email = \"adrianlyjak@gmail.com\"}]\nrequires-python = \">=3.10\"\ndependencies = [\n  \"llama-index-core>=0.14,<0.15.0\",\n  \"llama-index-workflows>=2.13.0,<3.0.0\",\n  \"pyvis>=0.3.2\"\n]\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"module\"\nasyncio_default_test_loop_scope = \"module\"\ntestpaths = [\"tests\"]\naddopts = \"-nauto --timeout=10\"\n\n[tool.uv.build-backend]\nmodule-name = \"llama_index.utils.workflow\"\n"
  },
  {
    "path": "packages/llama-index-utils-workflow/tests/conftest.py",
    "content": "from typing import Annotated\n\nimport pytest\nfrom pydantic import Field\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.resource import Resource\nfrom workflows.workflow import Workflow\n\n\nclass OneTestEvent(Event):\n    test_param: str = Field(default=\"test\")\n\n\nclass AnotherTestEvent(Event):\n    another_test_param: str = Field(default=\"another_test\")\n\n\nclass LastEvent(Event):\n    pass\n\n\nclass DummyWorkflow(Workflow):\n    @step()\n    async def start_step(self, ev: StartEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    @step()\n    async def middle_step(self, ev: OneTestEvent) -> LastEvent:\n        return LastEvent()\n\n    @step()\n    async def end_step(self, ev: LastEvent) -> StopEvent:\n        return StopEvent(result=\"Workflow completed\")\n\n\n# --- Resource-based workflow for testing ---\n\n\nclass DatabaseClient:\n    \"\"\"A mock database client for testing resources.\"\"\"\n\n    pass\n\n\ndef get_database_client() -> DatabaseClient:\n    \"\"\"Factory function to create a database client.\n\n    This is a test docstring that should appear in the resource metadata.\n    \"\"\"\n    return DatabaseClient()\n\n\nclass CacheClient:\n    \"\"\"A mock cache client for testing resources.\"\"\"\n\n    pass\n\n\ndef get_cache_client() -> CacheClient:\n    \"\"\"Factory function to create a cache client.\"\"\"\n    return CacheClient()\n\n\nclass ResourceWorkflow(Workflow):\n    \"\"\"A workflow with resource dependencies for testing visualization.\"\"\"\n\n    @step()\n    async def start_step(self, ev: StartEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    @step()\n    async def step_with_db(\n        self,\n        ev: OneTestEvent,\n        db_client: Annotated[DatabaseClient, Resource(get_database_client)],\n    ) -> LastEvent:\n        return LastEvent()\n\n    @step()\n    async def step_with_both_resources(\n        self,\n        ev: LastEvent,\n        db: Annotated[DatabaseClient, Resource(get_database_client)],\n        cache: Annotated[CacheClient, Resource(get_cache_client)],\n    ) -> StopEvent:\n        return StopEvent(result=\"Workflow completed\")\n\n\n@pytest.fixture()\ndef workflow() -> Workflow:\n    return DummyWorkflow()\n\n\n@pytest.fixture()\ndef workflow_with_resources() -> Workflow:\n    return ResourceWorkflow()\n\n\n@pytest.fixture()\ndef events() -> list[type[Event]]:\n    return [OneTestEvent, AnotherTestEvent]\n\n\n# --- Nested workflow for testing ---\n\n\nclass ChildWorkflowA(Workflow):\n    @step()\n    async def child_start(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"Child processed\")\n\n\nclass ParentWorkflow(Workflow):\n    @step()\n    async def parent_start(self, ev: StartEvent) -> Event:\n        # Instantiate the nested workflow. The drawing function will inspect this.\n        child_wf = ChildWorkflowA()\n        # The actual run logic is not important for drawing.\n        _ = await child_wf.run(input=\"dummy\")\n        return Event(result=\"some result\")\n\n    @step()\n    async def parent_end(self, ev: Event) -> StopEvent:\n        return StopEvent(result=\"Final Result\")\n\n\n@pytest.fixture()\ndef nested_workflow() -> Workflow:\n    return ParentWorkflow()\n"
  },
  {
    "path": "packages/llama-index-utils-workflow/tests/test_drawing.py",
    "content": "import json\nfrom pathlib import Path\nfrom typing import Annotated\nfrom unittest.mock import MagicMock, mock_open, patch\n\nimport pytest\nfrom conftest import DummyWorkflow, ParentWorkflow, ResourceWorkflow\nfrom llama_index.utils.workflow import (\n    draw_all_possible_flows,\n    draw_all_possible_flows_mermaid,\n    draw_most_recent_execution,\n    draw_most_recent_execution_mermaid,\n)\nfrom pydantic import BaseModel\nfrom workflows.decorators import step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.resource import Resource, ResourceConfig\nfrom workflows.workflow import Workflow\n\n\n@pytest.mark.asyncio\nasync def test_workflow_draw_methods(workflow: Workflow) -> None:\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        draw_all_possible_flows(workflow, filename=\"test_all_flows.html\")\n        mock_network.assert_called_once()\n        mock_network.return_value.show.assert_called_once_with(\n            \"test_all_flows.html\", notebook=False\n        )\n\n    handler = workflow.run()\n    await handler\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        draw_most_recent_execution(handler, filename=\"test_recent_execution.html\")\n        mock_network.assert_called_once()\n        mock_network.return_value.show.assert_called_once_with(\n            \"test_recent_execution.html\", notebook=False\n        )\n\n\ndef test_draw_all_possible_flows_with_max_label_length(\n    workflow: Workflow,\n) -> None:\n    \"\"\"Test the max_label_length parameter.\"\"\"\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        mock_net_instance = MagicMock()\n        mock_network.return_value = mock_net_instance\n\n        # Test with max_label_length=10\n        draw_all_possible_flows(\n            workflow, filename=\"test_truncated.html\", max_label_length=10\n        )\n\n        # Extract actual label mappings from add_node calls\n        label_mappings = {}\n        for call in mock_net_instance.add_node.call_args_list:\n            _, kwargs = call\n            label = kwargs.get(\"label\")\n            title = kwargs.get(\"title\")\n\n            # For items with titles (truncated), map title->label\n            if title:\n                label_mappings[title] = label\n            # For items without titles (not truncated), map label->label\n            elif label:\n                label_mappings[label] = label\n\n        # Test cases using actual events from DummyWorkflow fixture\n        test_cases = [\n            (\"OneTestEvent\", \"OneTestEv*\"),  # 12 chars -> truncated to 10\n            (\"LastEvent\", \"LastEvent\"),  # 9 chars -> no truncation\n            (\n                \"StartEvent\",\n                \"StartEvent\",\n            ),  # 10 chars -> no truncation (exactly at limit)\n            (\"StopEvent\", \"StopEvent\"),  # 9 chars -> no truncation\n        ]\n\n        # Verify actual results match expected for available test cases\n        for original, expected_label in test_cases:\n            if original in label_mappings:\n                actual_label = label_mappings[original]\n                assert actual_label == expected_label, (\n                    f\"Expected '{original}' to become '{expected_label}', but got '{actual_label}'\"\n                )\n                assert len(actual_label) <= 10, (\n                    f\"Label '{actual_label}' exceeds max_label_length=10\"\n                )\n\n\ndef test_draw_all_possible_flows_mermaid_basic(workflow: Workflow) -> None:\n    \"\"\"Test basic Mermaid diagram generation.\"\"\"\n    with patch(\"builtins.open\", mock_open()) as mock_file:\n        result = draw_all_possible_flows_mermaid(\n            workflow, filename=\"test_mermaid.mermaid\"\n        )\n\n        # Verify file was written\n        mock_file.assert_called_once_with(\"test_mermaid.mermaid\", \"w\")\n\n        # Verify basic structure\n        assert isinstance(result, str)\n        assert result.startswith(\"flowchart TD\")\n\n        # Verify contains style definitions\n        assert \"classDef stepStyle fill:#ADD8E6\" in result\n        assert \"classDef startEventStyle fill:#E27AFF\" in result\n        assert \"classDef stopEventStyle fill:#FFA07A\" in result\n        assert \"classDef defaultEventStyle fill:#90EE90\" in result\n        assert \"classDef externalStyle fill:#BEDAE4\" in result\n\n\ndef test_draw_all_possible_flows_mermaid_no_file(workflow: Workflow) -> None:\n    \"\"\"Test Mermaid diagram generation without file output.\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow)\n\n    # Should still return the diagram string\n    assert isinstance(result, str)\n    assert result.startswith(\"flowchart TD\")\n\n\ndef test_mermaid_node_shapes_and_styles(workflow: Workflow) -> None:\n    \"\"\"Test that Mermaid nodes have correct shapes and styles.\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow)\n\n    lines = result.split(\"\\n\")\n\n    # Check for step nodes (should use box shape [...] and stepStyle)\n    step_nodes = [line for line in lines if \"step_\" in line and \":::stepStyle\" in line]\n    for step_line in step_nodes:\n        assert \"[\" in step_line and \"]\" in step_line, (\n            f\"Step node should use box shape: {step_line}\"\n        )\n        assert \":::stepStyle\" in step_line, (\n            f\"Step node should use stepStyle: {step_line}\"\n        )\n\n    # Check for event nodes (should use ellipse shape ([...]) and event styles)\n    event_nodes = [\n        line\n        for line in lines\n        if \"event_\" in line\n        and (\n            \":::startEventStyle\" in line\n            or \":::stopEventStyle\" in line\n            or \":::defaultEventStyle\" in line\n        )\n    ]\n    for event_line in event_nodes:\n        assert \"([\" in event_line and \")\" in event_line, (\n            f\"Event node should use ellipse shape: {event_line}\"\n        )\n\n\ndef test_mermaid_edges_generation(workflow: Workflow) -> None:\n    \"\"\"Test that Mermaid edges are properly generated.\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow)\n\n    lines = result.split(\"\\n\")\n    edge_lines = [line.strip() for line in lines if \" --> \" in line]\n\n    # Should have at least some edges\n    assert len(edge_lines) > 0, \"Should generate at least some edges\"\n\n    # All edge lines should follow the pattern: source --> target\n    for edge_line in edge_lines:\n        assert edge_line.count(\" --> \") == 1, (\n            f\"Edge should have exactly one arrow: {edge_line}\"\n        )\n        source, target = edge_line.split(\" --> \")\n        assert source.strip(), f\"Edge source should not be empty: {edge_line}\"\n        assert target.strip(), f\"Edge target should not be empty: {edge_line}\"\n\n\ndef test_mermaid_id_cleaning(workflow: Workflow) -> None:\n    \"\"\"Test that Mermaid IDs are properly cleaned for validity.\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow)\n\n    lines = result.split(\"\\n\")\n\n    # Check that all node IDs are valid (no spaces, special chars that would break Mermaid)\n    for line in lines:\n        if line.strip().startswith((\"step_\", \"event_\", \"external_step\")):\n            # Extract the ID (first word)\n            parts = line.strip().split()\n            if parts:\n                node_id = parts[0]\n                # Should not contain spaces, dots, or hyphens\n                assert \" \" not in node_id, (\n                    f\"Node ID should not contain spaces: {node_id}\"\n                )\n                assert \".\" not in node_id, f\"Node ID should not contain dots: {node_id}\"\n                # Note: We allow underscores as they're valid in Mermaid\n\n\ndef test_mermaid_vs_pyvis_consistency(workflow: Workflow) -> None:\n    \"\"\"Test that Mermaid and Pyvis generate consistent node/edge counts.\"\"\"\n    # Generate Pyvis version\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        mock_net_instance = MagicMock()\n        mock_network.return_value = mock_net_instance\n\n        draw_all_possible_flows(workflow, filename=\"test.html\")\n\n        # Count unique nodes (Pyvis deduplicates automatically)\n        pyvis_unique_nodes = set()\n        for call in mock_net_instance.add_node.call_args_list:\n            args, kwargs = call\n            node_id = args[0]  # First argument is the node ID\n            pyvis_unique_nodes.add(node_id)\n\n        pyvis_edge_calls = len(mock_net_instance.add_edge.call_args_list)\n\n    # Generate Mermaid version\n    mermaid_result = draw_all_possible_flows_mermaid(workflow)\n    lines = mermaid_result.split(\"\\n\")\n\n    # Count Mermaid nodes (lines with node definitions, but NOT edge lines)\n    mermaid_node_lines = []\n    for line in lines:\n        line = line.strip()\n        if not line:\n            continue\n        # Node lines start with step_, event_, or external_step BUT don't contain -->\n        if \" --> \" not in line and line.startswith(\n            (\"step_\", \"event_\", \"external_step\")\n        ):\n            mermaid_node_lines.append(line)\n\n    # Count Mermaid edges (lines with arrows)\n    mermaid_edge_lines = [line for line in lines if \" --> \" in line]\n\n    # Should have same number of nodes and edges\n    assert len(mermaid_node_lines) == len(pyvis_unique_nodes), (\n        f\"Mermaid nodes ({len(mermaid_node_lines)}) should match Pyvis unique nodes ({len(pyvis_unique_nodes)})\"\n    )\n    assert len(mermaid_edge_lines) == pyvis_edge_calls, (\n        f\"Mermaid edges ({len(mermaid_edge_lines)}) should match Pyvis edges ({pyvis_edge_calls})\"\n    )\n\n\ndef test_mermaid_file_writing(workflow: Workflow) -> None:\n    \"\"\"Test that Mermaid diagram is correctly written to file.\"\"\"\n    mock_file_handle = mock_open()\n\n    with patch(\"builtins.open\", mock_file_handle):\n        result = draw_all_possible_flows_mermaid(\n            workflow, filename=\"test_output.mermaid\"\n        )\n\n        # Verify file was opened for writing\n        mock_file_handle.assert_called_once_with(\"test_output.mermaid\", \"w\")\n\n        # Verify content was written\n        written_content = \"\".join(\n            call.args[0] for call in mock_file_handle().write.call_args_list\n        )\n\n        assert written_content == result, \"File content should match returned string\"\n        assert written_content.startswith(\"flowchart TD\"), (\n            \"File should contain valid Mermaid syntax\"\n        )\n\n\ndef test_mermaid_empty_filename(workflow: Workflow) -> None:\n    \"\"\"Test that Mermaid works with empty/None filename.\"\"\"\n    # Test without filename (defaults internally)\n    result1 = draw_all_possible_flows_mermaid(workflow)\n    assert isinstance(result1, str)\n    assert result1.startswith(\"flowchart TD\")\n\n    # Test with empty string\n    result2 = draw_all_possible_flows_mermaid(workflow, filename=\"\")\n    assert isinstance(result2, str)\n    assert result2.startswith(\"flowchart TD\")\n\n    # Both should be identical\n    assert result1 == result2\n\n\n@pytest.mark.asyncio\nasync def test_draw_most_recent_execution_mermaid(workflow: Workflow) -> None:\n    \"\"\"Test Mermaid diagram generation for the most recent execution.\"\"\"\n    handler = workflow.run()\n    await handler\n\n    with patch(\"builtins.open\", mock_open()) as mock_file:\n        result = draw_most_recent_execution_mermaid(\n            handler, filename=\"test_recent.mermaid\"\n        )\n\n        # Verify file was written\n        mock_file.assert_called_once_with(\"test_recent.mermaid\", \"w\")\n\n        # Verify basic structure\n        assert isinstance(result, str)\n        assert result.startswith(\"flowchart TD\")\n\n        # Verify it contains style definitions\n        assert \"classDef stepStyle fill:#ADD8E6\" in result\n        assert \"classDef startEventStyle fill:#E27AFF\" in result\n        assert \"classDef stopEventStyle fill:#FFA07A\" in result\n        assert \"classDef defaultEventStyle fill:#90EE90\" in result\n        assert \"classDef externalStyle fill:#BEDAE4\" in result\n\n        # Verify it contains nodes and edges\n        lines = result.split(\"\\n\")\n        node_lines = [line for line in lines if \":::\" in line]\n        edge_lines = [line for line in lines if \" --> \" in line]\n        assert len(node_lines) > 0\n        assert len(edge_lines) > 0\n\n\n# --- Resource node rendering tests ---\n\n\ndef test_mermaid_resource_nodes_rendered(\n    workflow_with_resources: Workflow,\n) -> None:\n    \"\"\"Test that resource nodes are rendered in Mermaid output.\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow_with_resources)\n\n    # Verify resource style is defined\n    assert \"classDef resourceStyle fill:#DDA0DD\" in result\n\n    # Verify resource nodes are present (hexagon shape with {{}})\n    lines = result.split(\"\\n\")\n    resource_lines = [line for line in lines if \"resource_\" in line and \":::\" in line]\n    assert len(resource_lines) > 0\n\n    # Check resource nodes use hexagon shape\n    for line in resource_lines:\n        assert \"{{\" in line and \"}}\" in line, (\n            f\"Resource node should use hexagon shape: {line}\"\n        )\n        assert \":::resourceStyle\" in line, (\n            f\"Resource node should use resourceStyle: {line}\"\n        )\n\n\ndef test_mermaid_resource_edges_have_labels(\n    workflow_with_resources: Workflow,\n) -> None:\n    \"\"\"Test that edges from resources to steps have labels (variable names).\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow_with_resources)\n\n    lines = result.split(\"\\n\")\n    # Look for edges with labels: resource_xxx -->|\"var_name\"| step_yyy\n    labeled_edge_lines = [line for line in lines if '-->|\"' in line]\n\n    # Should have labeled edges for resource connections\n    assert len(labeled_edge_lines) > 0\n\n    # Check that the labels are variable names\n    expected_labels = {\"db_client\", \"db\", \"cache\"}\n    found_labels = set()\n    for line in labeled_edge_lines:\n        # Extract label from -->|\"label\"|\n        if '-->|\"' in line:\n            start = line.index('-->|\"') + 5\n            end = line.index('\"|', start)\n            label = line[start:end]\n            found_labels.add(label)\n\n    assert found_labels.intersection(expected_labels), (\n        f\"Expected some of {expected_labels}, found {found_labels}\"\n    )\n\n\ndef test_pyvis_resource_nodes_rendered(workflow_with_resources: Workflow) -> None:\n    \"\"\"Test that resource nodes are rendered in Pyvis output.\"\"\"\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        mock_net_instance = MagicMock()\n        mock_network.return_value = mock_net_instance\n\n        draw_all_possible_flows(workflow_with_resources, filename=\"test.html\")\n\n        # Extract all add_node calls\n        node_calls = mock_net_instance.add_node.call_args_list\n\n        # Find resource nodes (should have hexagon shape and plum color)\n        resource_nodes = []\n        for call in node_calls:\n            args, kwargs = call\n            node_id = args[0]\n            if \"resource_\" in node_id:\n                resource_nodes.append((node_id, kwargs))\n\n        assert len(resource_nodes) > 0, \"Should have resource nodes\"\n\n        for node_id, kwargs in resource_nodes:\n            assert kwargs.get(\"shape\") == \"hexagon\", (\n                f\"Resource node {node_id} should be hexagon\"\n            )\n            assert kwargs.get(\"color\") == \"#DDA0DD\", (\n                f\"Resource node {node_id} should be plum color\"\n            )\n            # Should have a title with metadata\n            assert kwargs.get(\"title\") is not None, (\n                f\"Resource node {node_id} should have title\"\n            )\n\n\ndef test_pyvis_resource_edges_have_labels(\n    workflow_with_resources: Workflow,\n) -> None:\n    \"\"\"Test that Pyvis edges from resources have labels.\"\"\"\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        mock_net_instance = MagicMock()\n        mock_network.return_value = mock_net_instance\n\n        draw_all_possible_flows(workflow_with_resources, filename=\"test.html\")\n\n        # Extract all add_edge calls\n        edge_calls = mock_net_instance.add_edge.call_args_list\n\n        # Find edges with labels\n        labeled_edges = []\n        for call in edge_calls:\n            args, kwargs = call\n            if \"label\" in kwargs:\n                labeled_edges.append((args, kwargs[\"label\"]))\n\n        assert len(labeled_edges) > 0, \"Should have labeled edges\"\n\n        # Check that labels are variable names\n        labels = {label for _, label in labeled_edges}\n        expected_labels = {\"db_client\", \"db\", \"cache\"}\n        assert labels.intersection(expected_labels), (\n            f\"Expected some of {expected_labels}, found {labels}\"\n        )\n\n\ndef test_mermaid_resource_style_always_defined(workflow: Workflow) -> None:\n    \"\"\"Test that resourceStyle is always defined even for workflows without resources.\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow)\n\n    # resourceStyle should be defined even if not used\n    assert \"classDef resourceStyle fill:#DDA0DD\" in result\n\n\ndef test_mermaid_resource_config_nodes_rendered(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that resource config nodes are rendered in Mermaid output.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"setting\": \"test\", \"value\": 1}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(config_data, f)\n\n    class ConfigData(BaseModel):\n        setting: str\n        value: int\n\n    class Client:\n        pass\n\n    def get_client(\n        config: Annotated[ConfigData, ResourceConfig(config_file=\"config.json\")],\n    ) -> Client:\n        return Client()\n\n    class WorkflowWithConfig(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            client: Annotated[Client, Resource(get_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    result = draw_all_possible_flows_mermaid(WorkflowWithConfig())\n\n    assert \"classDef resourceConfigStyle fill:#B2DFDB\" in result\n    lines = result.split(\"\\n\")\n    resource_config_lines = [\n        line\n        for line in lines\n        if \"resource_config_\" in line and \":::\" in line and \" --> \" not in line\n    ]\n    assert len(resource_config_lines) == 1\n    assert \":::resourceConfigStyle\" in resource_config_lines[0]\n\n\ndef test_pyvis_resource_config_nodes_rendered(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that resource config nodes are rendered in Pyvis output.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"setting\": \"test\", \"value\": 1}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(config_data, f)\n\n    class ConfigData(BaseModel):\n        setting: str\n        value: int\n\n    class Client:\n        pass\n\n    def get_client(\n        config: Annotated[ConfigData, ResourceConfig(config_file=\"config.json\")],\n    ) -> Client:\n        return Client()\n\n    class WorkflowWithConfig(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            client: Annotated[Client, Resource(get_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        mock_net_instance = MagicMock()\n        mock_network.return_value = mock_net_instance\n\n        draw_all_possible_flows(WorkflowWithConfig(), filename=\"test.html\")\n\n        node_calls = mock_net_instance.add_node.call_args_list\n        resource_config_nodes = []\n        for call in node_calls:\n            args, kwargs = call\n            node_id = args[0]\n            if \"resource_config_\" in node_id:\n                resource_config_nodes.append((node_id, kwargs))\n\n        assert len(resource_config_nodes) == 1, \"Should have resource config node\"\n        node_id, kwargs = resource_config_nodes[0]\n        assert kwargs.get(\"shape\") == \"box\", (\n            f\"Resource config node {node_id} should be box\"\n        )\n        assert kwargs.get(\"color\") == \"#B2DFDB\", (\n            f\"Resource config node {node_id} should be light teal\"\n        )\n\n\ndef test_resource_node_deduplication_in_rendering(\n    workflow_with_resources: Workflow,\n) -> None:\n    \"\"\"Test that deduplicated resource nodes render correctly.\"\"\"\n    result = draw_all_possible_flows_mermaid(workflow_with_resources)\n\n    lines = result.split(\"\\n\")\n\n    # Count unique resource node definitions (not edges)\n    resource_node_defs = [\n        line\n        for line in lines\n        if \"resource_\" in line\n        and \":::\" in line\n        and \" --> \" not in line\n        and \"-->|\" not in line\n    ]\n\n    # The workflow has 2 unique resources (DatabaseClient used twice, CacheClient once)\n    # So we should see exactly 2 resource node definitions\n    assert len(resource_node_defs) == 2, (\n        f\"Expected 2 unique resource nodes, found {len(resource_node_defs)}: {resource_node_defs}\"\n    )\n\n\ndef test_draw_all_possible_flows_with_child_workflow_mermaid(\n    nested_workflow: Workflow,\n) -> None:\n    \"\"\"Test Mermaid diagram generation for nested workflows.\"\"\"\n    result = draw_all_possible_flows_mermaid(\n        nested_workflow, include_child_workflows=True\n    )\n\n    # Basic checks\n    assert isinstance(result, str)\n    assert result.startswith(\"flowchart TD\")\n\n    # Check for parent workflow nodes\n    assert \"step_parent_start\" in result\n    assert \"step_parent_end\" in result\n\n    # Check for child workflow nodes (prefixed)\n    assert \"step_parent_start_ChildWorkflowA_child_start\" in result\n    assert \"event_parent_start_ChildWorkflowA_StartEvent\" in result\n    assert \"event_parent_start_ChildWorkflowA_StopEvent\" in result\n\n    # Check for connector nodes (calls/returns are now nodes, not edge labels)\n    assert \"child_connector_parent_start_ChildWorkflowA_calls\" in result\n    assert \"calls: ChildWorkflowA\" in result\n    assert \"child_connector_parent_start_ChildWorkflowA_returns\" in result\n    assert \"returns: ChildWorkflowA\" in result\n\n    # Parent step -> calls node -> child StartEvent\n    assert (\n        \"step_parent_start --> child_connector_parent_start_ChildWorkflowA_calls\"\n        in result\n    )\n    assert (\n        \"child_connector_parent_start_ChildWorkflowA_calls --> event_parent_start_ChildWorkflowA_StartEvent\"\n        in result\n    )\n    # Child StopEvent -> returns node -> parent step\n    assert (\n        \"event_parent_start_ChildWorkflowA_StopEvent --> child_connector_parent_start_ChildWorkflowA_returns\"\n        in result\n    )\n    assert (\n        \"child_connector_parent_start_ChildWorkflowA_returns --> step_parent_start\"\n        in result\n    )\n\n\ndef test_draw_all_possible_flows_with_child_workflow_pyvis(\n    nested_workflow: Workflow,\n) -> None:\n    \"\"\"Test Pyvis diagram generation includes nested child workflow nodes and edges.\"\"\"\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        mock_net_instance = MagicMock()\n        mock_network.return_value = mock_net_instance\n\n        draw_all_possible_flows(\n            nested_workflow, filename=\"test.html\", include_child_workflows=True\n        )\n\n        node_ids = [call[0][0] for call in mock_net_instance.add_node.call_args_list]\n\n        # Parent nodes present\n        assert \"parent_start\" in node_ids\n        assert \"parent_end\" in node_ids\n\n        # Child workflow nodes present (prefixed)\n        assert \"parent_start_ChildWorkflowA_child_start\" in node_ids\n        assert \"parent_start_ChildWorkflowA_StartEvent\" in node_ids\n        assert \"parent_start_ChildWorkflowA_StopEvent\" in node_ids\n\n        # Connector nodes present\n        assert \"parent_start_ChildWorkflowA_calls\" in node_ids\n        assert \"parent_start_ChildWorkflowA_returns\" in node_ids\n\n        # Check stitching edges through connector nodes\n        edge_pairs = [\n            (call[0][0], call[0][1])\n            for call in mock_net_instance.add_edge.call_args_list\n        ]\n        # parent_start -> calls node -> child StartEvent\n        assert (\"parent_start\", \"parent_start_ChildWorkflowA_calls\") in edge_pairs\n        assert (\n            \"parent_start_ChildWorkflowA_calls\",\n            \"parent_start_ChildWorkflowA_StartEvent\",\n        ) in edge_pairs\n        # child StopEvent -> returns node -> parent_start\n        assert (\n            \"parent_start_ChildWorkflowA_StopEvent\",\n            \"parent_start_ChildWorkflowA_returns\",\n        ) in edge_pairs\n        assert (\"parent_start_ChildWorkflowA_returns\", \"parent_start\") in edge_pairs\n\n\n# --- Tests using workflow classes (not instances) ---\n\n\ndef test_draw_all_possible_flows_mermaid_with_class() -> None:\n    \"\"\"Test that draw_all_possible_flows_mermaid accepts a workflow class.\"\"\"\n    result = draw_all_possible_flows_mermaid(DummyWorkflow)\n\n    assert isinstance(result, str)\n    assert result.startswith(\"flowchart TD\")\n    assert \"start_step\" in result\n    assert \"middle_step\" in result\n    assert \"end_step\" in result\n\n\ndef test_draw_all_possible_flows_with_class() -> None:\n    \"\"\"Test that draw_all_possible_flows accepts a workflow class.\"\"\"\n    with patch(\"llama_index.utils.workflow.Network\") as mock_network:\n        draw_all_possible_flows(DummyWorkflow, filename=\"test_class.html\")\n        mock_network.assert_called_once()\n        mock_network.return_value.show.assert_called_once_with(\n            \"test_class.html\", notebook=False\n        )\n\n\ndef test_class_and_instance_produce_same_mermaid() -> None:\n    \"\"\"Test that passing a class or an instance produces the same diagram.\"\"\"\n    result_class = draw_all_possible_flows_mermaid(DummyWorkflow)\n    result_instance = draw_all_possible_flows_mermaid(DummyWorkflow())\n\n    assert result_class == result_instance\n\n\ndef test_draw_all_possible_flows_mermaid_with_resource_class() -> None:\n    \"\"\"Test that draw_all_possible_flows_mermaid accepts a workflow class with resources.\"\"\"\n    result = draw_all_possible_flows_mermaid(ResourceWorkflow)\n\n    assert \"classDef resourceStyle fill:#DDA0DD\" in result\n    lines = result.split(\"\\n\")\n    resource_lines = [line for line in lines if \"resource_\" in line and \":::\" in line]\n    assert len(resource_lines) > 0\n\n\ndef test_draw_all_possible_flows_with_child_workflow_class_mermaid() -> None:\n    \"\"\"Test Mermaid diagram generation for nested workflows using class.\"\"\"\n    result = draw_all_possible_flows_mermaid(\n        ParentWorkflow, include_child_workflows=True\n    )\n\n    assert isinstance(result, str)\n    assert \"step_parent_start\" in result\n    assert \"step_parent_end\" in result\n"
  },
  {
    "path": "packages/llama-index-workflows/CHANGELOG.md",
    "content": "# llama-index-workflows\n\n## 2.20.0\n\n### Minor Changes\n\n- 9bf247a: Add replay_ticks_stream and ReplayResult to surface the last exit-indicating command from tick replay\n- 2cc9fae: Add `@catch_error` handler (supports `for_steps=[...]` and `max_recoveries`) and `Context.retry_info()` for handling exhausted step retries inline. `retry_info().last_exception` and `StepFailedEvent.exception` are live Python exceptions.\n\n## 2.19.1\n\n### Patch Changes\n\n- f7e037e: Stream ticks during resume so peak memory is bounded by batch size rather than total tick history.\n\n## 2.19.0\n\n### Minor Changes\n\n- 2592c80: Add composable retry primitives (`retry_policy(retry=..., wait=..., stop=...)`, `retry_if_exception_type`, `wait_exponential`, `stop_after_attempt`, etc.).\n\n## 2.18.0\n\n### Minor Changes\n\n- 43ff242: Add `Context.get_step_context()` static method to retrieve the step context without a `ctx` parameter in the step signature\n\n## 2.17.3\n\n### Patch Changes\n\n- b8c7c7e: fix memory leak where asyncio timers could capture a Workflow reference via RunContext\n\n## 2.17.2\n\n### Patch Changes\n\n- 7e06f87: Make retry jitter deterministic for journaled replay support\n\n## 2.17.1\n\n### Patch Changes\n\n- 979d68b: Fix draw_all_possible_flows to accept workflow classes\n- 983f6f6: feat: Enhance VerboseDecorator with tick-level logging\n\n## 2.17.0\n\n### Minor Changes\n\n- 7fc1aae: feat: add graph structure validation (reachability, terminal events) with opt-out\n- b32ec53: Drop python 3.9 support\n\n## 2.16.1\n\n### Patch Changes\n\n- c7bbedb: Fix wait_for_event timeout not being enforced\n- 703ec92: Internal support for post tick processed callbacks\n\n## 2.16.0\n\n### Minor Changes\n\n- 5e7f9e5: Add event input/output summaries to step spans and rehydrate span context across serialization boundaries. Log instead of fail cancelled steps from cancelled workflows. Do not fail from wait_for_event exceptions.\n\n### Patch Changes\n\n- 9f26314: feat: add ExponentialBackoffRetryPolicy for retry steps\n\n## 2.15.1\n\n### Patch Changes\n\n- 6ec262c: Reduce noisy errors during shutdown\n\n## 2.15.0\n\n### Minor Changes\n\n- 3c22216: Make WorkflowTick serializable, and support switching workflow name and runtime before launch\n\n### Patch Changes\n\n- 77a3f9c: Add workflow release for idle DBOS workflows (with replica support)\n- 707a254: Fix `Workflow(verbose=True)` being a no-op by adding a `VerboseDecorator` that intercepts `StepStateChanged` events to print step starts and completions\n- 05f5f4e: Fix idle detection only working for wait_for_event, not for steps waiting on InputRequiredEvent\n- 96e437e: Move task execution into the runtime, for maximal control of specific runtime semantics around determinism\n\n## 2.15.0-rc.1\n\n### Patch Changes\n\n- 3720c61: Add workflow release for idle DBOS workflows (with replica support)\n- a2aad32: Move task execution into the runtime, for maximal control of specific runtime semantics around determinism\n\n## 2.15.0-rc.0\n\n### Minor Changes\n\n- b515a46: Make WorkflowTick serializable, and support switching workflow name and runtime before launch\n\n### Patch Changes\n\n- e981f73: Fix idle detection only working for wait_for_event, not for steps waiting on InputRequiredEvent\n- 7433d4c: Add fix for double send when waiter event and accepted event match\n\n## 2.14.2\n\n### Patch Changes\n\n- 3590913: Fix span tracking in observability tooling\n- 7433d4c: Add fix for double send when waiter event and accepted event match\n\n## 2.14.1\n\n### Patch Changes\n\n- 6ece797: Fix concurrent step cancellation regression where StopEvent no longer cancelled as quickly as previously\n\n## 2.14.0\n\n### Minor Changes\n\n- 73c1254: refactor: expand runtime plugin architecture\n\n  - Refactoring to better support alternate distributed backends\n  - Some `Context` methods may now raise errors if used in an unexpected context\n  - `WorkflowHandler` is no longer a future. Retains compatibility methods for main use cases (exception, cancel, etc)\n\n- 45e7614: Replace InMemoryStateStore types with a corresponding StateStore protocol\n- 2900f58: Support state type inheritance in workflows\n\n### Patch Changes\n\n- 45e7614: Refact: make control loop more deterministic\n\n  - Switches out the asyncio delay mechanism for a pull-with-timeout that is more deterministic friendly\n  - Adds a priority queue of delayed tasks\n  - Switches out the misc firing /spawning of async tasks to a more rigorous pattern where tasks are only created in the main loop, and gathered in one location. This makes the concurrency more straightforward to reason about\n\n- 6fdc45c: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.15/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.15/dist/app.css\n\n## 2.13.1\n\n### Patch Changes\n\n- e958ed2: fix: ResourceConfig was loading config file eagerly\n- ebaf212: Add resource validation to workflow.validate()\n\n## 2.13.0\n\n### Minor Changes\n\n- 6dd7fc0: Add resource config node support to workflow representation\n- be19869: Add support for injecting resources more flexibly\n\n  - Add support for injecting Resources recursively, so a Resource can depend on another Resource or ResourceConfig\n  - Add support for injecting ResourceConfig directly into steps\n  - Fix issues with resolving from String quoted types\n\n## 2.12.2\n\n### Patch Changes\n\n- bfbfba4: Return an empty list for empty target events, rather than None\n- 85f948e: fix: rebuild_state_from_ticks clears in_progress before replaying\n\n  Fixed ctx.to_dict() failing with \"Worker X not found in in_progress\" when checkpointing resumed workflows. The function now also rewinds in progress when recreating from ticks, to match the actual behavior when resuming a workflow.\n\n## 2.12.1\n\n### Patch Changes\n\n- 40be1c7: add workflow class name to WorkflowGraph representation\n\n## 2.12.0\n\n### Minor Changes\n\n- e53c654: Add further detail to workflow graph, mainly adding `Resource` nodes to workflow graph and visualizations\n- 2ff316d: Updates workflow server with functionality to drop and restore idle workflow handlers that are waiting on external input.\n- 0d72b4d: reorganize workflow graph representation types\n- f96faa2: Add dedicated StopEvent subclasses for workflow termination (timeout, cancellation, failure)\n\n### Patch Changes\n\n- 3b043b8: Track when workflows are idle (waiting on external input)\n- 7a85c96: Add ResourceConfig for resource-level configuration injection\n\n## 2.11.7\n\n### Patch Changes\n\n- 6c35e4d: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.12/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.12/dist/app.css\n\n- f58537a: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.14/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.14/dist/app.css\n\n## 2.11.6\n\n### Patch Changes\n\n- 94fa8ce: Fix infinite retries with no delay\n- f8fa366: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.10/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.10/dist/app.css\n\n## 2.11.5\n\n### Patch Changes\n\n- 27a4cf0: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.2/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.2/dist/app.css\n\n## 2.11.4\n\n### Patch Changes\n\n- 95abac0: Update debugger assets\n\n  - JavaScript: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.0/dist/app.js\n  - CSS: https://cdn.jsdelivr.net/npm/@llamaindex/workflow-debugger@0.2.0/dist/app.css\n\n- 8f344bd: Fix resuming from serialized context for workflows that uses typed events\n\n## 2.11.3\n\n### Patch Changes\n\n- f307253: Update typechecking to support ty\n- 91159d7: Moving `_extract_workflow_structure` to its own module in workflow core\n- 300fd05: Add stricter ruff formatting checks\n- 32ae78a: Switch build backend to uv\n\n## 2.11.2\n\n### Patch Changes\n\n- ee56c97: Fix remove task functionality on \\_execute_task, specially when the task has gone missing\n"
  },
  {
    "path": "packages/llama-index-workflows/README.md",
    "content": "# LlamaIndex Workflows\n\nLlamaIndex Workflows are a framework for orchestrating and chaining together complex systems of steps and events.\n\n## What can you build with Workflows?\n\nWorkflows shine when you need to orchestrate complex, multi-step processes that involve AI models, APIs, and decision-making. Here are some examples of what you can build:\n\n- **AI Agents** - Create intelligent systems that can reason, make decisions, and take actions across multiple steps\n- **Document Processing Pipelines** - Build systems that ingest, analyze, summarize, and route documents through various processing stages\n- **Multi-Model AI Applications** - Coordinate between different AI models (LLMs, vision models, etc.) to solve complex tasks\n- **Research Assistants** - Develop workflows that can search, analyze, synthesize information, and provide comprehensive answers\n- **Content Generation Systems** - Create pipelines that generate, review, edit, and publish content with human-in-the-loop approval\n- **Customer Support Automation** - Build intelligent routing systems that can understand, categorize, and respond to customer inquiries\n\nThe async-first, event-driven architecture makes it easy to build workflows that can route between different capabilities, implement parallel processing patterns, loop over complex sequences, and maintain state across multiple steps - all the features you need to make your AI applications production-ready.\n\n## Key Features\n\n- **async-first** - workflows are built around python's async functionality - steps are async functions that process incoming events from an asyncio queue and emit new events to other queues. This also means that workflows work best in your async apps like FastAPI, Jupyter Notebooks, etc.\n- **event-driven** - workflows consist of steps and events. Organizing your code around events and steps makes it easier to reason about and test.\n- **state management** - each run of a workflow is self-contained, meaning you can launch a workflow, save information within it, serialize the state of a workflow and resume it later.\n- **observability** - workflows are automatically instrumented for observability, meaning you can use tools like `Arize Phoenix` and `OpenTelemetry` right out of the box.\n\n## Quick Start\n\nInstall the package:\n\n```bash\npip install llama-index-workflows\n```\n\nAnd create your first workflow:\n\n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nclass MyEvent(Event):\n    msg: list[str]\n\nclass RunState(BaseModel):\n    num_runs: int = Field(default=0)\n\nclass MyWorkflow(Workflow):\n    @step\n    async def start(self, ctx: Context[RunState], ev: StartEvent) -> MyEvent:\n        async with ctx.store.edit_state() as state:\n            state.num_runs += 1\n\n            return MyEvent(msg=[ev.input_msg] * state.num_runs)\n\n    @step\n    async def process(self, ctx: Context[RunState], ev: MyEvent) -> StopEvent:\n        data_length = len(\"\".join(ev.msg))\n        new_msg = f\"Processed {len(ev.msg)} times, data length: {data_length}\"\n        return StopEvent(result=new_msg)\n\nasync def main():\n    workflow = MyWorkflow()\n\n    # [optional] provide a context object to the workflow\n    ctx = Context(workflow)\n    result = await workflow.run(input_msg=\"Hello, world!\", ctx=ctx)\n    print(\"Workflow result:\", result)\n\n    # re-running with the same context will retain the state\n    result = await workflow.run(input_msg=\"Hello, world!\", ctx=ctx)\n    print(\"Workflow result:\", result)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nIn the example above\n- Steps that accept a `StartEvent` will be run first.\n- Steps that return a `StopEvent` will end the workflow.\n- Intermediate events are user defined and can be used to pass information between steps.\n- The `Context` object is also used to share information between steps.\n\nVisit the [complete documentation](https://docs.llamaindex.ai/en/stable/understanding/workflows/) for more examples using `llama-index`!\n"
  },
  {
    "path": "packages/llama-index-workflows/package.json",
    "content": "{\n  \"name\": \"llama-index-workflows\",\n  \"version\": \"2.20.0\",\n  \"private\": false,\n  \"license\": \"MIT\",\n  \"scripts\": {}\n}\n"
  },
  {
    "path": "packages/llama-index-workflows/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.9.10,<0.10.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"pre-commit>=4.2.0\",\n  \"pytest>=8.4.2\",\n  \"pytest-asyncio>=1.0.0\",\n  \"pytest-cov>=7.0.0\",\n  \"httpx>=0.25.0\",\n  \"pyyaml>=6.0.2\",\n  \"packaging>=21.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"tenacity>=9.1.2\",\n  \"structlog>=25.5.0\",\n  \"starlette>=0.39.0\",\n  \"uvicorn>=0.32.0\",\n  \"time-machine>=2.19.0,<3.0.0\"\n]\n\n[project]\nname = \"llama-index-workflows\"\nversion = \"2.20.0\"\ndescription = \"An event-driven, async-first, step-based way to control the execution flow of AI applications like Agents.\"\nreadme = \"README.md\"\nlicense = \"MIT\"\nrequires-python = \">=3.10\"\ndependencies = [\n  \"llama-index-instrumentation>=0.4.3\",\n  \"pydantic>=2.11.5\",\n  \"typing-extensions>=4.6.0\"\n]\n\n# backward compatibility, for when these were embedded.\n# Optionals are now just an alias to additionally install\n# a less pinned version of the client/server packages.\n[project.optional-dependencies]\nserver = [\"llama-agents-server>=0.1.0,<1.0.0\"]\nclient = [\"llama-agents-client>=0.1.0,<1.0.0\"]\n\n[tool.basedpyright]\nexclude = [\"**/.venv\", \"**/node_modules\", \"**/.uv\"]\n# Python 3.14 beta has stdlib changes that basedpyright doesn't understand yet\nignore = [\"**/cpython-3.14*/**\"]\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"module\"\nasyncio_default_test_loop_scope = \"module\"\ntestpaths = [\"tests\"]\naddopts = \"-nauto --timeout=10\"\n\n[tool.uv.build-backend]\nmodule-name = [\"workflows\", \"llama_agents.workflows\"]\nnamespace = true\n"
  },
  {
    "path": "packages/llama-index-workflows/src/llama_agents/workflows/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Alias: llama_agents.workflows -> workflows\n#\n# This module makes the entire `workflows` package available under\n# `llama_agents.workflows`, including all sub-modules. It uses a\n# custom meta-path finder to lazily redirect any import of\n# `llama_agents.workflows.<sub>` to `workflows.<sub>`.\n\nfrom __future__ import annotations\n\nimport importlib\nimport sys\nfrom importlib.abc import Loader, MetaPathFinder\nfrom importlib.machinery import ModuleSpec\nfrom types import ModuleType\nfrom typing import Sequence\n\n_ALIAS_PREFIX = \"llama_agents.workflows\"\n_REAL_PREFIX = \"workflows\"\n\n\nclass _AliasLoader(Loader):\n    \"\"\"Loader that returns an already-imported module from sys.modules.\"\"\"\n\n    def __init__(self, real_name: str) -> None:\n        self.real_name = real_name\n\n    def create_module(self, spec: ModuleSpec) -> ModuleType | None:\n        return importlib.import_module(self.real_name)\n\n    def exec_module(self, module: ModuleType) -> None:\n        # Module is already fully initialized by the real import.\n        pass\n\n\nclass _AliasFinder(MetaPathFinder):\n    \"\"\"Meta-path finder that redirects llama_agents.workflows.* to workflows.*\"\"\"\n\n    def find_spec(\n        self,\n        fullname: str,\n        path: Sequence[str] | None = None,\n        target: ModuleType | None = None,\n    ) -> ModuleSpec | None:\n        # Only handle llama_agents.workflows.* (not the root itself)\n        if not fullname.startswith(_ALIAS_PREFIX + \".\"):\n            return None\n        suffix = fullname[len(_ALIAS_PREFIX) :]\n        real_name = _REAL_PREFIX + suffix\n        return ModuleSpec(fullname, _AliasLoader(real_name))\n\n\n# Install the finder once\nif not any(isinstance(f, _AliasFinder) for f in sys.meta_path):\n    sys.meta_path.append(_AliasFinder())\n\n# Re-export everything from the real workflows package\nfrom workflows import *  # noqa: E402, F403\nfrom workflows import __all__  # noqa: E402, F401\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom pkgutil import extend_path\n\nfrom .context import Context\nfrom .decorators import catch_error, step\nfrom .workflow import Workflow\n\n__path__ = extend_path(__path__, __name__)\n\n\n__all__ = [\n    \"Context\",\n    \"Workflow\",\n    \"catch_error\",\n    \"step\",\n]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/_event_summary.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom workflows.events import Event, StopEvent\n\n\ndef _summarize_value(value: Any, max_val_length: int = 50) -> str:\n    if isinstance(value, str):\n        if len(value) > max_val_length:\n            value = value[:max_val_length] + \"...\"\n        return repr(value)\n    if isinstance(value, list):\n        if len(value) > 3:\n            return f\"[{len(value)} items]\"\n        return repr(value)\n    if isinstance(value, dict):\n        if len(value) > 3:\n            return f\"{{{len(value)} keys}}\"\n        return repr(value)\n    return repr(value)\n\n\ndef summarize_event(event: Event, max_length: int = 200) -> str:\n    try:\n        parts: list[str] = []\n\n        # Special-case StopEvent to include result\n        if isinstance(event, StopEvent):\n            result = event._result  # noqa: SLF001\n            if result is not None:\n                parts.append(f\"result={_summarize_value(result)}\")\n\n        # Declared pydantic fields\n        for field_name in event.__class__.model_fields:\n            val = getattr(event, field_name, None)\n            parts.append(f\"{field_name}={_summarize_value(val)}\")\n\n        # Dynamic _data entries\n        data = event._data  # noqa: SLF001\n        for key, val in data.items():\n            parts.append(f\"{key}={_summarize_value(val)}\")\n\n        class_name = event.__class__.__name__\n        inner = \", \".join(parts)\n        result_str = f\"{class_name}({inner})\"\n\n        if len(result_str) > max_length:\n            result_str = result_str[: max_length - 3] + \"...\"\n\n        return result_str\n    except Exception:\n        try:\n            return repr(event)\n        except Exception:\n            return event.__class__.__name__\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/client/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Re-export client components from the optional llama-agents-client package.\"\"\"\n\nimport warnings\n\nwarnings.warn(\n    \"Importing from 'workflows.client' is deprecated. \"\n    \"Install 'llama-agents-client' and use \"\n    \"'from llama_agents.client import ...' instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\ntry:\n    from llama_agents.client import WorkflowClient\nexcept ImportError as e:\n    raise ImportError(\n        \"workflows.client requires the 'client' extra. \"\n        \"Install with: pip install 'llama-index-workflows[client]'\"\n    ) from e\n\n__all__ = [\"WorkflowClient\"]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom .context import Context\nfrom .serializers import BaseSerializer, JsonSerializer, PickleSerializer\n\n__all__ = [\n    \"Context\",\n    \"PickleSerializer\",\n    \"JsonSerializer\",\n    \"BaseSerializer\",\n]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/context.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport warnings\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    AsyncGenerator,\n    Generic,\n    TypeVar,\n    cast,\n)\n\nfrom llama_index_instrumentation.dispatcher import (\n    active_instrument_tags,\n    instrument_tags,\n)\n\nfrom workflows.context.external_context import ExternalContext\nfrom workflows.context.internal_context import InternalContext\nfrom workflows.context.pre_context import PreContext\nfrom workflows.errors import (\n    ContextSerdeError,\n    ContextStateError,\n    WorkflowRuntimeError,\n)\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.handler import WorkflowHandler\nfrom workflows.retry_policy import RetryInfo\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n)\nfrom workflows.runtime.types.results import InternalContextVar\nfrom workflows.types import RunResultT\nfrom workflows.utils import _nanoid as nanoid\n\nfrom .serializers import BaseSerializer, JsonSerializer\nfrom .state_store import MODEL_T, StateStore\n\nif TYPE_CHECKING:  # pragma: no cover\n    from workflows import Workflow\n\n\nT = TypeVar(\"T\", bound=Event)\nEventBuffer = dict[str, list[Event]]\n\n\n# TODO(v3) remove this class, and replace with direct references to the pre/internal/external contexts\nclass Context(Generic[MODEL_T]):\n    \"\"\"\n    Global, per-run context for a `Workflow`. Provides an interface into the\n    underlying broker run, for both external (workflow run oberservers) and\n    internal consumption by workflow steps.\n\n    The `Context` coordinates event delivery between steps, tracks in-flight work,\n    exposes a global state store, and provides utilities for streaming and\n    synchronization. It is created by a `Workflow` at run time and can be\n    persisted and restored.\n\n    Args:\n        workflow (Workflow): The owning workflow instance. Used to infer\n            step configuration and instrumentation.\n        previous_context: A previous context snapshot to resume from.\n        serializer: A serializer to use for serializing and deserializing the current and previous context snapshots.\n\n    Attributes:\n        is_running (bool): Whether the workflow is currently running.\n        store (StateStore[MODEL_T]): Type-safe, async state store shared\n            across steps. See also\n            [StateStore][workflows.context.state_store.StateStore].\n\n    Examples:\n        Basic usage inside a step:\n\n        ```python\n        from workflows import step\n        from workflows.events import StartEvent, StopEvent\n\n        @step\n        async def start(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            await ctx.store.set(\"query\", ev.topic)\n            ctx.write_event_to_stream(ev)  # surface progress to UI\n            return StopEvent(result=\"ok\")\n        ```\n\n        Persisting the state of a workflow across runs:\n\n        ```python\n        from workflows import Context\n\n        # Create a context and run the workflow with the same context\n        ctx = Context(my_workflow)\n        result_1 = await my_workflow.run(..., ctx=ctx)\n        result_2 = await my_workflow.run(..., ctx=ctx)\n\n        # Serialize the context and restore it\n        ctx_dict = ctx.to_dict()\n        restored_ctx = Context.from_dict(my_workflow, ctx_dict)\n        result_3 = await my_workflow.run(..., ctx=restored_ctx)\n        ```\n\n\n    See Also:\n        - [Workflow][workflows.Workflow]\n        - [Event][workflows.events.Event]\n        - [InMemoryStateStore][workflows.context.state_store.InMemoryStateStore]\n    \"\"\"\n\n    # Current face - context is in exactly one state at a time\n    _face: (\n        PreContext[MODEL_T] | ExternalContext[MODEL_T, Any] | InternalContext[MODEL_T]\n    )\n\n    def __init__(\n        self,\n        workflow: Workflow,\n        previous_context: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> None:\n        # Start in pre-run (config) state - PreContext handles deserialization\n        pre_context: PreContext[MODEL_T] = PreContext(\n            workflow=workflow,\n            previous_context=previous_context,\n            serializer=serializer,\n        )\n        self._face = pre_context\n\n    @classmethod\n    def _create_face(\n        cls,\n        face: PreContext[MODEL_T]\n        | ExternalContext[MODEL_T, Any]\n        | InternalContext[MODEL_T],\n    ) -> Context[MODEL_T]:\n        new_ctx = cast(Context[MODEL_T], object.__new__(cls))\n        new_ctx._face = face\n        return new_ctx\n\n    @staticmethod\n    def get_step_context() -> Context:\n        \"\"\"Return the `Context` for the currently executing step.\n\n        This is useful for decorators or wrappers around step functions that\n        need access to the step context without requiring the user-defined\n        step to declare a ``ctx: Context`` parameter.\n\n        Returns:\n            Context: The context instance (in internal-face state) for the\n            running step.\n\n        Raises:\n            WorkflowRuntimeError: If called outside of a step function.\n\n        Examples:\n            ```python\n            from workflows import Context\n\n            # Inside a decorator that wraps a step function\n            ctx = Context.get_step_context()\n            ctx.send_event(ProgressEvent(msg=\"step starting\"))\n            ```\n        \"\"\"\n        try:\n            ref = InternalContextVar.get()\n        except LookupError:\n            raise WorkflowRuntimeError(\n                \"Context.get_step_context() may only be called from within a step function\"\n            )\n        ctx = ref()\n        if ctx is None:\n            raise WorkflowRuntimeError(\n                \"Context.get_step_context() may only be called from within a step function\"\n            )\n        return ctx\n\n    @property\n    def is_running(self) -> bool:\n        \"\"\"Whether the workflow is currently running.\"\"\"\n        if isinstance(self._face, PreContext):\n            return self._face.is_running\n        elif isinstance(self._face, ExternalContext):\n            return self._face.is_running\n        else:\n            _warn_is_running_in_step()\n            return True\n\n    def _require_pre(self, fn: str) -> PreContext[MODEL_T]:\n        \"\"\"Require context to be in pre-run state. Raises ContextStateError if not.\"\"\"\n        if isinstance(self._face, PreContext):\n            return self._face  # type: ignore[ty:invalid-return-type]\n        raise ContextStateError(\n            f\"{fn} requires a pre-run context. The workflow has already started.\"\n        )\n\n    def _require_external(self, fn: str) -> ExternalContext[MODEL_T, Any]:\n        \"\"\"Require context to be in external state. Raises ContextStateError if not.\"\"\"\n        if isinstance(self._face, ExternalContext):\n            return self._face\n        if isinstance(self._face, PreContext):\n            raise ContextStateError(\n                f\"{fn} requires a running workflow. Call workflow.run() first.\"\n            )\n        raise ContextStateError(\n            f\"{fn} is only available from handler code, not from within steps.\"\n        )\n\n    def _require_internal(self, fn: str) -> InternalContext[MODEL_T]:\n        \"\"\"Require context to be in internal state. Raises ContextStateError if not.\"\"\"\n        if isinstance(self._face, InternalContext):\n            return self._face  # type: ignore[ty:invalid-return-type]\n        if isinstance(self._face, PreContext):\n            raise ContextStateError(\n                f\"{fn} requires a running workflow. Call workflow.run() first.\"\n            )\n        raise ContextStateError(f\"{fn} is only available from within step functions.\")\n\n    @classmethod\n    def _create_internal(\n        cls,\n        workflow: Workflow,\n    ) -> Context[MODEL_T]:\n        \"\"\"Create a Context directly in internal face state.\n\n        Requires a current run context (via with_current_run_id) to be set.\n        \"\"\"\n        internal_adapter = workflow._runtime.get_internal_adapter(workflow)\n        new_ctx = cast(Context[MODEL_T], object.__new__(cls))\n        new_ctx._face = cast(\n            InternalContext[MODEL_T],\n            InternalContext(\n                internal_adapter=internal_adapter,\n                workflow=workflow,\n            ),\n        )\n        return new_ctx\n\n    @classmethod\n    def _create_external(\n        cls,\n        workflow: Workflow,\n        external_adapter: ExternalRunAdapter,\n        serializer: BaseSerializer = JsonSerializer(),\n    ) -> Context[MODEL_T]:\n        \"\"\"Create a Context directly in external face state with a broker.\"\"\"\n\n        new_ctx = cast(Context[MODEL_T], object.__new__(cls))\n\n        # Set external face\n        new_ctx._face = cast(\n            ExternalContext[MODEL_T, Any],\n            ExternalContext(\n                workflow=workflow,\n                external_adapter=external_adapter,\n                serializer=serializer,\n            ),\n        )\n        return new_ctx\n\n    def _workflow_run(\n        self,\n        workflow: Workflow,\n        start_event: StartEvent\n        | None,  # None only when resuming a workflow from a snapshotted context\n        run_id: str | None = None,\n    ) -> WorkflowHandler:\n        \"\"\"\n        called by package internally from the workflow to run it\n        \"\"\"\n        run_id = run_id or nanoid()\n        with instrument_tags(\n            {**active_instrument_tags.get(), \"llamaindex.run_id\": run_id}\n        ):\n            # Get or create PreContext for initialization\n            if isinstance(self._face, PreContext):\n                pre = self._face\n            elif isinstance(self._face, ExternalContext):\n                # Check for concurrent run\n                if self._face.is_running:\n                    raise ContextStateError(\n                        \"Cannot start a new run while context is already running. \"\n                        \"Wait for completion or use a new Context.\"\n                    )\n                # Continuation: create fresh PreContext from current state\n                pre = PreContext(\n                    workflow=workflow,\n                    previous_context=self._face.to_dict(),\n                    serializer=self._face._serializer,\n                )\n            else:\n                raise ContextStateError(\n                    \"Cannot start workflow from a step function context\"\n                )\n\n            # Compute state from serialized snapshot\n            init_state = BrokerState.from_serialized(\n                pre.init_snapshot, workflow, pre._serializer\n            )\n\n            # TODO(v3) - make this async\n            external_adapter = workflow._runtime.run_workflow(\n                run_id=run_id,\n                workflow=workflow,\n                init_state=init_state,\n                start_event=start_event,\n                serialized_state=pre.serialized_state,\n                serializer=pre.serializer,\n            )\n\n            # TODO(v3): Remove mutation. Handler will just be the external face.\n            self._face = cast(\n                ExternalContext[MODEL_T, Any],\n                ExternalContext(\n                    workflow=workflow,\n                    external_adapter=external_adapter,\n                    serializer=pre._serializer,\n                ),\n            )\n\n            return WorkflowHandler(\n                workflow=workflow,\n                external_adapter=external_adapter,\n                ctx=self,\n            )\n\n    def _workflow_cancel_run(self) -> None:\n        \"\"\"Called internally from the handler to cancel a context's run.\"\"\"\n        if isinstance(self._face, ExternalContext):\n            self._face.cancel()\n        elif isinstance(self._face, PreContext):\n            _warn_cancel_before_start()\n        else:\n            _warn_cancel_in_step()\n\n    @property\n    def store(self) -> StateStore[MODEL_T]:\n        \"\"\"Typed, process-local state store shared across steps.\n\n        If no state was initialized yet, a default\n        [DictState][workflows.context.state_store.DictState] store is created.\n\n        Returns:\n            StateStore[MODEL_T]: The state store instance.\n        \"\"\"\n        return self._face.store\n\n    def to_dict(self, serializer: BaseSerializer | None = None) -> dict[str, Any]:\n        \"\"\"Serialize the context to a JSON-serializable dict.\n\n        Persists the global state store, event queues, buffers, accepted events,\n        broker log, and running flag. This payload can be fed to\n        [from_dict][workflows.context.context.Context.from_dict] to resume a run\n        or carry state across runs.\n\n        Args:\n            serializer (BaseSerializer | None): Value serializer used for state\n                and event payloads. Defaults to\n                [JsonSerializer][workflows.context.serializers.JsonSerializer].\n\n        Returns:\n            dict[str, Any]: A dict suitable for JSON encoding and later\n            restoration via `from_dict`.\n\n        See Also:\n            - [InMemoryStateStore.to_dict][workflows.context.state_store.InMemoryStateStore.to_dict]\n\n        Examples:\n            ```python\n            ctx_dict = ctx.to_dict()\n            my_db.set(\"key\", json.dumps(ctx_dict))\n\n            ctx_dict = my_db.get(\"key\")\n            restored_ctx = Context.from_dict(my_workflow, json.loads(ctx_dict))\n            result = await my_workflow.run(..., ctx=restored_ctx)\n            ```\n        \"\"\"\n        return self._require_external(fn=\"to_dict\").to_dict(serializer)\n\n    @classmethod\n    def from_dict(\n        cls,\n        workflow: Workflow,\n        data: dict[str, Any],\n        serializer: BaseSerializer | None = None,\n    ) -> Context[MODEL_T]:\n        \"\"\"Reconstruct a `Context` from a serialized payload.\n\n        Args:\n            workflow (Workflow): The workflow instance that will own this\n                context.\n            data (dict[str, Any]): Payload produced by\n                [to_dict][workflows.context.context.Context.to_dict].\n            serializer (BaseSerializer | None): Serializer used to decode state\n                and events. Defaults to JSON.\n\n        Returns:\n            Context[MODEL_T]: A context instance initialized with the persisted\n                state and queues.\n\n        Raises:\n            ContextSerdeError: If the payload is missing required fields or is\n                in an incompatible format.\n\n        Examples:\n            ```python\n            ctx_dict = ctx.to_dict()\n            my_db.set(\"key\", json.dumps(ctx_dict))\n\n            ctx_dict = my_db.get(\"key\")\n            restored_ctx = Context.from_dict(my_workflow, json.loads(ctx_dict))\n            result = await my_workflow.run(..., ctx=restored_ctx)\n            ```\n        \"\"\"\n        try:\n            return cls(workflow, previous_context=data, serializer=serializer)\n        except KeyError as e:\n            msg = \"Error creating a Context instance: the provided payload has a wrong or old format.\"\n            raise ContextSerdeError(msg) from e\n\n    async def running_steps(self) -> list[str]:\n        \"\"\"Return the list of currently running step names.\n\n        Returns:\n            list[str]: Names of steps that have at least one active worker.\n        \"\"\"\n        return await self._require_external(fn=\"running_steps\").running_steps()\n\n    def collect_events(\n        self, ev: Event, expected: list[type[Event]], buffer_id: str | None = None\n    ) -> list[Event] | None:\n        \"\"\"\n        Buffer events until all expected types are available, then return them.\n\n        This utility is helpful when a step can receive multiple event types\n        and needs to proceed only when it has a full set. The returned list is\n        ordered according to `expected`.\n\n        Args:\n            ev (Event): The incoming event to add to the buffer.\n            expected (list[Type[Event]]): Event types to collect, in order.\n            buffer_id (str | None): Optional stable key to isolate buffers across\n                steps or workers. Defaults to an internal key derived from the\n                task name or expected types.\n\n        Returns:\n            list[Event] | None: The events in the requested order when complete,\n            otherwise `None`.\n\n        Examples:\n            ```python\n            @step\n            async def synthesize(\n                self, ctx: Context, ev: QueryEvent | RetrieveEvent\n            ) -> StopEvent | None:\n                events = ctx.collect_events(ev, [QueryEvent, RetrieveEvent])\n                if events is None:\n                    return None\n                query_ev, retrieve_ev = events\n                # ... proceed with both inputs present ...\n            ```\n\n        See Also:\n            - [Event][workflows.events.Event]\n        \"\"\"\n        return self._require_internal(fn=\"collect_events\").collect_events(\n            ev, expected, buffer_id\n        )\n\n    def send_event(self, message: Event, step: str | None = None) -> None:\n        \"\"\"Dispatch an event to one or all workflow steps.\n\n        If `step` is omitted, the event is broadcast to all step queues and\n        non-matching steps will ignore it. When `step` is provided, the target\n        step must accept the event type or a\n        [WorkflowRuntimeError][workflows.errors.WorkflowRuntimeError] is raised.\n\n        Args:\n            message (Event): The event to enqueue.\n            step (str | None): Optional step name to target.\n\n        Raises:\n            WorkflowRuntimeError: If the target step does not exist or does not\n                accept the event type.\n\n        Examples:\n            It's common to use this method to fan-out events:\n\n            ```python\n            @step\n            async def my_step(self, ctx: Context, ev: StartEvent) -> WorkerEvent | GatherEvent:\n                for i in range(10):\n                    ctx.send_event(WorkerEvent(msg=i))\n                return GatherEvent()\n            ```\n\n            You also see this method used from the caller side to send events into the workflow:\n\n            ```python\n            handler = my_workflow.run(...)\n            async for ev in handler.stream_events():\n                if isinstance(ev, SomeEvent):\n                    handler.ctx.send_event(SomeOtherEvent(msg=\"Hello!\"))\n\n            result = await handler\n            ```\n        \"\"\"\n        # send_event can be called from internal (steps) or external (handler) contexts\n        if isinstance(self._face, InternalContext):\n            self._face.send_event(message, step)\n        elif isinstance(self._face, ExternalContext):\n            self._face.send_event(message, step)\n        else:\n            raise ContextStateError(\n                \"send_event() called before workflow started. \"\n                \"Call workflow.run() first.\"\n            )\n\n    async def wait_for_event(\n        self,\n        event_type: type[T],\n        waiter_event: Event | None = None,\n        waiter_id: str | None = None,\n        requirements: dict[str, Any] | None = None,\n        timeout: float | None = 2000,\n    ) -> T:\n        \"\"\"Wait for the next matching event of type `event_type`.\n\n        The runtime pauses by throwing an internal control-flow exception and replays\n        the entire step when the event arrives, so keep this call near the top of the\n        step and make any preceding work safe to repeat.\n\n        Optionally emits a `waiter_event` to the event stream once per `waiter_id` to\n        inform callers that the workflow is waiting for external input.\n        This helps to prevent duplicate waiter events from being sent to the event stream.\n\n        Args:\n            event_type (type[T]): Concrete event class to wait for.\n            waiter_event (Event | None): Optional event to write to the stream\n                once when the wait begins.\n            waiter_id (str | None): Stable identifier to avoid emitting multiple\n                waiter events for the same logical wait.\n            requirements (dict[str, Any] | None): Key/value filters that must be\n                satisfied by the event via `event.get(key) == value`.\n            timeout (float | None): Max seconds to wait. `None` means no\n                timeout. Defaults to 2000 seconds.\n\n        Returns:\n            T: The received event instance of the requested type.\n\n        Raises:\n            asyncio.TimeoutError: If the timeout elapses.\n\n        Examples:\n            ```python\n            @step\n            async def my_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n                response = await ctx.wait_for_event(\n                    HumanResponseEvent,\n                    waiter_event=InputRequiredEvent(msg=\"What's your name?\"),\n                    waiter_id=\"user_name\",\n                    timeout=60,\n                )\n                return StopEvent(result=response.response)\n            ```\n        \"\"\"\n        return await self._require_internal(fn=\"wait_for_event\").wait_for_event(\n            event_type, waiter_event, waiter_id, requirements, timeout\n        )\n\n    def retry_info(self) -> RetryInfo:\n        \"\"\"Return a snapshot of the currently-executing step's retry state.\n\n        Returns:\n            RetryInfo: 0-based retry number (0 on first run, 1 on first retry),\n            seconds since the first attempt, the most recent prior exception\n            (or `None`), and the timezone-aware UTC datetime of that failure\n            (or `None`).\n\n        Raises:\n            WorkflowRuntimeError: If called outside of a step function.\n\n        Examples:\n            ```python\n            @step(retry_policy=ConstantDelay(maximum_attempts=3, delay=0))\n            async def flaky(self, ctx: Context, ev: StartEvent) -> StopEvent:\n                info = ctx.retry_info()\n                if info.last_exception is not None:\n                    logger.info(\n                        \"retry %d: %s\",\n                        info.retry_number,\n                        str(info.last_exception),\n                    )\n                ...\n            ```\n        \"\"\"\n        return self._require_internal(fn=\"retry_info\").retry_info()\n\n    def write_event_to_stream(self, ev: Event | None) -> None:\n        \"\"\"Enqueue an event for streaming to [WorkflowHandler]](workflows.handler.WorkflowHandler).\n\n        Args:\n            ev (Event | None): The event to stream. `None` can be used as a\n                sentinel in some streaming modes.\n\n        Examples:\n            ```python\n            @step\n            async def my_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n                ctx.write_event_to_stream(ev)\n                return StopEvent(result=\"ok\")\n            ```\n        \"\"\"\n        self._require_internal(fn=\"write_event_to_stream\").write_event_to_stream(ev)\n\n    async def _finalize_step(self) -> None:\n        \"\"\"Finalize step execution by awaiting background tasks.\n\n        Called after a step function completes to ensure all fire-and-forget\n        operations (e.g., write_event_to_stream, send_event) complete before\n        returning control to the control loop.\n        \"\"\"\n        await self._require_internal(fn=\"_finalize_step\")._finalize_step()\n\n    def get_result(self) -> RunResultT:\n        \"\"\"Return the final result of the workflow run.\n\n        Deprecated:\n            This method is deprecated and will be removed in a future release.\n            Prefer awaiting the handler returned by `Workflow.run`, e.g.:\n            `result = await workflow.run(..., ctx=ctx)`.\n\n        Examples:\n            ```python\n            # Preferred\n            result = await my_workflow.run(..., ctx=ctx)\n\n            # Deprecated\n            result_agent = ctx.get_result()\n            ```\n\n        Returns:\n            RunResultT: The value provided via a `StopEvent`.\n\n        Raises:\n            ContextStateError: If called before the workflow is running or\n                from within a step function.\n        \"\"\"\n        _warn_get_result()\n        stop_event = self._require_external(fn=\"get_result\").get_result()\n        return stop_event.result if type(stop_event) is StopEvent else stop_event\n\n    def stream_events(self) -> AsyncGenerator[Event, None]:\n        \"\"\"Stream events published by the workflow.\n\n        Returns an async generator that yields events as they are published\n        by steps via `write_event_to_stream()`.\n\n        Returns:\n            AsyncGenerator[Event, None]: Stream of published events.\n\n        Raises:\n            ContextStateError: If called before the workflow is running or\n                from within a step function.\n        \"\"\"\n        return self._require_external(fn=\"stream_events\").stream_events()\n\n    @property\n    def streaming_queue(self) -> asyncio.Queue:\n        \"\"\"Deprecated queue-based event stream.\n\n        Returns an asyncio.Queue that is populated by iterating this context's\n        stream_events(). A deprecation warning is emitted once per process.\n        \"\"\"\n        _warn_streaming_queue()\n        self._require_external(fn=\"streaming_queue\")\n        q: asyncio.Queue[Event] = asyncio.Queue()\n\n        async def _pump() -> None:\n            async for ev in self.stream_events():\n                await q.put(ev)\n                if isinstance(ev, StopEvent):\n                    break\n\n        try:\n            asyncio.create_task(_pump())\n        except RuntimeError:\n            loop = asyncio.get_event_loop()\n            loop.create_task(_pump())\n        return q\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_get_result() -> None:\n    warnings.warn(\n        (\n            \"Context.get_result() is deprecated and will be removed in a future \"\n            \"release. Prefer awaiting the WorkflowHandler returned by \"\n            \"Workflow.run: `result = await workflow.run(..., ctx=ctx)`.\"\n        ),\n        DeprecationWarning,\n        stacklevel=2,\n    )\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_streaming_queue() -> None:\n    warnings.warn(\n        (\n            \"Context.streaming_queue is deprecated and will be removed in a future \"\n            \"release. Prefer iterating Context.stream_events(): \"\n            \"`async for ev in ctx.stream_events(): ...`\"\n        ),\n        DeprecationWarning,\n        stacklevel=2,\n    )\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_is_running_in_step() -> None:\n    warnings.warn(\n        \"is_running called from within a step; the workflow is always \"\n        \"running inside a step. This usage is deprecated.\",\n        DeprecationWarning,\n        stacklevel=3,\n    )\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_cancel_before_start() -> None:\n    warnings.warn(\n        \"cancel() called before workflow started; nothing to cancel.\",\n        stacklevel=3,\n    )\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_cancel_in_step() -> None:\n    warnings.warn(\n        \"cancel() called from within a step; use send_event() instead.\",\n        stacklevel=3,\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/context_types.py",
    "content": "import json\nfrom typing import Any\n\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic.functional_validators import model_validator\nfrom typing_extensions import TypeVar\n\nfrom workflows.context.state_store import DictState\nfrom workflows.events import SerializableOptionalException\n\nMODEL_T = TypeVar(\"MODEL_T\", bound=BaseModel, default=DictState)  # type: ignore[reportGeneralTypeIssues]\n\n\nclass SerializedContextV0(BaseModel):\n    \"\"\"\n    Legacy format for serialized context (V0). Supported for backwards compatibility, but does not\n    include all currently required runtime state.\n    \"\"\"\n\n    # Serialized state store payload produced by InMemoryStateStore.to_dict(serializer).\n    # Shape:\n    #   {\n    #     \"state_type\": str,            # class name of the model (e.g. \"DictState\" or custom model)\n    #     \"state_module\": str,          # module path of the model\n    #     \"state_data\": ...             # see below\n    #   }\n    # For DictState: state_data = {\"_data\": {key: serialized_value_str}}, where each value is the\n    # serializer-encoded string (e.g. JSON string from JsonSerializer.serialize).\n    # For typed Pydantic models: state_data is a serializer-encoded string containing JSON for a dict with\n    # discriminator fields (e.g. {\"__is_pydantic\": true, \"value\": <model_dump>, \"qualified_name\": <module.Class>}).\n    state: dict[str, Any] = Field(default_factory=dict)\n\n    # Streaming queue contents used by the event stream. This is a JSON string representing a list\n    # of serializer-encoded events (each element is a string as returned by BaseSerializer.serialize).\n    # Example: '[\"<serialized_event>\", \"<serialized_event>\"]'.\n    streaming_queue: str = Field(default=\"[]\")\n\n    # Per-step (and waiter) inbound event queues. Maps queue name -> JSON string representing a list\n    # of serializer-encoded events (same format as streaming_queue).\n    queues: dict[str, str] = Field(default_factory=dict)\n\n    # Buffered events used by Context.collect_events. Maps buffer_id -> { fully.qualified.EventType: [serialized_event_str, ...] }.\n    # Each inner list element is a serializer-encoded string for an Event.\n    event_buffers: dict[str, dict[str, list[str]]] = Field(default_factory=dict)\n\n    # Events that were in-flight for each step at serialization time. Maps step_name -> [serialized_event_str, ...].\n    in_progress: dict[str, list[str]] = Field(default_factory=dict)\n\n    # Pairs recorded when a step produced an output event: (step_name, input_event_class_name).\n    # Note: stored as Python tuples here; if JSON-encoded externally they become 2-element lists.\n    accepted_events: list[tuple[str, str]] = Field(default_factory=list)\n\n    # Broker log of all dispatched events in order, as serializer-encoded strings.\n    broker_log: list[str] = Field(default_factory=list)\n\n    # Whether the workflow was running when serialized.\n    is_running: bool = Field(default=False)\n\n    # IDs currently waiting in wait_for_event to suppress duplicate waiter events. These IDs may appear\n    # as keys in `queues` (they are used as queue names for waiter-specific queues).\n    waiting_ids: list[str] = Field(default_factory=list)\n\n\nclass SerializedEventAttempt(BaseModel):\n    \"\"\"Serialized representation of an EventAttempt with retry information.\"\"\"\n\n    model_config = ConfigDict(arbitrary_types_allowed=True)\n\n    # The event being processed (as serializer-encoded string)\n    event: str\n    # Number of times this event has been attempted (0 for first attempt)\n    attempts: int = 0\n    # Unix timestamp of first attempt, or None if not yet attempted\n    first_attempt_at: float | None = None\n    # Most recent exception when this event is scheduled for retry, if any.\n    last_exception: SerializableOptionalException = None\n    # Unix timestamp of the most recent failure, or None.\n    last_failed_at: float | None = None\n    # Per-handler recovery counts on this event's lineage. Maps catch_error\n    # handler step name -> invocations so far. Empty on the main graph.\n    recovery_counts: dict[str, int] = Field(default_factory=dict)\n\n\nclass SerializedWaiter(BaseModel):\n    \"\"\"Serialized representation of a waiter created by wait_for_event.\"\"\"\n\n    # Unique waiter ID\n    waiter_id: str\n    # The original event that triggered the wait (serialized)\n    event: str\n    # Fully qualified name of the event type being waited for (e.g. \"mymodule.MyEvent\")\n    waiting_for_event: str\n    # Requirements dict for matching the waited-for event\n    has_requirements: bool = Field(default=False)\n    # Resolved event if available (serialized), None otherwise\n    resolved_event: str | None = None\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def deserialize_requirements(cls, v: dict[str, Any]) -> dict[str, Any]:\n        # handle old requirements object\n        if (\n            \"requirements\" in v\n            and isinstance(v[\"requirements\"], dict)\n            and len(v[\"requirements\"]) > 0\n        ):\n            v[\"has_requirements\"] = True\n        return v\n\n\nclass SerializedStepWorkerState(BaseModel):\n    \"\"\"Serialized representation of a step worker's state.\"\"\"\n\n    # Queue of events waiting to be processed (with retry info)\n    queue: list[SerializedEventAttempt] = Field(default_factory=list)\n    # Events currently being processed (no retry info needed, will be re-queued on failure)\n    in_progress: list[str] = Field(default_factory=list)\n    # Collected events for ctx.collect_events(), keyed by buffer_id -> [event, ...]\n    # Events are serialized strings\n    collected_events: dict[str, list[str]] = Field(default_factory=dict)\n    # Active waiters created by ctx.wait_for_event()\n    collected_waiters: list[SerializedWaiter] = Field(default_factory=list)\n\n\nclass SerializedContext(BaseModel):\n    \"\"\"\n    Current format for serialized context. Uses proper JSON structures instead of nested JSON strings.\n    This format better represents BrokerState needs including retry information and waiter state.\n    \"\"\"\n\n    # Version marker to distinguish from V0\n    version: int = Field(default=1)\n\n    # Serialized state store payload (same format as V0)\n    state: dict[str, Any] = Field(default_factory=dict)\n\n    # Whether the workflow was running when serialized\n    is_running: bool = Field(default=False)\n\n    # Per-step worker state with queues, in-progress events, collected events, and waiters\n    # Maps step_name -> SerializedStepWorkerState\n    workers: dict[str, SerializedStepWorkerState] = Field(default_factory=dict)\n\n    @staticmethod\n    def from_v0(v0: SerializedContextV0) -> \"SerializedContext\":\n        \"\"\"Convert V0 format to current format.\n\n        Note: V0 doesn't store retry information or waiter state, so these will be lost.\n        V0 also doesn't properly separate collected_events by buffer_id per step.\n        \"\"\"\n        workers: dict[str, SerializedStepWorkerState] = {}\n\n        # Convert queues and in_progress per step\n        all_step_names = (\n            set(v0.queues.keys())\n            | set(v0.in_progress.keys())\n            | set(v0.event_buffers.keys())\n        )\n\n        for step_name in all_step_names:\n            # Skip waiter-specific queues (identified by waiter IDs)\n            if step_name in v0.waiting_ids:\n                continue\n\n            queue_events: list[SerializedEventAttempt] = []\n\n            # Convert in_progress events to queue entries with no retry info\n            if step_name in v0.in_progress:\n                for event_str in v0.in_progress[step_name]:\n                    queue_events.append(\n                        SerializedEventAttempt(\n                            event=event_str, attempts=0, first_attempt_at=None\n                        )\n                    )\n\n            # Convert queued events\n            if step_name in v0.queues:\n                queue_str = v0.queues[step_name]\n                queue_list = json.loads(queue_str)\n                for event_str in queue_list:\n                    queue_events.append(\n                        SerializedEventAttempt(\n                            event=event_str, attempts=0, first_attempt_at=None\n                        )\n                    )\n\n            # Convert collected events (V0 doesn't track buffer_id properly, so we use \"default\")\n            collected: dict[str, list[str]] = {}\n            if step_name in v0.event_buffers:\n                # V0 format: step_name -> { event_type -> [event_str, ...] }\n                # We flatten this into a single \"default\" buffer\n                all_events = []\n                for event_list in v0.event_buffers[step_name].values():\n                    all_events.extend(event_list)\n                if all_events:\n                    collected[\"default\"] = all_events\n\n            workers[step_name] = SerializedStepWorkerState(\n                queue=queue_events,\n                in_progress=[],  # V0 in_progress are moved to queue\n                collected_events=collected,\n                collected_waiters=[],  # V0 doesn't store waiter state\n            )\n\n        return SerializedContext(\n            version=1,\n            state=v0.state,\n            is_running=v0.is_running,\n            workers=workers,\n        )\n\n    @staticmethod\n    def from_dict_auto(data: dict[str, Any]) -> \"SerializedContext\":\n        \"\"\"Parse a dict as either V0 or V1 format and return V1.\"\"\"\n        # Check if it has version field\n        if \"version\" in data and data[\"version\"] == 1:\n            return SerializedContext.model_validate(data)\n        else:\n            # Assume V0 format\n            v0 = SerializedContextV0.model_validate(data)\n            return SerializedContext.from_v0(v0)\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/external_context.py",
    "content": "\"\"\"External context - for handlers and code outside workflow steps.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import TYPE_CHECKING, Any, AsyncGenerator, Coroutine, Generic\n\nfrom typing_extensions import TypeVar\n\nfrom workflows.context.context_types import MODEL_T\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import StateStore\nfrom workflows.errors import WorkflowRuntimeError\nfrom workflows.events import StopEvent\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    SnapshottableAdapter,\n    V2RuntimeCompatibilityShim,\n    as_snapshottable_adapter,\n    as_v2_runtime_compatibility_shim,\n)\nfrom workflows.runtime.types.ticks import TickAddEvent, TickCancelRun, WorkflowTick\n\nif TYPE_CHECKING:\n    from workflows.context.serializers import BaseSerializer\n    from workflows.events import Event\n    from workflows.workflow import Workflow\n\nRunResultT = TypeVar(\"RunResultT\", default=Any)  # type: ignore[misc]\n\n\nclass ExternalContext(Generic[MODEL_T, RunResultT]):\n    \"\"\"Context for handler code and external workflow interaction.\n\n    Used by WorkflowHandler to send events into the workflow,\n    stream events out, and retrieve the final result.\n    \"\"\"\n\n    _workflow: Workflow\n    _external_adapter: ExternalRunAdapter\n    _workers: list[asyncio.Task[Any]]\n    _serializer: BaseSerializer\n\n    def __init__(\n        self,\n        workflow: Workflow,\n        external_adapter: ExternalRunAdapter,\n        serializer: BaseSerializer = JsonSerializer(),\n    ) -> None:\n        self._workflow = workflow\n        self._external_adapter = external_adapter\n        self._serializer = serializer\n        self._workers = []\n\n    @property\n    def is_running(self) -> bool:\n        \"\"\"Whether the workflow is currently running.\"\"\"\n        as_shim = as_v2_runtime_compatibility_shim(self._external_adapter)\n        if as_shim is None:\n            # Assume running if not v2 runtime compatible. This is mainly just used for resuming\n            # an interrupted serialized context, which is not supported the same in distributed runtimes\n            return True\n\n        return as_shim.is_running\n\n    def _execute_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:\n        \"\"\"Execute a coroutine as a background task.\"\"\"\n        task = asyncio.create_task(coro)\n        self._workers.append(task)\n\n        def _remove_task(_: asyncio.Task[Any]) -> None:\n            try:\n                self._workers.remove(task)\n            except ValueError:\n                # Task was already cleared during shutdown or cleanup.\n                pass\n\n        task.add_done_callback(_remove_task)\n        return task\n\n    @property\n    def _tick_log(self) -> list[WorkflowTick]:\n        \"\"\"Get the tick log from the snapshottable adapter.\"\"\"\n        return self._require_snapshottable().replay()\n\n    def _require_snapshottable(self) -> SnapshottableAdapter:\n        snapshottable = as_snapshottable_adapter(self._external_adapter)\n        if snapshottable is None:\n            raise WorkflowRuntimeError(\n                f\"Runtime of type {self._workflow.runtime.__class__.__qualname__} is not snapshottable\"\n            )\n        return snapshottable\n\n    @property\n    def _state(self) -> BrokerState:\n        \"\"\"Compute current state from init state and tick log.\"\"\"\n        from workflows.runtime.control_loop import rebuild_state_from_ticks\n\n        ticks = self._tick_log\n        snapshottable = self._require_snapshottable()\n        state = snapshottable.init_state\n        new_state = rebuild_state_from_ticks(state, ticks)\n        return new_state\n\n    @property\n    def store(self) -> StateStore[MODEL_T]:\n        \"\"\"Access workflow state store.\"\"\"\n        state_store = self._external_adapter.get_state_store()\n        if state_store is None:\n            raise RuntimeError(\"State store not available from adapter\")\n        return state_store  # type: ignore[return-value]\n\n    def send_event(self, message: Event, step: str | None = None) -> None:\n        \"\"\"Send an event into the workflow.\"\"\"\n        if step is not None:\n            self._workflow._validate_valid_step_message(step, message)\n\n        self._execute_task(\n            self._external_adapter.send_event(\n                TickAddEvent(event=message, step_name=step)\n            )\n        )\n\n    async def running_steps(self) -> list[str]:\n        \"\"\"Get list of currently running step names.\"\"\"\n        state = self._state\n        return [\n            step for step in state.workers.keys() if state.workers[step].in_progress\n        ]\n\n    def _require_v2_runtime_compatibility(self) -> V2RuntimeCompatibilityShim:\n        v2_shim = as_v2_runtime_compatibility_shim(self._external_adapter)\n        if v2_shim is None:\n            raise WorkflowRuntimeError(\n                f\"Runtime of type {self._workflow.runtime.__class__.__qualname__} is not v2 runtime compatible\"\n            )\n        return v2_shim\n\n    def get_result(self) -> StopEvent:\n        \"\"\"Get the workflow's final result. Raises if not yet complete.\"\"\"\n        result = self._require_v2_runtime_compatibility().get_result_or_none()\n        if result is None:\n            raise WorkflowRuntimeError(\n                f\"Workflow run with run_id {self._external_adapter.run_id} is not complete\"\n            )\n        return result\n\n    def stream_events(self) -> AsyncGenerator[Event, None]:\n        \"\"\"Stream events published by the workflow.\"\"\"\n        return self._external_adapter.stream_published_events()\n\n    def to_dict(self, serializer: BaseSerializer | None = None) -> dict[str, Any]:\n        \"\"\"Serialize context state for persistence.\"\"\"\n        active_serializer = serializer or self._serializer\n\n        # Fetch state store from adapter and serialize\n        state_data = {}\n        state_store = self._external_adapter.get_state_store()\n        if state_store is not None:\n            state_data = state_store.to_dict(active_serializer)\n\n        # Get the broker state\n        broker_state = self._state\n\n        context = broker_state.to_serialized(active_serializer)\n        context.state = state_data\n        return context.model_dump(mode=\"python\")\n\n    def cancel(self) -> None:\n        \"\"\"Request workflow cancellation.\"\"\"\n        self._execute_task(self._external_adapter.send_event(TickCancelRun()))\n\n    async def shutdown(self) -> None:\n        \"\"\"Cancel the running workflow and clean up resources.\n\n        Sends a cancel signal, cancels all outstanding workers (both external\n        and broker workers), and closes the adapter. State remains available\n        for inspection.\n        \"\"\"\n        await self._external_adapter.send_event(TickCancelRun())\n        # Clean up external context workers\n        for worker in self._workers:\n            worker.cancel()\n        self._workers.clear()\n        await self._external_adapter.close()\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/internal_context.py",
    "content": "\"\"\"Internal context - for step execution within workflows.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport time\nfrom collections import Counter, defaultdict\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING, Any, Coroutine, Generic, TypeVar, cast\n\nfrom workflows.context.context_types import MODEL_T\nfrom workflows.context.state_store import StateStore\nfrom workflows.errors import WorkflowRuntimeError\nfrom workflows.retry_policy import RetryInfo\nfrom workflows.runtime.types.results import (\n    AddCollectedEvent,\n    AddWaiter,\n    DeleteCollectedEvent,\n    DeleteWaiter,\n    StepWorkerContext,\n    StepWorkerStateContextVar,\n    WaitingForEvent,\n)\nfrom workflows.runtime.types.ticks import TickAddEvent\n\nif TYPE_CHECKING:\n    from workflows.events import Event\n    from workflows.runtime.types.plugin import InternalRunAdapter\n    from workflows.workflow import Workflow\n\nT = TypeVar(\"T\", bound=\"Event\")\n\nlogger = logging.getLogger(__name__)\n\n\nclass InternalContext(Generic[MODEL_T]):\n    \"\"\"Context for code running inside workflow step functions.\n\n    Provides access to state store, event collection, waiting for events,\n    and publishing to the event stream.\n    \"\"\"\n\n    _internal_adapter: InternalRunAdapter\n    _workflow: Workflow\n    _workers: list[asyncio.Task[Any]]\n\n    def __init__(\n        self,\n        internal_adapter: InternalRunAdapter,\n        workflow: Workflow,\n    ) -> None:\n        self._internal_adapter = internal_adapter\n        self._workflow = workflow\n        self._workers = []\n\n    def _execute_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:\n        \"\"\"Execute a coroutine as a tracked background task.\"\"\"\n        task = asyncio.create_task(coro)\n        self._workers.append(task)\n\n        def _on_done(t: asyncio.Task[Any]) -> None:\n            try:\n                self._workers.remove(t)\n            except ValueError:\n                # Task was already cleared during shutdown or cleanup.\n                pass\n            # Log exceptions from fire-and-forget tasks (cancelled is not an error)\n            if not t.cancelled():\n                exc = t.exception()\n                if exc is not None:\n                    logger.error(\n                        \"Background task failed with exception\",\n                        exc_info=(type(exc), exc, exc.__traceback__),\n                    )\n\n        task.add_done_callback(_on_done)\n        return task\n\n    def cancel_background_tasks(self) -> None:\n        \"\"\"Cancel all tracked background tasks.\"\"\"\n        for worker in self._workers:\n            worker.cancel()\n        self._workers.clear()\n\n    async def _finalize_step(self) -> None:\n        \"\"\"Await all background tasks and finalize the step.\n\n        Called after a step function completes to ensure all fire-and-forget\n        operations (e.g., write_event_to_stream, send_event) complete before\n        returning control to the control loop. This prevents non-deterministic\n        ordering of durable operations on replay.\n        \"\"\"\n        workers = self._workers[:]\n        if workers:\n            await asyncio.gather(*workers, return_exceptions=True)\n        await self._internal_adapter.finalize_step()\n\n    @staticmethod\n    def _get_step_ctx(fn: str) -> StepWorkerContext:\n        \"\"\"Get the current step worker context. Raises if not in a step.\"\"\"\n        try:\n            return StepWorkerStateContextVar.get()\n        except LookupError:\n            raise WorkflowRuntimeError(\n                f\"{fn} may only be called from within a step function\"\n            )\n\n    @property\n    def store(self) -> StateStore[MODEL_T]:\n        \"\"\"Access workflow state store.\"\"\"\n        state_store = self._internal_adapter.get_state_store()\n        if state_store is None:\n            raise RuntimeError(\"State store not available from adapter\")\n        return state_store  # type: ignore[return-value]\n\n    def collect_events(\n        self,\n        ev: Event,\n        expected: list[type[Event]],\n        buffer_id: str | None = None,\n    ) -> list[Event] | None:\n        \"\"\"Collect events until all expected types are received.\"\"\"\n        step_ctx = self._get_step_ctx(fn=\"collect_events\")\n\n        # If no events are expected, return an empty list immediately\n        if not expected:\n            return []\n\n        buffer_id = buffer_id or \"default\"\n        collected_events = step_ctx.state.collected_events.get(buffer_id, [])\n\n        remaining_event_types = Counter(expected) - Counter(\n            [type(e) for e in collected_events]\n        )\n\n        if remaining_event_types != Counter([type(ev)]):\n            if type(ev) in remaining_event_types:\n                step_ctx.returns.return_values.append(\n                    AddCollectedEvent(event_id=buffer_id, event=ev)\n                )\n            return None\n\n        total = []\n        by_type: dict[type[Event], list[Event]] = defaultdict(list)\n        for e in collected_events + [ev]:\n            by_type[type(e)].append(e)\n        # order by expected type\n        for e_type in expected:\n            total.append(by_type[e_type].pop(0))\n        # Clear the collected events when the step is complete\n        step_ctx.returns.return_values.append(DeleteCollectedEvent(event_id=buffer_id))\n        return total\n\n    def send_event(self, message: Event, step: str | None = None) -> None:\n        \"\"\"Send an event to trigger another step.\"\"\"\n        if step is not None:\n            self._workflow._validate_valid_step_message(step, message)\n\n        recovery_counts: dict[str, int] = {}\n        try:\n            recovery_counts = dict(\n                StepWorkerStateContextVar.get().retry.recovery_counts\n            )\n        except LookupError:\n            pass\n\n        self._execute_task(\n            self._internal_adapter.send_event(\n                TickAddEvent(\n                    event=message,\n                    step_name=step,\n                    recovery_counts=recovery_counts,\n                )\n            )\n        )\n\n    async def wait_for_event(\n        self,\n        event_type: type[T],\n        waiter_event: Event | None = None,\n        waiter_id: str | None = None,\n        requirements: dict[str, Any] | None = None,\n        timeout: float | None = 2000,\n    ) -> T:\n        \"\"\"Wait for an event of the specified type.\"\"\"\n        step_ctx = self._get_step_ctx(fn=\"wait_for_event\")\n\n        collected_waiters = step_ctx.state.collected_waiters\n        requirements = requirements or {}\n\n        # Generate a unique key for the waiter\n        event_str = f\"{event_type.__module__}.{event_type.__name__}\"\n        requirements_str = str(requirements)\n        waiter_id = waiter_id or f\"waiter_{event_str}_{requirements_str}\"\n\n        waiter = next((w for w in collected_waiters if w.waiter_id == waiter_id), None)\n        if waiter is not None and waiter.timed_out:\n            step_ctx.returns.return_values.append(DeleteWaiter(waiter_id=waiter_id))\n            raise asyncio.TimeoutError(f\"Timed out waiting for {event_type.__name__}\")\n        if waiter is None or waiter.resolved_event is None:\n            raise WaitingForEvent(\n                AddWaiter(\n                    waiter_id=waiter_id,\n                    requirements=requirements,\n                    timeout=timeout,\n                    event_type=event_type,\n                    waiter_event=waiter_event,\n                )\n            )\n        else:\n            step_ctx.returns.return_values.append(DeleteWaiter(waiter_id=waiter_id))\n            return cast(T, waiter.resolved_event)\n\n    def write_event_to_stream(self, ev: Event | None) -> None:\n        \"\"\"Write an event to the published event stream.\"\"\"\n        if ev is not None:\n            self._execute_task(self._internal_adapter.write_to_event_stream(ev))\n\n    def retry_info(self) -> RetryInfo:\n        \"\"\"Snapshot of the currently-executing step's retry state.\n\n        Returns a `RetryInfo(retry_number=0, elapsed_seconds=0.0,\n        last_exception=None, last_failed_at=None)` on the first attempt. After a\n        retry it reflects the current retry number, seconds since the first\n        attempt, and the most recent failure.\n\n        Raises:\n            WorkflowRuntimeError: If called outside of a step function.\n        \"\"\"\n        step_ctx = self._get_step_ctx(fn=\"retry_info\")\n        retry = step_ctx.retry\n        if retry.retry_number <= 0 or not retry.first_attempt_at:\n            elapsed = 0.0\n        else:\n            elapsed = max(0.0, time.time() - retry.first_attempt_at)\n        last_failed_at: datetime | None = (\n            datetime.fromtimestamp(retry.last_failed_at, tz=timezone.utc)\n            if retry.last_failed_at is not None\n            else None\n        )\n        return RetryInfo(\n            retry_number=retry.retry_number,\n            elapsed_seconds=elapsed,\n            last_exception=retry.last_exception,\n            last_failed_at=last_failed_at,\n        )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/pre_context.py",
    "content": "\"\"\"Pre-run context - configuration face before workflow execution.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, Generic, cast\n\nfrom pydantic import ValidationError\n\nfrom workflows.context.context_types import MODEL_T, SerializedContext\nfrom workflows.context.serializers import BaseSerializer, JsonSerializer\nfrom workflows.context.state_store import (\n    InMemoryStateStore,\n    StateStore,\n    infer_state_type,\n)\nfrom workflows.errors import ContextSerdeError\nfrom workflows.runtime.types.internal_state import BrokerState\n\nif TYPE_CHECKING:\n    from workflows.workflow import Workflow\n\n\nclass PreContext(Generic[MODEL_T]):\n    \"\"\"Context state before workflow starts.\n\n    Provides access to workflow configuration and serialization\n    for persistence/restoration. A staging store is lazily created\n    on first `.store` access and carried into the runtime when the\n    workflow starts.\n    \"\"\"\n\n    _init_snapshot: SerializedContext\n    _serializer: BaseSerializer\n    _workflow: \"Workflow\"\n    _store: InMemoryStateStore[MODEL_T] | None\n\n    def __init__(\n        self,\n        workflow: \"Workflow\",\n        previous_context: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> None:\n        self._serializer = serializer or JsonSerializer()\n        self._workflow = workflow\n        self._store = None\n\n        # Parse the serialized context\n        if previous_context is not None:\n            try:\n                # Auto-detect and convert V0 to V1 if needed\n                previous_context_parsed = SerializedContext.from_dict_auto(\n                    previous_context\n                )\n                # Validate it fully parses synchronously to avoid delayed validation errors\n                BrokerState.from_serialized(\n                    previous_context_parsed, workflow, self._serializer\n                )\n            except ValidationError as e:\n                raise ContextSerdeError(\n                    f\"Context dict specified in an invalid format: {e}\"\n                ) from e\n        else:\n            previous_context_parsed = SerializedContext()\n\n        self._init_snapshot = previous_context_parsed\n\n    @property\n    def store(self) -> StateStore[MODEL_T]:\n        \"\"\"Lazily-created staging store for pre-run state access.\n\n        For fresh contexts, the state type is inferred from workflow step\n        annotations. For deserialized contexts, the store is restored from\n        the serialized state data.\n        \"\"\"\n        if self._store is None:\n            serialized_state = self._init_snapshot.state\n            if serialized_state:\n                self._store = cast(\n                    InMemoryStateStore[MODEL_T],\n                    InMemoryStateStore.from_dict(serialized_state, self._serializer),\n                )\n            else:\n                state_type = infer_state_type(self._workflow)\n                self._store = cast(\n                    InMemoryStateStore[MODEL_T],\n                    InMemoryStateStore(state_type()),\n                )\n        return self._store\n\n    @property\n    def serialized_state(self) -> dict[str, Any] | None:\n        \"\"\"Return the serialized state for handoff to the runtime.\n\n        If the staging store was accessed, its current contents are\n        serialized.  Otherwise the snapshot's original state is returned\n        unchanged, avoiding unnecessary work.\n        \"\"\"\n        if self._store is not None:\n            return self._store.to_dict(self._serializer)\n        return self._init_snapshot.state\n\n    @property\n    def is_running(self) -> bool:\n        \"\"\"Whether the workflow is currently running.\n\n        Returns the is_running state from the init snapshot, which may be True\n        if restoring a context that was previously mid-run.\n        \"\"\"\n        return self._init_snapshot.is_running\n\n    @property\n    def init_snapshot(self) -> SerializedContext:\n        \"\"\"The initial serialized context snapshot.\"\"\"\n        return self._init_snapshot\n\n    @property\n    def serializer(self) -> BaseSerializer:\n        \"\"\"The serializer used for this context.\"\"\"\n        return self._serializer\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/py.typed",
    "content": ""
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/serializers.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport base64\nimport json\nimport pickle\nfrom abc import ABC, abstractmethod\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom .utils import get_qualified_name, import_module_from_qualified_name\n\n\nclass BaseSerializer(ABC):\n    \"\"\"\n    Interface for value serialization used by the workflow context and state store.\n\n    Implementations must encode arbitrary Python values into a string and be able\n    to reconstruct the original values from that string.\n\n    See Also:\n        - [JsonSerializer][workflows.context.serializers.JsonSerializer]\n        - [PickleSerializer][workflows.context.serializers.PickleSerializer]\n    \"\"\"\n\n    @abstractmethod\n    def serialize(self, value: Any) -> str: ...\n\n    @abstractmethod\n    def deserialize(self, value: str) -> Any: ...\n\n\nclass JsonSerializer(BaseSerializer):\n    \"\"\"\n    JSON-first serializer that understands Pydantic models and LlamaIndex components.\n\n    Behavior:\n    - Pydantic models are encoded as JSON with their qualified class name so they\n      can be faithfully reconstructed.\n    - LlamaIndex components (objects exposing `class_name` and `to_dict`) are\n      serialized to their dict form alongside the qualified class name.\n    - Dicts and lists are handled recursively.\n\n    Fallback for unsupported objects is to attempt JSON encoding directly; if it\n    fails, a `ValueError` is raised.\n\n    Examples:\n        ```python\n        s = JsonSerializer()\n        payload = s.serialize({\"x\": 1, \"y\": [2, 3]})\n        data = s.deserialize(payload)\n        assert data == {\"x\": 1, \"y\": [2, 3]}\n        ```\n\n    See Also:\n        - [BaseSerializer][workflows.context.serializers.BaseSerializer]\n        - [PickleSerializer][workflows.context.serializers.PickleSerializer]\n    \"\"\"\n\n    def serialize_value(self, value: Any) -> Any:\n        \"\"\"\n        Events with a wrapper type that includes type metadata, so that they can be reserialized into the original Event type.\n        Traverses dicts and lists recursively.\n\n        Args:\n            value (Any): The value to serialize.\n\n        Returns:\n            Any: The serialized value. A dict, list, string, number, or boolean.\n        \"\"\"\n        # This has something to do with BaseComponent from llama_index.core. Is it still needed?\n        if hasattr(value, \"class_name\"):\n            retval = {\n                \"__is_component\": True,\n                \"value\": value.to_dict(),\n                \"qualified_name\": get_qualified_name(value),\n            }\n            return retval\n\n        if isinstance(value, BaseModel):\n            return {\n                \"__is_pydantic\": True,\n                \"value\": value.model_dump(mode=\"json\"),\n                \"qualified_name\": get_qualified_name(value),\n            }\n\n        if isinstance(value, dict):\n            return {k: self.serialize_value(v) for k, v in value.items()}\n\n        if isinstance(value, list):\n            return [self.serialize_value(item) for item in value]\n\n        return value\n\n    def serialize(self, value: Any) -> str:\n        \"\"\"Serialize an arbitrary value to a JSON string.\n\n        Args:\n            value (Any): The value to encode.\n\n        Returns:\n            str: JSON string.\n\n        Raises:\n            ValueError: If the value cannot be encoded to JSON.\n        \"\"\"\n        try:\n            serialized_value = self.serialize_value(value)\n            return json.dumps(serialized_value)\n        except Exception:\n            raise ValueError(f\"Failed to serialize value: {type(value)}: {value!s}\")\n\n    def deserialize_value(self, data: Any) -> Any:\n        \"\"\"Helper to deserialize a single dict or other json value from its discriminator fields back into a python class.\n\n        Args:\n            data (Any): a dict, list, string, number, or boolean\n\n        Returns:\n            Any: The deserialized value.\n        \"\"\"\n        if isinstance(data, dict):\n            if data.get(\"__is_pydantic\") and data.get(\"qualified_name\"):\n                module_class = import_module_from_qualified_name(data[\"qualified_name\"])\n                return module_class.model_validate(data[\"value\"])\n            elif data.get(\"__is_component\") and data.get(\"qualified_name\"):\n                module_class = import_module_from_qualified_name(data[\"qualified_name\"])\n                return module_class.from_dict(data[\"value\"])\n            return {k: self.deserialize_value(v) for k, v in data.items()}\n        elif isinstance(data, list):\n            return [self.deserialize_value(item) for item in data]\n        return data\n\n    def deserialize(self, value: str) -> Any:\n        \"\"\"Deserialize a JSON string into Python objects.\n\n        Args:\n            value (str): JSON string.\n\n        Returns:\n            Any: The reconstructed value.\n        \"\"\"\n        data = json.loads(value)\n        return self.deserialize_value(data)\n\n\nclass PickleSerializer(JsonSerializer):\n    \"\"\"\n    Hybrid serializer: JSON when possible, Pickle as a safe fallback.\n\n    This serializer attempts JSON first for readability and portability, and\n    transparently falls back to Pickle for objects that cannot be represented in\n    JSON. Deserialization prioritizes Pickle and falls back to JSON.\n\n    Warning:\n        Pickle can execute arbitrary code during deserialization. Only\n        deserialize trusted payloads.\n\n    Note: Used to be called `JsonPickleSerializer` but it was renamed to `PickleSerializer`.\n\n    Examples:\n        ```python\n        s = PickleSerializer()\n        class Foo:\n            def __init__(self, x):\n                self.x = x\n        payload = s.serialize(Foo(1))  # will likely use Pickle\n        obj = s.deserialize(payload)\n        assert isinstance(obj, Foo)\n        ```\n    \"\"\"\n\n    def serialize(self, value: Any) -> str:\n        \"\"\"Serialize with JSON preference and Pickle fallback.\n\n        Args:\n            value (Any): The value to encode.\n\n        Returns:\n            str: Encoded string (JSON or base64-encoded Pickle bytes).\n        \"\"\"\n        try:\n            return super().serialize(value)\n        except Exception:\n            return base64.b64encode(pickle.dumps(value)).decode(\"utf-8\")\n\n    def deserialize(self, value: str) -> Any:\n        \"\"\"Deserialize with Pickle preference and JSON fallback.\n\n        Args:\n            value (str): Encoded string.\n\n        Returns:\n            Any: The reconstructed value.\n\n        Notes:\n            Use only with trusted payloads due to Pickle security implications.\n        \"\"\"\n        try:\n            return pickle.loads(base64.b64decode(value))\n        except Exception:\n            return super().deserialize(value)\n\n\nJsonPickleSerializer = PickleSerializer\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/state_store.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport warnings\nfrom contextlib import asynccontextmanager\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    AsyncContextManager,\n    AsyncGenerator,\n    Generic,\n    Literal,\n    Protocol,\n    runtime_checkable,\n)\n\nfrom pydantic import BaseModel, ValidationError, model_validator\nfrom typing_extensions import TypeVar\n\nfrom workflows.decorators import StepConfig\nfrom workflows.events import DictLikeModel\n\nfrom .serializers import BaseSerializer\n\nif TYPE_CHECKING:\n    from workflows.workflow import Workflow\n\nMAX_DEPTH = 1000\n\n# Keys set by pre-built workflows that are known to be unserializable in some cases.\nKNOWN_UNSERIALIZABLE_KEYS: tuple[str, ...] = (\"memory\",)\n\n\nclass InMemorySerializedState(BaseModel):\n    \"\"\"Serialized state containing actual data (from InMemoryStateStore).\"\"\"\n\n    store_type: Literal[\"in_memory\"] = \"in_memory\"\n    state_type: str = \"DictState\"\n    state_module: str = \"workflows.context.state_store\"\n    state_data: Any = (\n        None  # {\"_data\": {...}} for DictState, serialized string for typed\n    )\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def default_store_type(cls, data: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Default missing store_type to 'in_memory' for backwards compatibility.\"\"\"\n        if isinstance(data, dict) and \"store_type\" not in data:\n            data = {**data, \"store_type\": \"in_memory\"}\n        return data\n\n\ndef parse_in_memory_state(\n    data: dict[str, Any],\n) -> InMemorySerializedState:\n    \"\"\"Parse raw dict into InMemorySerializedState.\n\n    Args:\n        data: Serialized state payload from InMemoryStateStore.to_dict().\n\n    Returns:\n        InMemorySerializedState if the format is recognized.\n\n    Raises:\n        ValueError: If store_type is not 'in_memory' or missing.\n    \"\"\"\n    store_type = data.get(\"store_type\")\n\n    if store_type == \"in_memory\" or store_type is None:\n        # Backwards compat: missing store_type = InMemory\n        return InMemorySerializedState.model_validate(data)\n    else:\n        raise ValueError(\n            f\"Cannot parse store_type '{store_type}' as InMemorySerializedState. \"\n            \"Use the appropriate store's from_dict() method.\"\n        )\n\n\ndef serialize_dict_state_data(\n    state: DictState,\n    serializer: BaseSerializer,\n    known_unserializable_keys: tuple[str, ...] = KNOWN_UNSERIALIZABLE_KEYS,\n) -> dict[str, Any]:\n    \"\"\"Serialize DictState items to {\"_data\": {...}} format.\n\n    Args:\n        state: The DictState to serialize.\n        serializer: Strategy for encoding values.\n        known_unserializable_keys: Keys to skip with warning if they fail to serialize.\n\n    Returns:\n        Dict with {\"_data\": {...}} structure containing serialized values.\n\n    Raises:\n        ValueError: If serialization fails for a non-known-unserializable key.\n    \"\"\"\n    serialized_data = {}\n    for key, value in state.items():\n        try:\n            serialized_data[key] = serializer.serialize(value)\n        except Exception as e:\n            if key in known_unserializable_keys:\n                warnings.warn(\n                    f\"Skipping serialization of known unserializable key: {key} -- \"\n                    \"This is expected but will require this item to be set manually after deserialization.\",\n                    category=UnserializableKeyWarning,\n                )\n                continue\n            raise ValueError(f\"Failed to serialize state value for key {key}: {e}\")\n    return {\"_data\": serialized_data}\n\n\ndef create_in_memory_payload(\n    state: BaseModel,\n    serializer: BaseSerializer,\n    known_unserializable_keys: tuple[str, ...] = KNOWN_UNSERIALIZABLE_KEYS,\n) -> InMemorySerializedState:\n    \"\"\"Create InMemorySerializedState from any state model.\n\n    Args:\n        state: The Pydantic model to serialize (DictState or typed model).\n        serializer: Strategy for encoding values.\n        known_unserializable_keys: Keys to skip with warning (DictState only).\n\n    Returns:\n        InMemorySerializedState containing the serialized data.\n    \"\"\"\n    if isinstance(state, DictState):\n        state_data = serialize_dict_state_data(\n            state, serializer, known_unserializable_keys\n        )\n    else:\n        state_data = serializer.serialize(state)\n\n    return InMemorySerializedState(\n        state_type=type(state).__name__,\n        state_module=type(state).__module__,\n        state_data=state_data,\n    )\n\n\ndef traverse_path_step(obj: Any, segment: str) -> Any:\n    \"\"\"Follow one segment into obj (dict key, list index, or attribute).\n\n    Args:\n        obj: The object to traverse into.\n        segment: The path segment (dict key, list index, or attribute name).\n\n    Returns:\n        The value at the given segment.\n\n    Raises:\n        KeyError, IndexError, AttributeError: If the segment doesn't exist.\n    \"\"\"\n    if isinstance(obj, dict):\n        return obj[segment]\n\n    # Attempt list/tuple index\n    try:\n        idx = int(segment)\n        return obj[idx]\n    except (ValueError, TypeError, IndexError):\n        pass\n\n    # Fallback to attribute access (Pydantic models, normal objects)\n    return getattr(obj, segment)\n\n\ndef assign_path_step(obj: Any, segment: str, value: Any) -> None:\n    \"\"\"Assign value to segment of obj (dict key, list index, or attribute).\n\n    Args:\n        obj: The object to assign into.\n        segment: The path segment (dict key, list index, or attribute name).\n        value: The value to assign.\n    \"\"\"\n    if isinstance(obj, dict):\n        obj[segment] = value\n        return\n\n    # Attempt list/tuple index assignment\n    try:\n        idx = int(segment)\n        obj[idx] = value\n        return\n    except (ValueError, TypeError, IndexError):\n        pass\n\n    # Fallback to attribute assignment\n    setattr(obj, segment, value)\n\n\ndef get_by_path(state: Any, path: str, default: Any = Ellipsis) -> Any:\n    \"\"\"Get a nested value from state using a dot-separated path.\n\n    Args:\n        state: The root state object.\n        path: Dot-separated path, e.g. \"user.profile.name\".\n        default: If provided, return this when the path does not exist;\n            otherwise, raise ValueError.\n\n    Returns:\n        The resolved value.\n\n    Raises:\n        ValueError: If the path is invalid and no default is provided,\n            or if path depth exceeds MAX_DEPTH.\n    \"\"\"\n    segments = path.split(\".\") if path else []\n    if len(segments) > MAX_DEPTH:\n        raise ValueError(f\"Path length exceeds {MAX_DEPTH} segments\")\n\n    try:\n        value: Any = state\n        for segment in segments:\n            value = traverse_path_step(value, segment)\n    except Exception:\n        if default is not Ellipsis:\n            return default\n        raise ValueError(f\"Path '{path}' not found in state\")\n    return value\n\n\ndef set_by_path(state: Any, path: str, value: Any) -> None:\n    \"\"\"Set a nested value on state using a dot-separated path.\n\n    Intermediate dicts are created as needed.\n\n    Args:\n        state: The root state object (mutated in place).\n        path: Dot-separated path to write.\n        value: Value to assign.\n\n    Raises:\n        ValueError: If the path is empty or exceeds MAX_DEPTH.\n    \"\"\"\n    if not path:\n        raise ValueError(\"Path cannot be empty\")\n\n    segments = path.split(\".\")\n    if len(segments) > MAX_DEPTH:\n        raise ValueError(f\"Path length exceeds {MAX_DEPTH} segments\")\n\n    current = state\n    for segment in segments[:-1]:\n        try:\n            current = traverse_path_step(current, segment)\n        except (KeyError, AttributeError, IndexError, TypeError):\n            intermediate: Any = {}\n            assign_path_step(current, segment, intermediate)\n            current = intermediate\n\n    assign_path_step(current, segments[-1], value)\n\n\ndef merge_state(current_state: MODEL_T, incoming: BaseModel) -> MODEL_T:\n    \"\"\"Replace or merge incoming state onto current state.\n\n    If incoming is the same type (or subclass) of current, it replaces directly.\n    If current's type is a subclass of incoming's type (parent provided),\n    fields are merged preserving child-specific fields.\n\n    Args:\n        current_state: The existing state.\n        incoming: The new state to apply.\n\n    Returns:\n        The resulting state after merge/replace.\n\n    Raises:\n        ValueError: If the types are not compatible.\n    \"\"\"\n    current_type = type(current_state)\n    new_type = type(incoming)\n\n    if isinstance(incoming, current_type):\n        return incoming  # type: ignore[return-value]\n    elif issubclass(current_type, new_type):\n        parent_data = incoming.model_dump()\n        return current_type.model_validate(\n            {**current_state.model_dump(), **parent_data}\n        )\n    else:\n        raise ValueError(\n            f\"State must be of type {current_type.__name__} or a parent type, \"\n            f\"got {new_type.__name__}\"\n        )\n\n\ndef create_cleared_state(state_type: type[MODEL_T]) -> MODEL_T:\n    \"\"\"Create a default instance of the state type, wrapping ValidationError.\n\n    Args:\n        state_type: The state model class to instantiate.\n\n    Returns:\n        A new default instance.\n\n    Raises:\n        ValueError: If the model cannot be instantiated from defaults.\n    \"\"\"\n    try:\n        return state_type()\n    except ValidationError:\n        raise ValueError(\"State must have defaults for all fields\")\n\n\n# Only warn once about unserializable keys\nclass UnserializableKeyWarning(Warning):\n    pass\n\n\nwarnings.simplefilter(\"once\", UnserializableKeyWarning)\n\n\nclass DictState(DictLikeModel):\n    \"\"\"\n    Dynamic, dict-like Pydantic model for workflow state.\n\n    Used as the default state model when no typed state is provided. Behaves\n    like a mapping while retaining Pydantic validation and serialization.\n\n    Examples:\n        ```python\n        from workflows.context.state_store import DictState\n\n        state = DictState()\n        state[\"foo\"] = 1\n        state.bar = 2  # attribute-style access works for nested structures\n        ```\n\n    See Also:\n        - [InMemoryStateStore][workflows.context.state_store.InMemoryStateStore]\n    \"\"\"\n\n    def __init__(self, **params: Any):\n        super().__init__(**params)\n\n\n# Default state type is DictState for the generic type\nMODEL_T = TypeVar(\"MODEL_T\", bound=BaseModel, default=DictState)  # type: ignore[reportGeneralTypeIssues]\n\n\n@runtime_checkable\nclass StateStore(Protocol[MODEL_T]):\n    \"\"\"Protocol defining the async state store interface.\n\n    State stores hold a single Pydantic model instance representing global\n    workflow state. Implementations must be async-safe and support both\n    atomic operations and transactional edits.\n\n    This protocol enables runtime plugins to provide custom state store\n    implementations (e.g., backed by databases, Redis, or external services)\n    while maintaining API compatibility with the default\n    [InMemoryStateStore][workflows.context.state_store.InMemoryStateStore].\n\n    For remote state stores, `to_dict`/`from_dict` should serialize non-secret\n    connection info (e.g., URL, table name) rather than the full state contents,\n    since the actual state lives in the external service.\n\n    See Also:\n        - [InMemoryStateStore][workflows.context.state_store.InMemoryStateStore]\n        - [Context.store][workflows.context.context.Context.store]\n    \"\"\"\n\n    state_type: type[MODEL_T]\n\n    async def get_state(self) -> MODEL_T:\n        \"\"\"Return a copy of the current state model.\"\"\"\n        ...\n\n    async def set_state(self, state: MODEL_T) -> None:\n        \"\"\"Replace or merge into the current state model.\"\"\"\n        ...\n\n    async def get(self, path: str, default: Any = ...) -> Any:\n        \"\"\"Get a nested value using dot-separated paths.\"\"\"\n        ...\n\n    async def set(self, path: str, value: Any) -> None:\n        \"\"\"Set a nested value using dot-separated paths.\"\"\"\n        ...\n\n    async def clear(self) -> None:\n        \"\"\"Reset the state to its type defaults.\"\"\"\n        ...\n\n    def edit_state(self) -> AsyncContextManager[MODEL_T]:\n        \"\"\"Edit state transactionally under a lock.\"\"\"\n        ...\n\n    def to_dict(self, serializer: \"BaseSerializer\") -> dict[str, Any]:\n        \"\"\"Serialize state for persistence.\"\"\"\n        ...\n\n\nclass InMemoryStateStore(Generic[MODEL_T]):\n    \"\"\"\n    Default in-memory implementation of the [StateStore][workflows.context.state_store.StateStore] protocol.\n\n    Holds a single Pydantic model instance representing global workflow state.\n    When the generic parameter is omitted, it defaults to\n    [DictState][workflows.context.state_store.DictState] for flexible,\n    dictionary-like usage.\n\n    Thread-safety is ensured with an internal `asyncio.Lock`. Consumers can\n    either perform atomic reads/writes via `get_state` and `set_state`, or make\n    in-place, transactional edits via the `edit_state` context manager.\n\n    Examples:\n        Typed state model:\n\n        ```python\n        from pydantic import BaseModel\n        from workflows.context.state_store import InMemoryStateStore\n\n        class MyState(BaseModel):\n            count: int = 0\n\n        store = InMemoryStateStore(MyState())\n        async with store.edit_state() as state:\n            state.count += 1\n        ```\n\n        Dynamic state with `DictState`:\n\n        ```python\n        from workflows.context.state_store import InMemoryStateStore, DictState\n\n        store = InMemoryStateStore(DictState())\n        await store.set(\"user.profile.name\", \"Ada\")\n        name = await store.get(\"user.profile.name\")\n        ```\n\n    See Also:\n        - [Context.store][workflows.context.context.Context.store]\n    \"\"\"\n\n    state_type: type[MODEL_T]\n\n    def __init__(self, initial_state: MODEL_T):\n        self._state = initial_state\n        self.state_type = type(initial_state)\n\n    @functools.cached_property\n    def _lock(self) -> asyncio.Lock:\n        \"\"\"Lazy lock initialization for Python 3.14+ compatibility.\n\n        asyncio.Lock() requires a running event loop in Python 3.14+.\n        Using cached_property defers creation to first use in async context.\n        \"\"\"\n        return asyncio.Lock()\n\n    async def get_state(self) -> MODEL_T:\n        \"\"\"Return a shallow copy of the current state model.\n\n        Returns:\n            MODEL_T: A `.model_copy()` of the internal Pydantic model.\n        \"\"\"\n        return self._state.model_copy()\n\n    async def set_state(self, state: MODEL_T) -> None:\n        \"\"\"Replace or merge into the current state model.\n\n        If the provided state is the exact type of the current state, it replaces\n        the state entirely. If the provided state is a parent type (i.e., the\n        current state type is a subclass of the provided state type), the fields\n        from the parent are merged onto the current state, preserving any child\n        fields that aren't present in the parent.\n\n        This enables workflow inheritance where a base workflow step can call\n        set_state with a base state type without obliterating child state fields.\n\n        Args:\n            state (MODEL_T): New state, either the same type or a parent type.\n\n        Raises:\n            ValueError: If the types are not compatible (neither same nor parent).\n        \"\"\"\n        async with self._lock:\n            self._state = merge_state(self._state, state)\n\n    def to_dict(self, serializer: \"BaseSerializer\") -> dict[str, Any]:\n        \"\"\"Serialize the state and model metadata for persistence.\n\n        For `DictState`, each individual item is serialized using the provided\n        serializer since values can be arbitrary Python objects. For other\n        Pydantic models, defers to the serializer (e.g. JSON) which can leverage\n        model-aware encoding.\n\n        Args:\n            serializer (BaseSerializer): Strategy used to encode values.\n\n        Returns:\n            dict[str, Any]: A payload suitable for\n            [from_dict][workflows.context.state_store.InMemoryStateStore.from_dict].\n        \"\"\"\n        payload = create_in_memory_payload(self._state, serializer)\n        return payload.model_dump()\n\n    @classmethod\n    def from_dict(\n        cls, serialized_state: dict[str, Any], serializer: \"BaseSerializer\"\n    ) -> \"InMemoryStateStore[MODEL_T]\":\n        \"\"\"Restore a state store from a serialized payload.\n\n        Args:\n            serialized_state (dict[str, Any]): The payload produced by\n                [to_dict][workflows.context.state_store.InMemoryStateStore.to_dict].\n            serializer (BaseSerializer): Strategy to decode stored values.\n\n        Returns:\n            InMemoryStateStore[MODEL_T]: A store with the reconstructed model.\n\n        Raises:\n            ValueError: If the payload is not in_memory format.\n        \"\"\"\n        if not serialized_state:\n            return cls(DictState())  # type: ignore[arg-type]\n\n        # Validate it's in_memory format (raises ValueError if not)\n        parse_in_memory_state(serialized_state)\n\n        state_instance = deserialize_state_from_dict(serialized_state, serializer)\n        return cls(state_instance)  # type: ignore[arg-type]\n\n    @asynccontextmanager\n    async def edit_state(self) -> AsyncGenerator[MODEL_T, None]:\n        \"\"\"Edit state transactionally under a lock.\n\n        Yields the mutable model and writes it back on exit. This pattern avoids\n        read-modify-write races and keeps updates atomic.\n\n        Yields:\n            MODEL_T: The current state model for in-place mutation.\n        \"\"\"\n        async with self._lock:\n            state = self._state\n\n            yield state\n\n            self._state = state\n\n    async def get(self, path: str, default: Any = Ellipsis) -> Any:\n        \"\"\"Get a nested value using dot-separated paths.\n\n        Args:\n            path (str): Dot-separated path, e.g. \"user.profile.name\".\n            default (Any): If provided, return this when the path does not\n                exist; otherwise, raise `ValueError`.\n\n        Returns:\n            Any: The resolved value.\n\n        Raises:\n            ValueError: If the path is invalid and no default is provided or if\n                the path depth exceeds limits.\n        \"\"\"\n        async with self._lock:\n            return get_by_path(self._state, path, default)\n\n    async def set(self, path: str, value: Any) -> None:\n        \"\"\"Set a nested value using dot-separated paths.\n\n        Args:\n            path (str): Dot-separated path to write.\n            value (Any): Value to assign.\n\n        Raises:\n            ValueError: If the path is empty or exceeds the maximum depth.\n        \"\"\"\n        async with self._lock:\n            set_by_path(self._state, path, value)\n\n    async def clear(self) -> None:\n        \"\"\"Reset the state to its type defaults.\n\n        Raises:\n            ValueError: If the model type cannot be instantiated from defaults\n                (i.e., fields missing default values).\n        \"\"\"\n        await self.set_state(create_cleared_state(self._state.__class__))\n\n\ndef deserialize_dict_state_data(\n    data: dict[str, Any],\n    serializer: BaseSerializer,\n) -> DictState:\n    \"\"\"Deserialize DictState from {\"_data\": {...}} format.\n\n    Args:\n        data: Dict with {\"_data\": {...}} structure containing serialized values.\n        serializer: Strategy for decoding values.\n\n    Returns:\n        DictState with deserialized values.\n\n    Raises:\n        ValueError: If deserialization fails for any key.\n    \"\"\"\n    _data_serialized = data.get(\"_data\", {})\n    deserialized_data = {}\n    for key, value in _data_serialized.items():\n        try:\n            deserialized_data[key] = serializer.deserialize(value)\n        except Exception as e:\n            raise ValueError(f\"Failed to deserialize state value for key {key}: {e}\")\n    return DictState(_data=deserialized_data)\n\n\ndef deserialize_state_from_dict(\n    serialized_state: dict[str, Any],\n    serializer: \"BaseSerializer\",\n    state_type: type[BaseModel] | None = None,\n) -> BaseModel:\n    \"\"\"Deserialize state from a serialized payload.\n\n    This is the inverse of InMemoryStateStore.to_dict(). It handles both\n    DictState (with per-key serialization) and typed Pydantic models.\n\n    Args:\n        serialized_state: The payload from to_dict(), containing state_data,\n            state_type, and state_module.\n        serializer: Strategy to decode stored values.\n        state_type: Optional explicit state type. When provided, uses\n            issubclass to determine if it's DictState. When omitted, falls\n            back to reading state_type from the dict.\n\n    Returns:\n        The deserialized state model instance.\n\n    Raises:\n        ValueError: If deserialization fails for any key.\n    \"\"\"\n    state_data = serialized_state.get(\"state_data\", {})\n    state_type_name = serialized_state.get(\"state_type\", \"DictState\")\n\n    if state_type_name == \"DictState\":\n        _data_serialized = state_data.get(\"_data\", {})\n        deserialized_data = {}\n        for key, value in _data_serialized.items():\n            try:\n                deserialized_data[key] = serializer.deserialize(value)\n            except Exception as e:\n                raise ValueError(\n                    f\"Failed to deserialize state value for key {key}: {e}\"\n                )\n        return DictState(_data=deserialized_data)\n    else:\n        return serializer.deserialize(state_data)\n\n\ndef infer_state_type(workflow: \"Workflow\") -> type[BaseModel]:\n    \"\"\"Infer the state type from workflow step configs.\n\n    Looks at Context[T] annotations in step functions to determine\n    the expected state type. Returns DictState if no typed state is found.\n\n    Args:\n        workflow: The workflow to inspect for state type annotations.\n\n    Returns:\n        The inferred state type, or DictState if none found.\n\n    Raises:\n        ValueError: If multiple different state types are found.\n    \"\"\"\n    state_types: set[type[BaseModel]] = set()\n    for _, step_func in workflow._get_steps().items():\n        step_config: StepConfig = step_func._step_config\n        if (\n            step_config.context_state_type is not None\n            and step_config.context_state_type != DictState\n            and issubclass(step_config.context_state_type, BaseModel)\n        ):\n            state_types.add(step_config.context_state_type)\n\n    state_type: type[BaseModel]\n    if state_types:\n        state_type = _find_most_derived_state_type(state_types)\n    else:\n        state_type = DictState\n\n    return state_type\n\n\ndef _find_most_derived_state_type(state_types: set[type[BaseModel]]) -> type[BaseModel]:\n    \"\"\"Find the most derived (most specific) state type from a set of types.\n\n    All types must be in a single inheritance chain, i.e., one type must be\n    a subclass of all other types (the most derived type).\n\n    Args:\n        state_types: Set of state types to analyze.\n\n    Returns:\n        The most derived type in the inheritance hierarchy.\n\n    Raises:\n        ValueError: If types are not in a compatible inheritance hierarchy.\n    \"\"\"\n    type_list = list(state_types)\n\n    if len(type_list) == 1:\n        return type_list[0]\n\n    # Find the most derived type - it should be a subclass of all others\n    most_derived: type[BaseModel] | None = None\n\n    for candidate in type_list:\n        is_most_derived = True\n        for other in type_list:\n            if other is candidate:\n                continue\n            # candidate must be a subclass of other (or equal to it)\n            if not issubclass(candidate, other):\n                is_most_derived = False\n                break\n        if is_most_derived:\n            most_derived = candidate\n            break\n\n    if most_derived is None:\n        # No single type is a subclass of all others - incompatible hierarchy\n        raise ValueError(\n            \"Multiple state types are not in a compatible inheritance hierarchy. \"\n            \"All state types must share a common inheritance chain. Found: \"\n            + \", \".join([st.__name__ for st in state_types])\n        )\n\n    return most_derived\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/context/utils.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom importlib import import_module\nfrom typing import (\n    Any,\n)\n\n\ndef get_qualified_name(value: Any) -> str:\n    \"\"\"\n    Get the qualified name of a value.\n\n    Args:\n        value (Any): The value to get the qualified name for.\n\n    Returns:\n        str: The qualified name in the format 'module.class'.\n\n    Raises:\n        AttributeError: If value does not have __module__ or __class__ attributes\n\n    \"\"\"\n    try:\n        return value.__module__ + \".\" + value.__class__.__name__\n    except AttributeError as e:\n        raise AttributeError(f\"Object {value} does not have required attributes: {e}\")\n\n\ndef import_module_from_qualified_name(qualified_name: str) -> Any:\n    \"\"\"\n    Import a module from a qualified name.\n\n    Args:\n        qualified_name (str): The fully qualified name of the module to import.\n\n    Returns:\n        Any: The imported module object.\n\n    Raises:\n        ValueError: If qualified_name is empty or malformed\n        ImportError: If module cannot be imported\n        AttributeError: If attribute cannot be found in module\n\n    \"\"\"\n    if not qualified_name or \".\" not in qualified_name:\n        raise ValueError(\"Qualified name must be in format 'module.attribute'\")\n\n    module_path = qualified_name.rsplit(\".\", 1)\n    try:\n        module = import_module(module_path[0])\n        return getattr(module, module_path[1])\n    except ImportError as e:\n        raise ImportError(f\"Failed to import module {module_path[0]}: {e}\")\n    except AttributeError as e:\n        raise AttributeError(\n            f\"Attribute {module_path[1]} not found in module {module_path[0]}: {e}\"\n        )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/decorators.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport inspect\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Callable,\n    Generic,\n    Literal,\n    ParamSpec,\n    Protocol,\n    TypeVar,\n    cast,\n    overload,\n)\n\nfrom pydantic import BaseModel\n\nfrom .errors import WorkflowValidationError\nfrom .events import StepFailedEvent\nfrom .resource import ResourceDefinition\nfrom .utils import (\n    inspect_signature,\n    is_free_function,\n    validate_step_signature,\n)\n\nif TYPE_CHECKING:  # pragma: no cover\n    from .workflow import Workflow\nfrom .retry_policy import RetryPolicy\n\nWorkflowGraphCheck = Literal[\"reachability\", \"terminal_event\", \"dead_end\"]\nStepGraphCheck = Literal[\"reachability\", \"dead_end\"]\n\n\nStepRole = Literal[\"step\", \"catch_error\"]\n\n\n@dataclasses.dataclass\nclass StepConfig:\n    accepted_events: list[Any]\n    event_name: str\n    return_types: list[Any]\n    context_parameter: str | None\n    num_workers: int\n    retry_policy: RetryPolicy | None\n    resources: list[ResourceDefinition]\n    context_state_type: type[BaseModel] | None = None\n    skip_graph_checks: list[StepGraphCheck] = dataclasses.field(default_factory=list)\n    role: StepRole = \"step\"\n    # Only meaningful when role == \"catch_error\".\n    # None means wildcard — covers any step not claimed by a scoped handler.\n    catch_error_for_steps: list[str] | None = None\n    catch_error_max_recoveries: int = 1\n\n\n@dataclasses.dataclass(frozen=True)\nclass CatchErrorHandler:\n    \"\"\"Runtime descriptor for a ``@catch_error`` handler.\n\n    Precomputed by ``Workflow._validate()`` from the handler's ``StepConfig``;\n    consumed by the control loop's failure-routing branch and by\n    ``BrokerState.from_workflow``.\n    \"\"\"\n\n    step_name: str\n    for_steps: list[str] | None\n    max_recoveries: int\n\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\nR_co = TypeVar(\"R_co\", covariant=True)\n\n\nclass StepFunction(Protocol, Generic[P, R_co]):\n    \"\"\"A decorated function, that has some _step_config metadata from the @step decorator\"\"\"\n\n    _step_config: StepConfig\n\n    __name__: str\n    __qualname__: str\n\n    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: ...\n\n\n@overload\ndef step(func: Callable[P, R]) -> StepFunction[P, R]: ...\n\n\n@overload\ndef step(\n    *,\n    workflow: type[\"Workflow\"] | None = None,\n    num_workers: int = 4,\n    retry_policy: RetryPolicy | None = None,\n    skip_graph_checks: list[StepGraphCheck] | None = None,\n) -> Callable[[Callable[P, R]], StepFunction[P, R]]: ...\n\n\ndef step(\n    func: Callable[P, R] | None = None,\n    *,\n    workflow: type[\"Workflow\"] | None = None,\n    num_workers: int = 4,\n    retry_policy: RetryPolicy | None = None,\n    skip_graph_checks: list[StepGraphCheck] | None = None,\n) -> Callable[[Callable[P, R]], StepFunction[P, R]] | StepFunction[P, R]:\n    \"\"\"\n    Decorate a callable to declare it as a workflow step.\n\n    The decorator inspects the function signature to infer the accepted event\n    type, return event types, optional `Context` parameter (optionally with a\n    typed state model), and any resource injections via `typing.Annotated`.\n\n    When applied to free functions, provide the workflow class via\n    `workflow=MyWorkflow`. For instance methods, the association is automatic.\n\n    Args:\n        workflow (type[Workflow] | None): Workflow class to attach the free\n            function step to. Not required for methods.\n        num_workers (int): Number of workers for this step. Defaults to 4.\n        retry_policy (RetryPolicy | None): Optional retry policy for failures.\n        skip_graph_checks (list[str] | None): Graph validation checks to skip\n            for this step. Currently supports ``\"reachability\"`` to allow\n            intentionally unreachable steps.\n\n    Returns:\n        Callable: The original function, annotated with internal step metadata.\n\n    Raises:\n        WorkflowValidationError: If signature validation fails or when decorating\n            a free function without specifying `workflow`.\n\n    Examples:\n        Method step:\n\n        ```python\n        class MyFlow(Workflow):\n            @step\n            async def start(self, ev: StartEvent) -> StopEvent:\n                return StopEvent(result=\"done\")\n        ```\n\n        Free function step:\n\n        ```python\n        class MyWorkflow(Workflow):\n            pass\n\n        @step(workflow=MyWorkflow)\n        async def generate(ev: StartEvent) -> NextEvent: ...\n        ```\n    \"\"\"\n\n    def decorator(func: Callable[P, R]) -> StepFunction[P, R]:\n        localns = _capture_decorator_localns()\n        return _apply_step_decorator(\n            func,\n            num_workers=num_workers,\n            retry_policy=retry_policy,\n            workflow=workflow,\n            localns=localns,\n            skip_graph_checks=skip_graph_checks or [],\n        )\n\n    if func is not None:\n        # The decorator was used without parentheses, like `@step`\n        localns = _capture_callsite_localns()\n        return _apply_step_decorator(\n            func,\n            num_workers=num_workers,\n            retry_policy=retry_policy,\n            workflow=workflow,\n            localns=localns,\n            skip_graph_checks=skip_graph_checks or [],\n        )\n    return decorator\n\n\ndef make_step_function(\n    func: Callable[P, R],\n    num_workers: int = 4,\n    retry_policy: RetryPolicy | None = None,\n    localns: dict[str, Any] | None = None,\n    skip_graph_checks: list[StepGraphCheck] | None = None,\n) -> StepFunction[P, R]:\n    # This will raise providing a message with the specific validation failure\n    spec = inspect_signature(func, localns=localns)\n    validate_step_signature(spec)\n\n    event_name, accepted_events = next(iter(spec.accepted_events.items()))\n\n    casted = cast(StepFunction[P, R], func)\n    casted._step_config = StepConfig(\n        accepted_events=accepted_events,\n        event_name=event_name,\n        return_types=spec.return_types,\n        context_parameter=spec.context_parameter,\n        context_state_type=spec.context_state_type,\n        num_workers=num_workers,\n        retry_policy=retry_policy,\n        resources=spec.resources,\n        skip_graph_checks=skip_graph_checks or [],\n    )\n\n    return casted\n\n\ndef _apply_step_decorator(\n    func: Callable[P, R],\n    *,\n    num_workers: int,\n    retry_policy: RetryPolicy | None,\n    workflow: type[\"Workflow\"] | None,\n    localns: dict[str, Any] | None,\n    skip_graph_checks: list[StepGraphCheck],\n) -> StepFunction[P, R]:\n    if not isinstance(num_workers, int) or num_workers <= 0:\n        raise WorkflowValidationError(\"num_workers must be an integer greater than 0\")\n\n    func = make_step_function(\n        func,\n        num_workers=num_workers,\n        retry_policy=retry_policy,\n        localns=localns,\n        skip_graph_checks=skip_graph_checks,\n    )\n\n    # If this is a free function, call add_step() explicitly.\n    if is_free_function(func.__qualname__):\n        if workflow is None:\n            msg = f\"To decorate {func.__name__} please pass a workflow class to the @step decorator.\"\n            raise WorkflowValidationError(msg)\n        workflow.add_step(func)\n\n    return func\n\n\n@overload\ndef catch_error(func: Callable[P, R]) -> StepFunction[P, R]: ...\n\n\n@overload\ndef catch_error(\n    *,\n    for_steps: list[str] | None = None,\n    max_recoveries: int = 1,\n) -> Callable[[Callable[P, R]], StepFunction[P, R]]: ...\n\n\ndef catch_error(\n    func: Callable[P, R] | None = None,\n    *,\n    for_steps: list[str] | None = None,\n    max_recoveries: int = 1,\n) -> Callable[[Callable[P, R]], StepFunction[P, R]] | StepFunction[P, R]:\n    \"\"\"Mark a method as a handler for steps that exhaust their retries.\n\n    Handlers can be scoped to specific steps via `for_steps`, or left as\n    wildcards (default) to cover any step not claimed by a scoped handler.\n    Each handler has a per-lineage recovery budget (`max_recoveries`): when the\n    budget is exceeded the workflow fails instead of re-entering the handler.\n\n    A handler may return any event type — the graph validator checks that the\n    handler's sub-graph eventually terminates at a `StopEvent`.\n\n    Args:\n        for_steps: Step names this handler covers. `None` means wildcard.\n        max_recoveries: How many times this handler may be invoked per lineage\n            before the workflow fails. Must be >= 1. Defaults to 1.\n\n    Examples:\n        ```python\n        from workflows import Workflow, catch_error, step, Context\n        from workflows.events import StartEvent, StepFailedEvent, StopEvent\n\n        class MyFlow(Workflow):\n            @step(retry_policy=...)\n            async def fetch(self, ev: StartEvent) -> FetchedEvent: ...\n\n            @catch_error(for_steps=[\"fetch\"], max_recoveries=2)\n            async def handle_fetch(self, ctx: Context, ev: StepFailedEvent) -> FallbackEvent:\n                return FallbackEvent(...)\n\n            @catch_error  # wildcard; covers any step not owned by a scoped handler\n            async def handle_default(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n                return StopEvent(result={\"failed\": ev.step_name})\n        ```\n    \"\"\"\n\n    if not isinstance(max_recoveries, int) or max_recoveries < 1:\n        raise WorkflowValidationError(\n            \"@catch_error max_recoveries must be an integer >= 1\"\n        )\n    if for_steps is not None:\n        if not isinstance(for_steps, list) or not all(\n            isinstance(s, str) for s in for_steps\n        ):\n            raise WorkflowValidationError(\n                \"@catch_error for_steps must be None or a list of step name strings\"\n            )\n\n    def _apply(inner: Callable[P, R], localns: dict[str, Any]) -> StepFunction[P, R]:\n        step_fn = make_step_function(\n            inner,\n            num_workers=1,\n            retry_policy=None,\n            localns=localns,\n        )\n        accepted = step_fn._step_config.accepted_events\n        if len(accepted) != 1 or accepted[0] is not StepFailedEvent:\n            name = getattr(inner, \"__name__\", repr(inner))\n            raise WorkflowValidationError(\n                f\"@catch_error handler '{name}' must accept StepFailedEvent \"\n                f\"as its event parameter.\"\n            )\n        step_fn._step_config.role = \"catch_error\"\n        step_fn._step_config.catch_error_for_steps = (\n            list(for_steps) if for_steps is not None else None\n        )\n        step_fn._step_config.catch_error_max_recoveries = max_recoveries\n        return step_fn\n\n    if func is not None:\n        # bare usage: `@catch_error`\n        return _apply(func, _capture_callsite_localns())\n\n    def decorator(inner: Callable[P, R]) -> StepFunction[P, R]:\n        return _apply(inner, _capture_decorator_localns())\n\n    return decorator\n\n\ndef _capture_decorator_localns() -> dict[str, Any]:\n    frame = inspect.currentframe()\n    if frame is None or frame.f_back is None:\n        return {}\n\n    try:\n        decorator_frame = frame.f_back\n        localns: dict[str, Any] = {}\n        localns.update(decorator_frame.f_locals)\n        if decorator_frame.f_back is not None:\n            localns.update(decorator_frame.f_back.f_locals)\n        return localns\n    finally:\n        del frame\n\n\ndef _capture_callsite_localns() -> dict[str, Any]:\n    frame = inspect.currentframe()\n    if frame is None or frame.f_back is None or frame.f_back.f_back is None:\n        return {}\n\n    try:\n        callsite_frame = frame.f_back.f_back\n        localns: dict[str, Any] = {}\n        localns.update(callsite_frame.f_locals)\n        if callsite_frame.f_back is not None:\n            localns.update(callsite_frame.f_back.f_locals)\n        return localns\n    finally:\n        del frame\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/errors.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\n\nclass WorkflowValidationError(Exception):\n    \"\"\"Raised when the workflow configuration or step signatures are invalid.\"\"\"\n\n\nclass WorkflowTimeoutError(Exception):\n    \"\"\"Raised when a workflow run exceeds the configured timeout.\"\"\"\n\n\nclass WorkflowRuntimeError(Exception):\n    \"\"\"Raised for runtime errors during step execution or event routing.\"\"\"\n\n\nclass WorkflowDone(Exception):\n    \"\"\"Internal control-flow exception used to terminate workers at run end.\"\"\"\n\n\nclass WorkflowCancelledByUser(Exception):\n    \"\"\"Raised when a run is cancelled via the handler or programmatically.\"\"\"\n\n\nclass WorkflowStepDoesNotExistError(Exception):\n    \"\"\"Raised when addressing a step that does not exist in the workflow.\"\"\"\n\n\nclass WorkflowConfigurationError(Exception):\n    \"\"\"Raised when a logical configuration error is detected pre-run.\"\"\"\n\n\nclass ContextSerdeError(Exception):\n    \"\"\"Raised when serializing/deserializing a `Context` fails.\"\"\"\n\n\nclass ContextStateError(Exception):\n    \"\"\"Raised when a context method is called in the wrong state.\n\n    Context transitions between three states:\n    - PreContext: Before workflow starts (configuration)\n    - ExternalContext: During run, for handler/external code\n    - InternalContext: During run, for step execution\n    \"\"\"\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/events.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom _collections_abc import dict_items, dict_keys, dict_values\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Annotated, Any\n\nfrom pydantic import (\n    BaseModel,\n    ConfigDict,\n    Field,\n    PlainSerializer,\n    PlainValidator,\n    PrivateAttr,\n    model_serializer,\n)\n\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.utils import import_module_from_qualified_name\n\n\nclass DictLikeModel(BaseModel):\n    \"\"\"\n    Base Pydantic model class that mimics a dict interface for dynamic fields.\n\n    Known, typed fields behave like regular Pydantic attributes. Any extra\n    keyword arguments are stored in an internal dict and can be accessed through\n    both attribute and mapping semantics. This hybrid model enables flexible\n    event payloads while preserving validation for declared fields.\n\n    PrivateAttr:\n        _data (dict[str, Any]): Underlying Python dict for dynamic fields.\n    \"\"\"\n\n    model_config = ConfigDict(arbitrary_types_allowed=True)\n    _data: dict[str, Any] = PrivateAttr(default_factory=dict)\n\n    def __init__(self, **params: Any):\n        \"\"\"\n        __init__.\n\n        NOTE: fields and private_attrs are pulled from params by name.\n        \"\"\"\n        # extract and set fields, private attrs and remaining shove in _data\n        fields = {}\n        private_attrs = {}\n        data = {}\n        for k, v in params.items():\n            if k in self.__class__.model_fields:\n                fields[k] = v\n            elif k in self.__private_attributes__:\n                private_attrs[k] = v\n            else:\n                data[k] = v\n        super().__init__(**fields)\n        for private_attr, value in private_attrs.items():\n            super().__setattr__(private_attr, value)\n        if data:\n            self._data.update(data)\n\n    def __getattr__(self, __name: str) -> Any:\n        if (\n            __name in self.__private_attributes__\n            or __name in self.__class__.model_fields\n        ):\n            return super().__getattr__(__name)  # type: ignore\n        else:\n            if __name not in self._data:\n                raise AttributeError(\n                    f\"'{self.__class__.__name__}' object has no attribute '{__name}'\"\n                )\n            return self._data[__name]\n\n    def __setattr__(self, name: str, value: Any) -> None:\n        if name in self.__private_attributes__ or name in self.__class__.model_fields:\n            super().__setattr__(name, value)\n        else:\n            self._data.__setitem__(name, value)\n\n    def __getitem__(self, key: str) -> Any:\n        return self._data[key]\n\n    def __setitem__(self, key: str, value: Any) -> None:\n        self._data[key] = value\n\n    def get(self, key: str, default: Any = None) -> Any:\n        return self._data.get(key, default)\n\n    def __contains__(self, key: str) -> bool:\n        return key in self._data\n\n    def keys(self) -> \"dict_keys[str, Any]\":\n        return self._data.keys()\n\n    def values(self) -> \"dict_values[str, Any]\":\n        return self._data.values()\n\n    def items(self) -> \"dict_items[str, Any]\":\n        return self._data.items()\n\n    def __len__(self) -> int:\n        return len(self._data)\n\n    def __iter__(self) -> Any:\n        return iter(self._data)\n\n    def to_dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]:\n        return self._data\n\n    def __bool__(self) -> bool:\n        \"\"\"Make test `if event:` pass on Event instances.\"\"\"\n        return True\n\n    @model_serializer(mode=\"wrap\")\n    def custom_model_dump(self, handler: Any) -> dict[str, Any]:\n        data = handler(self)\n        # include _data in serialization\n        if self._data:\n            data[\"_data\"] = self._data\n        return data\n\n\nclass Event(DictLikeModel):\n    \"\"\"\n    Base class for all workflow events.\n\n    Events are light-weight, serializable payloads passed between steps.\n    They support both attribute and mapping access to dynamic fields.\n\n    Examples:\n        Subclassing with typed fields:\n\n        ```python\n        from pydantic import Field\n\n        class CustomEv(Event):\n            score: int = Field(ge=0)\n\n        e = CustomEv(score=10)\n        print(e.score)\n        ```\n\n    See Also:\n        - [StartEvent][workflows.events.StartEvent]\n        - [StopEvent][workflows.events.StopEvent]\n        - [InputRequiredEvent][workflows.events.InputRequiredEvent]\n        - [HumanResponseEvent][workflows.events.HumanResponseEvent]\n    \"\"\"\n\n    def __init__(self, **params: Any):\n        super().__init__(**params)\n\n\n_json_serializer = JsonSerializer()\n\n\ndef _serialize_event(event: Event) -> Any:\n    return _json_serializer.serialize_value(event)\n\n\ndef _deserialize_event(data: Any) -> Event:\n    return _json_serializer.deserialize_value(data)\n\n\nSerializableEvent = Annotated[\n    Event,\n    PlainSerializer(_serialize_event, return_type=Any),\n    PlainValidator(_deserialize_event),\n]\n\n\ndef _serialize_optional_event(event: Event | None) -> Any:\n    if event is None:\n        return None\n    return _json_serializer.serialize_value(event)\n\n\ndef _deserialize_optional_event(data: Any) -> Event | None:\n    if data is None:\n        return None\n    return _json_serializer.deserialize_value(data)\n\n\nSerializableOptionalEvent = Annotated[\n    Event | None,\n    PlainSerializer(_serialize_optional_event, return_type=Any),\n    PlainValidator(_deserialize_optional_event),\n]\n\n\ndef _serialize_exception(exc: Exception) -> dict[str, Any]:\n    exc_type = type(exc)\n    qualified_name = f\"{exc_type.__module__}.{exc_type.__qualname__}\"\n    return {\n        \"exception_type\": qualified_name,\n        \"exception_message\": str(exc),\n    }\n\n\ndef _deserialize_exception(data: Any) -> Exception:\n    if isinstance(data, Exception):\n        return data\n    exc_message = data[\"exception_message\"]\n    try:\n        exc_cls = import_module_from_qualified_name(data[\"exception_type\"])\n        return exc_cls(exc_message)\n    except (ImportError, AttributeError, ValueError):\n        return Exception(exc_message)\n\n\nSerializableException = Annotated[\n    Exception,\n    PlainSerializer(_serialize_exception, return_type=dict[str, Any]),\n    PlainValidator(_deserialize_exception),\n]\n\n\ndef _serialize_optional_exception(exc: Exception | None) -> Any:\n    if exc is None:\n        return None\n    return _serialize_exception(exc)\n\n\ndef _deserialize_optional_exception(data: Any) -> Exception | None:\n    if data is None:\n        return None\n    return _deserialize_exception(data)\n\n\nSerializableOptionalException = Annotated[\n    Exception | None,\n    PlainSerializer(_serialize_optional_exception, return_type=Any),\n    PlainValidator(_deserialize_optional_exception),\n]\n\n\ndef _serialize_event_type(event_type: type[Event]) -> str:\n    return f\"{event_type.__module__}.{event_type.__qualname__}\"\n\n\ndef _deserialize_event_type(data: Any) -> type[Event]:\n    if isinstance(data, type):\n        return data\n    return import_module_from_qualified_name(data)\n\n\nSerializableEventType = Annotated[\n    type[Event],\n    PlainSerializer(_serialize_event_type, return_type=str),\n    PlainValidator(_deserialize_event_type),\n]\n\n\nclass StartEvent(Event):\n    \"\"\"Implicit entry event sent to kick off a `Workflow.run()`.\"\"\"\n\n\nclass StopEvent(Event):\n    \"\"\"Terminal event that signals the workflow has completed.\n\n    The `result` property contains the return value of the workflow run. When a\n    custom stop event subclass is used, the workflow result is that event\n    instance itself.\n\n    Examples:\n        ```python\n        # default stop event: result holds the value\n        return StopEvent(result={\"answer\": 42})\n        ```\n\n        Subclassing to provide a custom result:\n\n        ```python\n        class MyStopEv(StopEvent):\n            pass\n\n        @step\n        async def my_step(self, ctx: Context, ev: StartEvent) -> MyStopEv:\n            return MyStopEv(result={\"answer\": 42})\n    \"\"\"\n\n    _result: Any = PrivateAttr(default=None)\n\n    def __init__(self, result: Any = None, **kwargs: Any) -> None:\n        # forces the user to provide a result\n        super().__init__(_result=result, **kwargs)\n\n    def _get_result(self) -> Any:\n        \"\"\"This can be overridden by subclasses to return the desired result.\"\"\"\n        return self._result\n\n    @property\n    def result(self) -> Any:\n        return self._get_result()\n\n    @model_serializer(mode=\"wrap\")\n    def custom_model_dump(self, handler: Any) -> dict[str, Any]:\n        data = handler(self)\n        # include _result in serialization for base StopEvent\n        if self._result is not None:\n            data[\"result\"] = self._result\n        return data\n\n    def __repr__(self) -> str:\n        dict_items = {**self._data, **self.model_dump()}\n        # Format as key=value pairs\n        parts = [f\"{k}={v!r}\" for k, v in dict_items.items()]\n        dict_str = \", \".join(parts)\n        return f\"{self.__class__.__name__}({dict_str})\"\n\n    def __str__(self) -> str:\n        return str(self._result)\n\n\nclass WorkflowTimedOutEvent(StopEvent):\n    \"\"\"Published when a workflow exceeds its configured timeout.\n\n    This event is published to the event stream when a workflow times out,\n    allowing consumers to understand why the workflow ended before the\n    WorkflowTimeoutError exception is raised.\n\n    Attributes:\n        timeout: The timeout duration in seconds that was exceeded.\n        active_steps: List of step names that were still active when the timeout occurred.\n\n    Examples:\n        ```python\n        async for event in handler.stream_events():\n            if isinstance(event, WorkflowTimedOutEvent):\n                print(f\"Workflow timed out after {event.timeout}s\")\n                print(f\"Active steps: {event.active_steps}\")\n        ```\n    \"\"\"\n\n    timeout: float\n    active_steps: list[str]\n\n\nclass WorkflowCancelledEvent(StopEvent):\n    \"\"\"Published when a workflow is cancelled by the user.\n\n    This event is published to the event stream when a workflow is cancelled\n    via the handler or programmatically, allowing consumers to understand why\n    the workflow ended before the WorkflowCancelledByUser exception is raised.\n\n    Examples:\n        ```python\n        async for event in handler.stream_events():\n            if isinstance(event, WorkflowCancelledEvent):\n                print(\"Workflow was cancelled by user\")\n        ```\n    \"\"\"\n\n\nclass IdleReleasedEvent(StopEvent):\n    \"\"\"Sentinel returned when a workflow is cleanly released due to idleness.\n\n    Unlike WorkflowCancelledEvent, this does not publish to the event stream\n    and does not raise an exception — the control loop simply returns this\n    event as the workflow result.\n    \"\"\"\n\n\nclass WorkflowFailedEvent(StopEvent):\n    \"\"\"Published when a workflow step fails permanently.\n\n    Published when a step fails and all retries are exhausted (or no retry\n    policy permits a retry, or a catch_error handler itself raised).\n\n    Attributes:\n        step_name: The name of the step that failed.\n        exception: The raised exception. ``__traceback__`` is present only\n            in-process; ``None`` after a replay.\n        attempts: The total number of attempts made before giving up.\n        elapsed_seconds: Time in seconds from first attempt to final failure.\n\n    Examples:\n        ```python\n        async for event in handler.stream_events():\n            if isinstance(event, WorkflowFailedEvent):\n                print(f\"Step '{event.step_name}' failed after {event.attempts} attempts\")\n                print(f\"{type(event.exception).__name__}: {event.exception}\")\n        ```\n    \"\"\"\n\n    step_name: str\n    exception: SerializableException\n    attempts: int\n    elapsed_seconds: float\n\n\nclass StepFailedEvent(Event):\n    \"\"\"Delivered to a `@catch_error` handler when a step exhausts its retries.\n\n    The handler may inspect the fields to decide how to recover. Returning a\n    `StopEvent` completes the workflow successfully; raising from the handler\n    propagates the new exception and fails the workflow.\n\n    Attributes:\n        step_name: The name of the step that failed.\n        input_event: The triggering event instance that caused the failure.\n        exception: The raised exception. ``__traceback__`` is present in-process\n            but ``None`` after the event has crossed a serialization boundary\n            (e.g., a replay).\n        attempts: Total number of attempts made before giving up.\n        elapsed_seconds: Seconds from first attempt to final failure.\n        failed_at: Timezone-aware UTC datetime of the final failure.\n    \"\"\"\n\n    step_name: str\n    input_event: SerializableEvent\n    exception: SerializableException\n    attempts: int\n    elapsed_seconds: float\n    failed_at: datetime\n\n\nclass InputRequiredEvent(Event):\n    \"\"\"Emitted when human input is required to proceed.\n\n    Automatically written to the event stream if returned from a step.\n\n    If returned from a step, it does not need to be consumed by other steps and will pass validation.\n    It's expected that the caller will respond to this event and send back a [HumanResponseEvent][workflows.events.HumanResponseEvent].\n\n    Use this directly or subclass it.\n\n    Typical flow: a step returns `InputRequiredEvent`, callers consume it from\n    the stream and send back a [HumanResponseEvent][workflows.events.HumanResponseEvent].\n\n    Examples:\n        ```python\n        from workflows.events import InputRequiredEvent, HumanResponseEvent\n\n        class HITLWorkflow(Workflow):\n            @step\n            async def my_step(self, ev: StartEvent) -> InputRequiredEvent:\n                return InputRequiredEvent(prefix=\"What's your name? \")\n\n            @step\n            async def my_step(self, ev: HumanResponseEvent) -> StopEvent:\n                return StopEvent(result=ev.response)\n        ```\n    \"\"\"\n\n\nclass HumanResponseEvent(Event):\n    \"\"\"Carries a human's response for a prior input request.\n\n    If consumed by a step and not returned by another, it will still pass validation.\n\n    Examples:\n        ```python\n        from workflows.events import InputRequiredEvent, HumanResponseEvent\n\n        class HITLWorkflow(Workflow):\n            @step\n            async def my_step(self, ev: StartEvent) -> InputRequiredEvent:\n                return InputRequiredEvent(prefix=\"What's your name? \")\n\n            @step\n            async def my_step(self, ev: HumanResponseEvent) -> StopEvent:\n                return StopEvent(result=ev.response)\n        ```\n    \"\"\"\n\n\nclass InternalDispatchEvent(Event):\n    \"\"\"\n    InternalDispatchEvent is a special event type that exposes processes running inside workflow, even if the user did not explicitly expose them by setting, e.g., `ctx.write_event_to_stream(`.\n\n    Examples:\n        ```python\n        wf = ExampleWorkflow()\n        handler = wf.run(message=\"Hello, who are you?\")\n\n        async for ev in handler.stream_event(expose_internal=True):\n            if isinstance(ev, InternalDispatchEvent):\n                print(type(ev), ev)\n        ```\n    \"\"\"\n\n    pass\n\n\nclass WorkflowIdleEvent(InternalDispatchEvent):\n    \"\"\"Emitted when workflow transitions to idle (waiting on external input).\n\n    A workflow is idle when:\n    1. The workflow is running (hasn't completed/failed/cancelled)\n    2. All steps have no pending events in their queues\n    3. All steps have no workers currently executing\n    4. At least one step has an active waiter (from ctx.wait_for_event())\n\n    This event is intentionally minimal - no metadata beyond the event type.\n    Resumption from idle is signaled by StepStateChanged with StepState.RUNNING.\n    \"\"\"\n\n    pass\n\n\nclass UnhandledEvent(InternalDispatchEvent):\n    \"\"\"Emitted when an incoming event is not handled by any step or waiter.\n\n    This helps callers understand when an external event is ignored and whether\n    the workflow is idle after processing the event.\n    \"\"\"\n\n    event_type: str = Field(description=\"Class name of the unhandled event.\")\n    qualified_name: str = Field(description=\"Fully qualified name of the event type.\")\n    step_name: str | None = Field(\n        default=None,\n        description=\"Target step name if the event was addressed to a step.\",\n    )\n    idle: bool = Field(description=\"Whether the workflow is idle after processing.\")\n\n\nclass StepState(Enum):\n    # is enqueued, but no capacity yet available to run\n    PREPARING = \"preparing\"\n    # is running now on a worker. Skips PREPARING if there is capacity available.\n    RUNNING = \"running\"\n    # is no longer running.\n    NOT_RUNNING = \"not_running\"\n\n\nclass StepStateChanged(InternalDispatchEvent):\n    \"\"\"\n    StepStateChanged is a special event type that exposes internal changes in the state of the event, including whether the step is running or in progress, what worker it is running on and what events it takes as input and output, as well as changes in the workflow state.\n\n    Attributes:\n        name (str): Name of the step\n        step_state (StepState): State of the step (\"running\", \"not_running\", \"in_progress\", \"not_in_progress\", \"exited\")\n        worker_id (str): ID of the worker that the step is running on\n        input_event_name (str): Name of the input event\n        output_event_name (Optional[str]): Name of the output event\n        context_state (dict[str, Any]): Snapshot of the current workflow state\n    \"\"\"\n\n    name: str = Field(description=\"Name of the step\")\n    step_state: StepState = Field(\n        description=\"State of the step ('running', 'not_running', 'in_progress', 'not_in_progress', 'exited')\"\n    )\n    worker_id: str = Field(description=\"ID of the worker that the step is running on\")\n    input_event_name: str = Field(description=\"Name of the input event\")\n    output_event_name: str | None = Field(\n        description=\"Name of the output event\", default=None\n    )\n\n\nEventType = type[Event]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/handler.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport warnings\nfrom collections.abc import Generator\nfrom typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable\n\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    as_v2_runtime_compatibility_shim,\n)\n\nfrom .errors import WorkflowCancelledByUser, WorkflowRuntimeError\nfrom .events import Event, InternalDispatchEvent, StopEvent, WorkflowCancelledEvent\nfrom .types import RunResultT\n\nif TYPE_CHECKING:\n    from .context import Context\n    from .workflow import Workflow\n\n\nclass WorkflowHandler(Awaitable[RunResultT]):\n    \"\"\"\n    Stable interface for communicating with a running workflow. Is awaitable and streamable, and supports things like cancellation.\n    \"\"\"\n\n    _ctx: Context\n\n    async def _await_result(self) -> RunResultT:\n        stop_event = await self.stop_event_result()\n        return stop_event.result if type(stop_event) is StopEvent else stop_event\n\n    def __await__(self) -> Generator[Any, Any, RunResultT]:\n        return self._await_result().__await__()\n\n    def __init__(\n        self,\n        workflow: \"Workflow\",\n        external_adapter: ExternalRunAdapter,\n        ctx: \"Context[Any] | None\" = None,\n    ) -> None:\n        from .context import Context\n\n        self._workflow = workflow\n        self._external_adapter = external_adapter\n        # TODO(v3): Remove ctx parameter. The handler will just be the external face.\n        self._ctx = (\n            ctx\n            if ctx is not None\n            else Context._create_external(\n                workflow=workflow, external_adapter=external_adapter\n            )\n        )\n        self.run_id = external_adapter.run_id\n        self._all_events_consumed = False\n        self._result: StopEvent | None = None\n        self._result_exception: BaseException | None = None\n        self._result_task = asyncio.create_task(self._wait_for_result())\n        self._result_task.add_done_callback(self._handle_result_task_done)\n\n    async def _wait_for_result(self) -> StopEvent:\n        result = await self._external_adapter.get_result()\n        self._result = result\n        return result\n\n    def _handle_result_task_done(self, task: asyncio.Task[StopEvent]) -> None:\n        if task.cancelled():\n            return\n        try:\n            exc = task.exception()\n        except asyncio.CancelledError:\n            return\n        if exc is None:\n            return\n        self._result_exception = exc\n        if isinstance(exc, WorkflowCancelledByUser) and self._result is None:\n            # Preserve cancellation in handler state without changing await semantics.\n            self._result = WorkflowCancelledEvent()\n\n    @property\n    def ctx(self) -> Context:\n        \"\"\"The workflow [Context][workflows.context.context.Context] for this run.\"\"\"\n        return self._ctx\n\n    def get_stop_event(self) -> StopEvent | None:\n        \"\"\"The stop event for this run. Always defined once the future is done. In a future major release, this will be removed, and the result will be the stop event itself.\"\"\"\n        return self._result\n\n    async def stop_event_result(self) -> StopEvent:\n        \"\"\"Get the stop event for this run. Always defined once the future is done. In a future major release, this will be removed, and the result will be the stop event itself.\"\"\"\n        return await self._result_task\n\n    def __str__(self) -> str:\n        return f\"WorkflowHandler(workflow={self._workflow.workflow_name}, run_id={self.run_id}, result={self._result})\"\n\n    def is_done(self) -> bool:\n        \"\"\"Return True when the workflow has completed.\"\"\"\n        return self._result_task.done()\n\n    async def stream_events(\n        self, expose_internal: bool = False\n    ) -> AsyncGenerator[Event, None]:\n        \"\"\"\n        Stream events from the workflow execution as they occur.\n\n        This method provides real-time access to events generated during workflow\n        execution, allowing for monitoring and processing of intermediate results.\n        Events are yielded in the order they are generated by the workflow.\n\n        The stream includes all events written to the context's streaming queue,\n        and terminates when a [StopEvent][workflows.events.StopEvent] is\n        encountered, indicating the workflow has completed.\n\n        Args:\n            expose_internal (bool): Whether to expose internal events.\n\n        Returns:\n            AsyncGenerator[Event, None]: An async generator that yields Event objects\n                as they are produced by the workflow.\n\n        Raises:\n            ValueError: If the context is not set on the handler.\n            WorkflowRuntimeError: If all events have already been consumed by a\n                previous call to `stream_events()` on the same handler instance.\n\n        Examples:\n            ```python\n            handler = workflow.run()\n\n            # Stream and process events in real-time\n            async for event in handler.stream_events():\n                if isinstance(event, StopEvent):\n                    print(f\"Workflow completed with result: {event.result}\")\n                else:\n                    print(f\"Received event: {event}\")\n\n            # Get final result\n            result = await handler\n            ```\n\n        Note:\n            Events can only be streamed once per handler instance. Subsequent\n            calls to `stream_events()` will raise a WorkflowRuntimeError.\n        \"\"\"\n\n        # Check if we already consumed all the streamed events\n        if self._all_events_consumed:\n            msg = \"All the streamed events have already been consumed.\"\n            raise WorkflowRuntimeError(msg)\n\n        async for ev in self.ctx.stream_events():\n            if isinstance(ev, InternalDispatchEvent) and not expose_internal:\n                continue\n            yield ev\n\n            if isinstance(ev, StopEvent):\n                self._all_events_consumed = True\n                break\n\n    def done(self) -> bool:\n        \"\"\"Return True when the workflow has completed.\"\"\"\n        _warn_done_deprecated()\n        return self._result_task.done()\n\n    def cancel(self) -> None:\n        \"\"\"Cancel the running workflow.\"\"\"\n        _warn_cancel_deprecated()\n        shim = as_v2_runtime_compatibility_shim(self._external_adapter)\n        if shim is None:\n            raise NotImplementedError(\n                \"Hard cancel is not supported by this runtime. \"\n                \"Use await handler.cancel_run() for graceful cancellation.\"\n            )\n        shim.abort()\n        self._result_task.cancel()\n\n    def exception(self) -> BaseException | None:\n        \"\"\"Get the exception for this run. Always defined once the future is done.\"\"\"\n        _warn_exception_deprecated()\n        try:\n            return self._result_task.exception()\n        except asyncio.CancelledError:\n            return None\n\n    def cancelled(self) -> bool:\n        \"\"\"Return True when the underlying workflow has been cancelled.\"\"\"\n        _warn_cancelled_deprecated()\n        if self._result_task.cancelled():\n            return True\n        exc = self.exception()\n        if exc is not None and isinstance(exc, WorkflowCancelledByUser):\n            return True\n        stop_event = self.get_stop_event()\n        if stop_event is not None and isinstance(stop_event, WorkflowCancelledEvent):\n            return True\n        return False\n\n    async def cancel_run(self, *, timeout: float = 5.0) -> None:\n        \"\"\"Cancel the running workflow.\n\n        Signals the underlying context to raise\n        [WorkflowCancelledByUser][workflows.errors.WorkflowCancelledByUser],\n        which will be caught by the workflow and gracefully end the run.\n\n        Examples:\n            ```python\n            handler = workflow.run()\n            await handler.cancel_run()\n            ```\n        \"\"\"\n        try:\n            await self._external_adapter.cancel()\n        except Exception:\n            pass\n        try:\n            await asyncio.wait_for(self._result_task, timeout=timeout)\n        except asyncio.TimeoutError:\n            pass\n        except asyncio.CancelledError:\n            pass\n        except Exception:\n            pass\n\n    async def send_event(self, event: Event, step: str | None = None) -> None:\n        \"\"\"Send an event into the workflow.\n\n        Args:\n            event: The event to send into the workflow.\n            step: Optional step name to target. If None, broadcasts to all.\n        \"\"\"\n        self.ctx.send_event(event, step)\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_done_deprecated() -> None:\n    warnings.warn(\n        \"WorkflowHandler.done() is deprecated and will be removed in a future release\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_cancel_deprecated() -> None:\n    warnings.warn(\n        \"WorkflowHandler.cancel() is deprecated and will be removed in a future release. Prefer to cancel the underlying workflow with await handler.cancel_run(), and then awaiting the result with await handler to obtain the cancellation exception.\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_exception_deprecated() -> None:\n    warnings.warn(\n        \"WorkflowHandler.exception() is deprecated and will be removed in a future release\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n\n\n@functools.lru_cache(maxsize=1)\ndef _warn_cancelled_deprecated() -> None:\n    warnings.warn(\n        \"WorkflowHandler.cancelled() is deprecated and will be removed in a future release\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/plugins/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Workflow runtime implementations.\"\"\"\n\nfrom workflows.plugins._context import get_current_runtime\nfrom workflows.plugins.basic import BasicRuntime, basic_runtime\n\n__all__ = [\n    \"get_current_runtime\",\n    \"basic_runtime\",\n    \"BasicRuntime\",\n]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/plugins/_context.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Context-scoped runtime access.\"\"\"\n\nfrom __future__ import annotations\n\nfrom workflows.runtime.types.plugin import Runtime, _current_runtime\n\n\ndef get_current_runtime() -> Runtime:\n    \"\"\"\n    Get the current runtime from context or fall back to basic_runtime.\n\n    Returns the context-scoped runtime if set, otherwise returns basic_runtime.\n    \"\"\"\n    # Inline import to avoid circular dependency (basic -> runtime -> workflow)\n    from workflows.plugins.basic import basic_runtime\n\n    runtime = _current_runtime.get()\n    return runtime if runtime is not None else basic_runtime\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/plugins/basic.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport time\nimport weakref\nfrom contextlib import asynccontextmanager, contextmanager\nfrom contextvars import ContextVar\nfrom typing import TYPE_CHECKING, Any, AsyncGenerator, Generator\n\nif TYPE_CHECKING:\n    from workflows.workflow import Workflow\n\nfrom llama_index_instrumentation import get_dispatcher\n\nfrom workflows.context.serializers import BaseSerializer, JsonSerializer\nfrom workflows.context.state_store import (\n    InMemoryStateStore,\n    StateStore,\n    infer_state_type,\n)\nfrom workflows.errors import WorkflowRuntimeError\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    SnapshottableAdapter,\n    V2RuntimeCompatibilityShim,\n    WaitResult,\n    WaitResultTick,\n    WaitResultTimeout,\n)\nfrom workflows.runtime.types.step_function import (\n    as_step_worker_functions,\n    create_workflow_run_function,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\nfrom workflows.workflow import Workflow\n\n\nclass AsyncioAdapterQueues:\n    \"\"\"Shared state between internal and external adapters.\n\n    The `complete` task is set by run_workflow() after instantiation due to\n    circular dependency: the task closure captures this object to prevent\n    premature GC from the WeakValueDictionary.\n    \"\"\"\n\n    # Set by run_workflow() after task creation\n    complete: asyncio.Task[StopEvent]\n\n    def __init__(\n        self,\n        run_id: str,\n        init_state: BrokerState,\n        state_store: StateStore[Any] | None = None,\n    ):\n        self.run_id = run_id\n        self.init_state = init_state\n        self.ticks: list[WorkflowTick] = []\n        self.state_store = state_store\n\n    # created lazily via cached_property for Python 3.14+ compatibility (they require a running event loop)\n    @functools.cached_property\n    def receive_queue(self) -> asyncio.Queue[WorkflowTick]:\n        return asyncio.Queue[WorkflowTick]()\n\n    # created lazily via cached_property for Python 3.14+ compatibility (they require a running event loop)\n    @functools.cached_property\n    def publish_queue(self) -> asyncio.Queue[Event]:\n        return asyncio.Queue[Event]()\n\n    # created lazily via cached_property for Python 3.14+ compatibility (they require a running event loop)\n    @functools.cached_property\n    def stream_lock(self) -> asyncio.Lock:\n        return asyncio.Lock()\n\n\nclass InternalAsyncioAdapter(InternalRunAdapter, SnapshottableAdapter):\n    \"\"\"\n    Internal adapter for asyncio-based workflow execution.\n\n    Used by the workflow control loop to receive ticks, publish events,\n    and manage timing. Also supports snapshotting for debugging/replay.\n    \"\"\"\n\n    def __init__(self, queues: AsyncioAdapterQueues) -> None:\n        self._queues = queues\n\n    @property\n    def run_id(self) -> str:\n        return self._queues.run_id\n\n    @property\n    def init_state(self) -> BrokerState:\n        return self._queues.init_state\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        self._queues.publish_queue.put_nowait(event)\n\n    async def get_now(self) -> float:\n        return time.monotonic()\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        self._queues.receive_queue.put_nowait(tick)\n\n    async def wait_receive(\n        self,\n        timeout_seconds: float | None = None,\n    ) -> WaitResult:\n        \"\"\"Wait for tick with optional timeout using asyncio primitives.\"\"\"\n        try:\n            if timeout_seconds is None:\n                tick = await self._queues.receive_queue.get()\n            else:\n                tick = await asyncio.wait_for(\n                    self._queues.receive_queue.get(),\n                    timeout=timeout_seconds,\n                )\n            return WaitResultTick(tick=tick)\n        except asyncio.TimeoutError:\n            return WaitResultTimeout()\n\n    async def on_tick(self, tick: WorkflowTick) -> None:\n        self._queues.ticks.append(tick)\n\n    def replay(self) -> list[WorkflowTick]:\n        return self._queues.ticks\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return self._queues.state_store\n\n\nclass ExternalAsyncioAdapter(\n    ExternalRunAdapter, SnapshottableAdapter, V2RuntimeCompatibilityShim\n):\n    \"\"\"\n    External adapter for asyncio-based workflow execution.\n\n    Used by external code to send events into the workflow\n    and stream events published by the workflow.\n    \"\"\"\n\n    def __init__(self, outer: BasicRuntime, queues: AsyncioAdapterQueues) -> None:\n        self._outer = outer\n        self._queues = queues\n\n    @property\n    def run_id(self) -> str:\n        return self._queues.run_id\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        self._queues.receive_queue.put_nowait(tick)\n\n    async def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        async with self._queues.stream_lock:\n            if self._queues.complete.done() and self._queues.publish_queue.empty():\n                raise WorkflowRuntimeError(\n                    \"Event stream already consumed. \"\n                    \"Events can only be streamed once per workflow run.\"\n                )\n            while True:\n                item = await self._queues.publish_queue.get()\n                yield item\n                if isinstance(item, StopEvent):\n                    break\n\n    def replay(self) -> list[WorkflowTick]:\n        return self._queues.ticks\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return self._queues.state_store\n\n    async def get_result(self) -> StopEvent:\n        return await self._queues.complete\n\n    def get_result_or_none(self) -> StopEvent | None:\n        if not self._queues.complete.done():\n            return None\n        return self._queues.complete.result()\n\n    @property\n    def is_running(self) -> bool:\n        return not self._queues.complete.done()\n\n    def abort(self) -> None:\n        \"\"\"Abort by cancelling the control loop task.\"\"\"\n        if not self._queues.complete.done():\n            self._queues.complete.cancel()\n        self._outer._queues.pop(self.run_id, None)\n\n    @property\n    def init_state(self) -> BrokerState:\n        return self._queues.init_state\n\n\nclass BasicRuntime(Runtime):\n    \"\"\"Default asyncio-based runtime with no durability.\"\"\"\n\n    @property\n    def is_launched(self) -> bool:\n        # BasicRuntime doesn't require launch() — always ready\n        return True\n\n    def __init__(self) -> None:\n        super().__init__()\n        # WeakValueDictionary allows queues to be GC'd when no adapters reference them.\n        # The task closure in run_workflow() captures a strong reference, keeping\n        # queues alive for fire-and-forget workflows even if the external adapter is dropped.\n        self._queues: weakref.WeakValueDictionary[str, AsyncioAdapterQueues] = (\n            weakref.WeakValueDictionary()\n        )\n        # Keyed by id(workflow) so each instance has independent concurrency limits\n        self._max_concurrent_runs: weakref.WeakValueDictionary[\n            int, asyncio.Semaphore\n        ] = weakref.WeakValueDictionary()\n\n    def register(self, workflow: Workflow) -> RegisteredWorkflow:\n        return RegisteredWorkflow(\n            workflow=workflow,\n            workflow_run_fn=create_workflow_run_function(workflow),\n            steps=as_step_worker_functions(workflow),\n        )\n\n    def _get_or_create_queues(\n        self, run_id: str, init_state: BrokerState\n    ) -> AsyncioAdapterQueues:\n        \"\"\"Get existing queues or create new ones for a run_id.\"\"\"\n        queues = self._queues.get(run_id)\n        if queues is None:\n            queues = AsyncioAdapterQueues(run_id=run_id, init_state=init_state)\n            self._queues[run_id] = queues\n        return queues\n\n    @asynccontextmanager\n    async def _maybe_acquire_max_concurrent_runs(\n        self, workflow: Workflow, run_id: str\n    ) -> AsyncGenerator[None, None]:\n        if workflow._num_concurrent_runs is None:\n            yield\n        else:\n            # Key by instance id so each workflow instance has independent concurrency limits\n            workflow_id = id(workflow)\n            if workflow_id in self._max_concurrent_runs:\n                sem = self._max_concurrent_runs[workflow_id]\n            else:\n                sem = asyncio.Semaphore(workflow._num_concurrent_runs)\n                self._max_concurrent_runs[workflow_id] = sem\n            async with sem:\n                yield\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> ExternalRunAdapter:\n        \"\"\"Set up a workflow run. Currently only creates state store.\n\n        Note: Execution is still managed by the broker for now. This will\n        change as we refactor to have the runtime fully own execution.\n        \"\"\"\n        if run_id in self._queues:\n            # not supported in any way right now. Might make sense to support run as new, or some other idempotency semantics\n            raise RuntimeError(f\"Workflow run with run_id '{run_id}' already exists.\")\n\n        registered = self.get_or_register(workflow)\n\n        # Create state store from serialized state or infer type from workflow\n        active_serializer = serializer or JsonSerializer()\n        if serialized_state:\n            state_store = InMemoryStateStore.from_dict(\n                serialized_state, active_serializer\n            )\n        else:\n            # Infer state type from workflow step configs\n            state_type = infer_state_type(registered.workflow)\n            state_store = InMemoryStateStore(state_type())\n        # might want to lock this better. Unlikely race condition if you spam with the same run_id.\n        queues = self._get_or_create_queues(run_id, init_state)\n        queues.state_store = state_store\n\n        # Capture propagation context (otel trace, instrument tags, etc.)\n        # BEFORE creating the task — contextvars won't be inherited.\n        captured_tags = get_dispatcher().capture_propagation_context()\n\n        async def run_with_concurrency_limit() -> StopEvent:\n            # Capture strong reference to queues for the task's lifetime,\n            # enabling fire-and-forget even if the caller drops the external adapter.\n            _ = queues\n            async with self._maybe_acquire_max_concurrent_runs(workflow, run_id):\n                return await registered.workflow_run_fn(\n                    init_state, start_event, captured_tags\n                )\n\n        with setting_run_id(run_id):\n            # actually pump the task through the runtime\n            task = asyncio.create_task(run_with_concurrency_limit())\n            queues.complete = task\n            return self.get_external_adapter(run_id)\n\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        run_id = get_current_run_id()\n        if run_id is None:\n            raise RuntimeError(\n                \"No current run id. Must be called within a workflow run.\"\n            )\n        if run_id not in self._queues:\n            raise RuntimeError(\n                f\"No queues found for run_id '{run_id}'. Must be called within a workflow run.\"\n            )\n        queues = self._queues[run_id]\n        return InternalAsyncioAdapter(queues)\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        if run_id not in self._queues:\n            raise RuntimeError(f\"No active workflow with run_id '{run_id}'. \")\n        return ExternalAsyncioAdapter(self, self._queues[run_id])\n\n\n_current_run_id: ContextVar[str | None] = ContextVar(\"current_run_id\", default=None)\n\n\ndef get_current_run_id() -> str | None:\n    \"\"\"Get the current run ID, if set.\"\"\"\n    return _current_run_id.get()\n\n\n@contextmanager\ndef setting_run_id(run_id: str) -> Generator[None, None, None]:\n    \"\"\"Set the current run ID for the duration of the block.\"\"\"\n    token = _current_run_id.set(run_id)\n    try:\n        yield\n    finally:\n        _current_run_id.reset(token)\n\n\nbasic_runtime = BasicRuntime()\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/protocol/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Re-export protocol types from the optional llama-agents-client package.\"\"\"\n\nimport warnings\n\nwarnings.warn(\n    \"Importing from 'workflows.protocol' is deprecated. \"\n    \"Install 'llama-agents-client' and use \"\n    \"'from llama_agents.client.protocol import ...' instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\ntry:\n    from llama_agents.client.protocol import (\n        CancelHandlerResponse,\n        HandlerData,\n        HandlersListResponse,\n        HealthResponse,\n        SendEventResponse,\n        Status,\n        WorkflowEventsListResponse,\n        WorkflowGraphResponse,\n        WorkflowSchemaResponse,\n        WorkflowsListResponse,\n        is_status_completed,\n    )\nexcept ImportError as e:\n    raise ImportError(\n        \"workflows.protocol requires the 'client' extra. \"\n        \"Install with: pip install 'llama-index-workflows[client]'\"\n    ) from e\n\n__all__ = [\n    \"Status\",\n    \"is_status_completed\",\n    \"HandlerData\",\n    \"HandlersListResponse\",\n    \"HealthResponse\",\n    \"WorkflowsListResponse\",\n    \"SendEventResponse\",\n    \"CancelHandlerResponse\",\n    \"WorkflowSchemaResponse\",\n    \"WorkflowEventsListResponse\",\n    \"WorkflowGraphResponse\",\n]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/py.typed",
    "content": ""
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/representation/__init__.py",
    "content": "from workflows.representation.build import get_workflow_representation\nfrom workflows.representation.types import (\n    WorkflowEventNode,\n    WorkflowExternalNode,\n    WorkflowGenericNode,\n    WorkflowGraph,\n    WorkflowGraphEdge,\n    WorkflowGraphNode,\n    WorkflowNodeBase,\n    WorkflowResourceConfigNode,\n    WorkflowResourceNode,\n    WorkflowStepNode,\n)\n\n__all__ = [\n    # Types\n    \"WorkflowNodeBase\",\n    \"WorkflowStepNode\",\n    \"WorkflowEventNode\",\n    \"WorkflowExternalNode\",\n    \"WorkflowResourceNode\",\n    \"WorkflowResourceConfigNode\",\n    \"WorkflowGenericNode\",\n    \"WorkflowGraphNode\",\n    \"WorkflowGraphEdge\",\n    \"WorkflowGraph\",\n    # Functions\n    \"get_workflow_representation\",\n]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/representation/build.py",
    "content": "from __future__ import annotations\n\nimport hashlib\nimport inspect\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom workflows import Workflow\nfrom workflows.decorators import StepFunction\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StopEvent,\n)\nfrom workflows.representation.types import (\n    WorkflowEventNode,\n    WorkflowExternalNode,\n    WorkflowGraph,\n    WorkflowGraphEdge,\n    WorkflowGraphNode,\n    WorkflowResourceConfigNode,\n    WorkflowResourceNode,\n    WorkflowStepNode,\n)\nfrom workflows.resource import (\n    ResourceDefinition,\n    ResourceDescriptor,\n    _get_resource_config_data,\n    _Resource,\n    _ResourceConfig,\n)\n\n\ndef _get_type_name(type_annotation: type | None) -> str | None:\n    \"\"\"Extract a readable type name from a type annotation.\"\"\"\n    if type_annotation is None:\n        return None\n    if hasattr(type_annotation, \"__name__\"):\n        return type_annotation.__name__\n    return str(type_annotation)\n\n\ndef _get_resource_identity(resource: ResourceDescriptor) -> int:\n    \"\"\"Get a unique identifier for resource deduplication.\n\n    For _Resource, uses the factory function identity.\n    For _ResourceConfig, uses (config_file, path_selector) hash.\n    \"\"\"\n    if isinstance(resource, _Resource):\n        return id(resource._factory)\n    if isinstance(resource, _ResourceConfig):\n        # Use hash of config_file + path_selector for deduplication\n        hash_input = f\"{resource.config_file}:{resource.path_selector or ''}\"\n        return hash(hash_input)\n    return id(resource)\n\n\ndef _get_event_type_chain(cls: type) -> list[str]:\n    \"\"\"Get the event type inheritance chain including the class itself.\n\n    Returns a list starting with the class name, followed by parent Event\n    subclasses up to (but not including) Event itself.\n    \"\"\"\n    names: list[str] = [cls.__name__]\n    for parent in cls.mro()[1:]:\n        if parent is Event:\n            break\n        if isinstance(parent, type) and issubclass(parent, Event):\n            names.append(parent.__name__)\n    return names\n\n\ndef _create_resource_config_node(\n    resource_config: _ResourceConfig,\n    type_annotation: type | None,\n) -> WorkflowResourceConfigNode:\n    \"\"\"Create a WorkflowResourceConfigNode from a _ResourceConfig.\"\"\"\n    # Compute unique hash for deduplication based on config file and path selector\n    hash_input = f\"{resource_config.config_file}:{resource_config.path_selector or ''}\"\n    unique_hash = hashlib.sha256(hash_input.encode()).hexdigest()[:12]\n\n    node_id = f\"resource_config_{unique_hash}\"\n    type_name = _get_type_name(type_annotation)\n    # Prefer explicit label, then type name, then config file path\n    label = resource_config.label or type_name or resource_config.config_file\n\n    # Extract JSON schema if type is a BaseModel\n    config_schema: dict[str, Any] | None = None\n    if (\n        type_annotation is not None\n        and isinstance(type_annotation, type)\n        and issubclass(type_annotation, BaseModel)\n    ):\n        model_cls: type[BaseModel] = type_annotation\n        config_schema = model_cls.model_json_schema()\n\n    # Read config value using existing infrastructure\n    config_value = _get_resource_config_data(\n        resource_config.config_file, resource_config.path_selector\n    )\n\n    return WorkflowResourceConfigNode(\n        id=node_id,\n        label=label,\n        type_name=type_name,\n        config_file=resource_config.config_file,\n        path_selector=resource_config.path_selector,\n        config_schema=config_schema,\n        config_value=config_value,\n        description=resource_config.description,\n    )\n\n\ndef _create_resource_node(resource_def: ResourceDefinition) -> WorkflowResourceNode:\n    \"\"\"Create a WorkflowResourceNode from a ResourceDefinition.\n\n    Extracts metadata (source file, line number, docstring) lazily here\n    rather than at Resource creation time for performance.\n    \"\"\"\n    resource = resource_def.resource\n    type_name = _get_type_name(resource_def.type_annotation)\n\n    # Extract source metadata lazily - only available for _Resource with factory\n    source_file: str | None = None\n    source_line: int | None = None\n    resource_description: str | None = None\n\n    if isinstance(resource, _Resource):\n        factory = resource._factory\n        source_file = inspect.getfile(factory)  # type: ignore[arg-type]\n        _, source_line = inspect.getsourcelines(factory)  # type: ignore[arg-type]\n        resource_description = inspect.getdoc(factory)\n\n    # Compute unique hash for deduplication\n    hash_input = f\"{resource.name}:{source_file or 'unknown'}\"\n    unique_hash = hashlib.sha256(hash_input.encode()).hexdigest()[:12]\n\n    # Label: prefer type_name, then getter_name, then id\n    node_id = f\"resource_{unique_hash}\"\n    label = type_name or resource.name or node_id\n\n    return WorkflowResourceNode(\n        id=node_id,\n        label=label,\n        type_name=type_name,\n        getter_name=resource.name,\n        source_file=source_file,\n        source_line=source_line,\n        description=resource_description,\n    )\n\n\ndef get_workflow_representation(workflow: Workflow | type[Workflow]) -> WorkflowGraph:\n    \"\"\"Build a graph representation of a workflow's structure.\n\n    Extracts the workflow's steps, events, and resources into a WorkflowGraph\n    that can be used for visualization or analysis.\n\n    Args:\n        workflow: A workflow instance or workflow class to build a representation for.\n\n    Returns:\n        A WorkflowGraph containing nodes for steps, events, resources,\n        and external interactions, with edges showing the data flow.\n    \"\"\"\n    # Get workflow steps\n    workflow_cls = workflow if isinstance(workflow, type) else type(workflow)\n    steps: dict[str, StepFunction] = workflow_cls._get_steps_from_class()\n\n    nodes: list[WorkflowGraphNode] = []\n    edges: list[WorkflowGraphEdge] = []\n    added_nodes: set[str] = set()  # Track added node IDs to avoid duplicates\n    # Track resource nodes by identity (factory id for _Resource)\n    added_resource_nodes: dict[int, WorkflowResourceNode] = {}\n    # Track resource config nodes by config_file>path_selector\n    added_resource_config_nodes: dict[str, WorkflowResourceConfigNode] = {}\n    # Track descriptor nodes by identity for step edges (_Resource or _ResourceConfig)\n    added_descriptor_nodes: dict[\n        int, WorkflowResourceNode | WorkflowResourceConfigNode\n    ] = {}\n    # Track which resources have had their dependencies expanded\n    expanded_resources: set[int] = set()\n    expanding_resources: set[int] = set()\n    # Track resource dependency edges to avoid duplicates\n    resource_edge_keys: set[tuple[str, str, str | None]] = set()\n\n    def _ensure_resource_config_node(\n        resource_config: _ResourceConfig,\n        type_annotation: type | None,\n    ) -> WorkflowResourceConfigNode:\n        selector = resource_config.path_selector\n        config_key = resource_config.config_file + (\n            (\">\" + selector) if selector else \"\"\n        )\n        if config_key in added_resource_config_nodes:\n            return added_resource_config_nodes[config_key]\n        node = _create_resource_config_node(resource_config, type_annotation)\n        nodes.append(node)\n        added_resource_config_nodes[config_key] = node\n        return node\n\n    def _ensure_resource_node(\n        resource: _Resource,\n        type_annotation: type | None,\n        param_name: str,\n    ) -> WorkflowResourceNode:\n        resource_id = _get_resource_identity(resource)\n        if resource_id in added_resource_nodes:\n            return added_resource_nodes[resource_id]\n        node = _create_resource_node(\n            ResourceDefinition(\n                name=param_name,\n                resource=resource,\n                type_annotation=type_annotation,\n            )\n        )\n        nodes.append(node)\n        added_resource_nodes[resource_id] = node\n        return node\n\n    def _track_resource_edge(\n        source: str,\n        target: str,\n        label: str | None,\n    ) -> None:\n        key = (source, target, label)\n        if key in resource_edge_keys:\n            return\n        resource_edge_keys.add(key)\n        edges.append(WorkflowGraphEdge(source=source, target=target, label=label))\n\n    def _ensure_descriptor_node(\n        descriptor: ResourceDescriptor,\n        type_annotation: type | None,\n        param_name: str,\n    ) -> WorkflowResourceNode | WorkflowResourceConfigNode:\n        descriptor_id = _get_resource_identity(descriptor)\n        if isinstance(descriptor, _ResourceConfig):\n            node = _ensure_resource_config_node(descriptor, type_annotation)\n            added_descriptor_nodes[descriptor_id] = node\n            return node\n        if isinstance(descriptor, _Resource):\n            node = _ensure_resource_node(descriptor, type_annotation, param_name)\n            added_descriptor_nodes[descriptor_id] = node\n            resource_id = _get_resource_identity(descriptor)\n            if resource_id in expanded_resources:\n                return node\n            if resource_id in expanding_resources:\n                return node\n            expanding_resources.add(resource_id)\n            for dep_name, dep_descriptor, dep_type in descriptor.get_dependencies():\n                dep_node = _ensure_descriptor_node(dep_descriptor, dep_type, dep_name)\n                _track_resource_edge(source=node.id, target=dep_node.id, label=dep_name)\n            expanding_resources.remove(resource_id)\n            expanded_resources.add(resource_id)\n            return node\n        raise TypeError(\n            f\"Unsupported resource descriptor type: {type(descriptor).__name__}\"\n        )\n\n    # Only one kind of `StopEvent` is allowed in a `Workflow`.\n    # Assuming that `Workflow` is validated before drawing, it's enough to find the first one.\n    current_stop_event = None\n    for step_func in steps.values():\n        for return_type in step_func._step_config.return_types:\n            if issubclass(return_type, StopEvent):\n                current_stop_event = return_type\n                break\n        if current_stop_event:\n            break\n\n    # First pass: Add all nodes\n    for step_name, step_func in steps.items():\n        step_config = step_func._step_config\n\n        # Add step node\n        if step_name not in added_nodes:\n            step_description = inspect.getdoc(step_func)\n            nodes.append(\n                WorkflowStepNode(\n                    id=step_name, label=step_name, description=step_description\n                )\n            )\n            added_nodes.add(step_name)\n\n        # Add event nodes for accepted events\n        for event_type in step_config.accepted_events:\n            if event_type == StopEvent and event_type != current_stop_event:\n                continue\n\n            if event_type.__name__ not in added_nodes:\n                nodes.append(\n                    WorkflowEventNode(\n                        id=event_type.__name__,\n                        label=event_type.__name__,\n                        event_type=event_type.__name__,\n                        event_types=_get_event_type_chain(event_type),\n                        event_schema=event_type.model_json_schema(),\n                    )\n                )\n                added_nodes.add(event_type.__name__)\n\n        # Add event nodes for return types\n        for return_type in step_config.return_types:\n            if return_type is type(None):\n                continue\n\n            if return_type.__name__ not in added_nodes:\n                nodes.append(\n                    WorkflowEventNode(\n                        id=return_type.__name__,\n                        label=return_type.__name__,\n                        event_type=return_type.__name__,\n                        event_types=_get_event_type_chain(return_type),\n                        event_schema=return_type.model_json_schema(),\n                    )\n                )\n                added_nodes.add(return_type.__name__)\n\n            # Add external_step node when InputRequiredEvent is found\n            if (\n                issubclass(return_type, InputRequiredEvent)\n                and \"external_step\" not in added_nodes\n            ):\n                nodes.append(\n                    WorkflowExternalNode(id=\"external_step\", label=\"external_step\")\n                )\n                added_nodes.add(\"external_step\")\n\n        # Add resource nodes (deduplicated by resource identity)\n        for resource_def in step_config.resources:\n            _ensure_descriptor_node(\n                resource_def.resource,\n                resource_def.type_annotation,\n                resource_def.name,\n            )\n\n    # Second pass: Add edges\n    for step_name, step_func in steps.items():\n        step_config = step_func._step_config\n\n        # Edges from steps to return types\n        for return_type in step_config.return_types:\n            if return_type is not type(None):\n                edges.append(\n                    WorkflowGraphEdge(source=step_name, target=return_type.__name__)\n                )\n\n            if issubclass(return_type, InputRequiredEvent):\n                edges.append(\n                    WorkflowGraphEdge(\n                        source=return_type.__name__, target=\"external_step\"\n                    )\n                )\n\n        # Edges from events to steps\n        for event_type in step_config.accepted_events:\n            if step_name == \"_done\" and issubclass(event_type, StopEvent):\n                if current_stop_event:\n                    edges.append(\n                        WorkflowGraphEdge(\n                            source=current_stop_event.__name__, target=step_name\n                        )\n                    )\n            else:\n                edges.append(\n                    WorkflowGraphEdge(source=event_type.__name__, target=step_name)\n                )\n\n            if (\n                issubclass(event_type, HumanResponseEvent)\n                and \"external_step\" in added_nodes\n            ):\n                edges.append(\n                    WorkflowGraphEdge(\n                        source=\"external_step\", target=event_type.__name__\n                    )\n                )\n\n        # Edges from steps to resources (with variable name as label)\n        for resource_def in step_config.resources:\n            resource_id = _get_resource_identity(resource_def.resource)\n            resource_node = added_descriptor_nodes[resource_id]\n            edges.append(\n                WorkflowGraphEdge(\n                    source=step_name,\n                    target=resource_node.id,\n                    label=resource_def.name,  # The variable name\n                )\n            )\n\n    workflow_name = workflow_cls.__name__\n    workflow_description = inspect.getdoc(workflow_cls)\n    return WorkflowGraph(\n        name=workflow_name, nodes=nodes, edges=edges, description=workflow_description\n    )\n\n\n__all__ = [\"get_workflow_representation\"]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/representation/types.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Literal\n\nfrom pydantic import BaseModel, Field\n\n\nclass WorkflowNodeBase(BaseModel):\n    \"\"\"Base class for all workflow graph nodes.\"\"\"\n\n    id: str = Field(description=\"Unique identifier for the node\")\n    label: str = Field(description=\"Display text for the node\")\n\n    def truncated_label(self, max_length: int) -> str:\n        \"\"\"Get truncated label for visualization (adds * suffix if truncated).\"\"\"\n        if len(self.label) <= max_length:\n            return self.label\n        return f\"{self.label[: max_length - 1]}*\"\n\n\nclass WorkflowStepNode(WorkflowNodeBase):\n    \"\"\"A workflow step node representing a function decorated with @step.\"\"\"\n\n    node_type: Literal[\"step\"] = Field(\n        default=\"step\", description=\"Discriminator field for node type\"\n    )\n    description: str | None = Field(\n        default=None,\n        description=\"Documentation string extracted from the step function\",\n    )\n\n\nclass WorkflowEventNode(WorkflowNodeBase):\n    \"\"\"An event node representing an Event class that flows between steps.\"\"\"\n\n    node_type: Literal[\"event\"] = Field(\n        default=\"event\", description=\"Discriminator field for node type\"\n    )\n    event_type: str = Field(\n        description=\"The event class name (e.g., 'StartEvent', 'MyCustomEvent')\"\n    )\n    event_types: list[str] = Field(\n        description=\"Event class inheritance chain for subclass checking. \"\n        \"First element is the class itself, followed by parent Event subclasses.\"\n    )\n    event_schema: dict[str, Any] | None = Field(\n        default=None,\n        description=\"Pydantic JSON schema for the event type\",\n    )\n\n    def is_subclass_of(self, *type_names: str) -> bool:\n        \"\"\"Check if this node's event_type is a subclass of any of the given types.\"\"\"\n        return any(name in self.event_types for name in type_names)\n\n\nclass WorkflowExternalNode(WorkflowNodeBase):\n    \"\"\"An external node representing human-in-the-loop or external system interaction.\"\"\"\n\n    node_type: Literal[\"external\"] = Field(\n        default=\"external\", description=\"Discriminator field for node type\"\n    )\n\n\nclass WorkflowResourceNode(WorkflowNodeBase):\n    \"\"\"A resource node representing an injected dependency (e.g., database client, API client).\"\"\"\n\n    node_type: Literal[\"resource\"] = Field(\n        default=\"resource\", description=\"Discriminator field for node type\"\n    )\n    type_name: str | None = Field(\n        default=None,\n        description=\"The type annotation of the resource (e.g., 'DatabaseClient', 'AsyncLlamaCloud')\",\n    )\n    getter_name: str | None = Field(\n        default=None,\n        description=\"Name of the factory function that creates the resource\",\n    )\n    source_file: str | None = Field(\n        default=None,\n        description=\"Absolute path to the source file containing the getter function\",\n    )\n    source_line: int | None = Field(\n        default=None, description=\"Line number where the getter function is defined\"\n    )\n    description: str | None = Field(\n        default=None,\n        description=\"Documentation string extracted from the getter function\",\n    )\n\n\nclass WorkflowResourceConfigNode(WorkflowNodeBase):\n    \"\"\"A resource config node representing a configuration loaded from a JSON file.\"\"\"\n\n    node_type: Literal[\"resource_config\"] = Field(\n        default=\"resource_config\", description=\"Discriminator field for node type\"\n    )\n    type_name: str | None = Field(\n        default=None,\n        description=\"The Pydantic BaseModel type that the config is validated against\",\n    )\n    config_file: str | None = Field(\n        default=None,\n        description=\"Path to the JSON configuration file\",\n    )\n    path_selector: str | None = Field(\n        default=None,\n        description=\"Dot-separated path selector for nested configuration values\",\n    )\n    config_schema: dict[str, Any] | None = Field(\n        default=None,\n        description=\"Pydantic JSON schema for the config type\",\n    )\n    config_value: dict[str, Any] | None = Field(\n        default=None,\n        description=\"The configuration value read from the file (if readable)\",\n    )\n    description: str | None = Field(\n        default=None,\n        description=\"Human-readable description of the config's purpose and contents\",\n    )\n\n\nclass WorkflowGenericNode(WorkflowNodeBase):\n    \"\"\"A generic node for custom visualization types not covered by standard node types.\n\n    Used for agent visualization (node_type='agent', 'tool', 'workflow_agent', etc.)\n    and other custom extensions. Supports optional event_type fields for type checking.\n    \"\"\"\n\n    node_type: str = Field(\n        description=\"Custom node type string (e.g., 'agent', 'tool', 'workflow_base')\"\n    )\n    event_type: str | None = Field(\n        default=None,\n        description=\"Optional type name for nodes that support inheritance checking (e.g., agent types)\",\n    )\n    event_types: list[str] | None = Field(\n        default=None,\n        description=\"Optional inheritance chain for subclass checking, similar to WorkflowEventNode\",\n    )\n\n    def is_subclass_of(self, *type_names: str) -> bool:\n        \"\"\"Check if this node's event_type is a subclass of any of the given types.\"\"\"\n        if not self.event_types:\n            return False\n        return any(name in self.event_types for name in type_names)\n\n\n# Union type for workflow graph nodes\n# Pydantic will try to match against types in order; WorkflowGenericNode is last as catch-all\nWorkflowGraphNode = (\n    WorkflowStepNode\n    | WorkflowEventNode\n    | WorkflowExternalNode\n    | WorkflowResourceNode\n    | WorkflowResourceConfigNode\n    | WorkflowGenericNode\n)\n\n\nclass WorkflowGraphEdge(BaseModel):\n    \"\"\"A directed edge connecting two nodes in the workflow graph.\"\"\"\n\n    source: str = Field(description=\"ID of the source node (where the edge originates)\")\n    target: str = Field(description=\"ID of the target node (where the edge points to)\")\n    label: str | None = Field(\n        default=None,\n        description=\"Optional edge label, used for resource edges to show the variable name\",\n    )\n\n\nclass WorkflowGraph(BaseModel):\n    \"\"\"Complete workflow graph structure containing all nodes and edges.\"\"\"\n\n    name: str = Field(description=\"Name of the workflow class\")\n    nodes: list[WorkflowGraphNode] = Field(\n        description=\"All nodes in the workflow graph\"\n    )\n    edges: list[WorkflowGraphEdge] = Field(\n        description=\"All directed edges connecting the nodes\"\n    )\n    description: str | None = Field(\n        default=None,\n        description=\"Documentation string extracted from the workflow class\",\n    )\n\n    def filter_by_node_type(self, *node_types: str) -> WorkflowGraph:\n        \"\"\"Create a simplified graph by removing nodes of specified types.\n\n        Edges passing through filtered nodes are resolved:\n        Node1 -> FilteredNode -> Node2 becomes Node1 -> Node2\n\n        Args:\n            *node_types: One or more node type strings to filter out\n                        (e.g., \"event\", \"resource\", \"step\", \"external\")\n\n        Returns:\n            A new WorkflowGraph with the specified node types removed\n            and edges resolved through them.\n        \"\"\"\n        filter_types = set(node_types)\n\n        # Identify nodes to filter out\n        filtered_node_ids: set[str] = set()\n        for node in self.nodes:\n            if node.node_type in filter_types:\n                filtered_node_ids.add(node.id)\n\n        # Keep remaining nodes\n        remaining_nodes = [n for n in self.nodes if n.id not in filtered_node_ids]\n        remaining_node_ids = {n.id for n in remaining_nodes}\n\n        # Build outgoing edge map and node lookup\n        outgoing_map: dict[str, list[WorkflowGraphEdge]] = {}\n        for edge in self.edges:\n            outgoing_map.setdefault(edge.source, []).append(edge)\n\n        node_by_id: dict[str, WorkflowGraphNode] = {n.id: n for n in self.nodes}\n\n        def resolve_targets(\n            from_id: str,\n            first_filtered_label: str | None,\n            visited: set[str],\n        ) -> list[tuple[str, str | None]]:\n            \"\"\"Find remaining nodes reachable from from_id, through filtered nodes.\"\"\"\n            results: list[tuple[str, str | None]] = []\n            for edge in outgoing_map.get(from_id, []):\n                target = edge.target\n                if target in visited:\n                    continue\n\n                if target in remaining_node_ids:\n                    # Use the first filtered node's label, or the edge label if direct\n                    label = (\n                        first_filtered_label\n                        if first_filtered_label is not None\n                        else edge.label\n                    )\n                    results.append((target, label))\n                elif target in filtered_node_ids:\n                    # Follow through filtered node, capturing its label if first\n                    visited.add(target)\n                    filtered_node = node_by_id[target]\n                    label = (\n                        first_filtered_label\n                        if first_filtered_label is not None\n                        else filtered_node.label\n                    )\n                    results.extend(resolve_targets(target, label, visited))\n            return results\n\n        # Build new edges\n        new_edges: list[WorkflowGraphEdge] = []\n        seen_edges: set[tuple[str, str]] = set()\n\n        for source_id in remaining_node_ids:\n            for target_id, label in resolve_targets(source_id, None, set()):\n                edge_key = (source_id, target_id)\n                if edge_key not in seen_edges:\n                    seen_edges.add(edge_key)\n                    new_edges.append(\n                        WorkflowGraphEdge(\n                            source=source_id,\n                            target=target_id,\n                            label=label,\n                        )\n                    )\n\n        return WorkflowGraph(\n            name=self.name,\n            nodes=remaining_nodes,\n            edges=new_edges,\n            description=self.description,\n        )\n\n\n__all__ = [\n    \"WorkflowNodeBase\",\n    \"WorkflowStepNode\",\n    \"WorkflowEventNode\",\n    \"WorkflowExternalNode\",\n    \"WorkflowResourceNode\",\n    \"WorkflowResourceConfigNode\",\n    \"WorkflowGenericNode\",\n    \"WorkflowGraphNode\",\n    \"WorkflowGraphEdge\",\n    \"WorkflowGraph\",\n]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/representation/validate.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom dataclasses import dataclass, field\n\nfrom workflows.decorators import CatchErrorHandler, StepConfig, WorkflowGraphCheck\nfrom workflows.errors import WorkflowConfigurationError, WorkflowValidationError\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StepFailedEvent,\n    StopEvent,\n)\nfrom workflows.resource import ResourceDescriptor, ResourceManager, _ResourceConfig\n\n# Graph nodes: step names (str) for steps, event classes (type) for events.\nGraphNode = str | type\n\n\n@dataclass\nclass StepGraph:\n    \"\"\"Lightweight adjacency-list representation of a workflow's step/event graph.\n\n    Nodes are step names (``str``) for steps and event classes (``type``) for\n    events.  An edge from an event node to a step node means the step accepts\n    that event; an edge from a step node to an event node means the step returns\n    that event type.\n    \"\"\"\n\n    outgoing: dict[GraphNode, list[GraphNode]] = field(default_factory=dict)\n    \"\"\"Adjacency list: node -> list of successor nodes.\"\"\"\n\n    event_types: set[type] = field(default_factory=set)\n    \"\"\"All event classes seen in the graph.\"\"\"\n\n    step_names: set[str] = field(default_factory=set)\n    \"\"\"Names of all steps in the graph.\"\"\"\n\n    forward_reachable: set[GraphNode] = field(default_factory=set)\n    \"\"\"Nodes reachable from input seeds (StartEvent, HumanResponseEvent subclasses).\"\"\"\n\n    reverse_reachable: set[GraphNode] = field(default_factory=set)\n    \"\"\"Nodes that can reach an output event (StopEvent, InputRequiredEvent) via reverse traversal.\"\"\"\n\n\ndef build_step_graph(\n    steps: dict[str, StepConfig],\n    start_event_class: type,\n    catch_error_steps: list[str] | None = None,\n) -> StepGraph:\n    \"\"\"Build a StepGraph from step configs and a start event class.\n\n    Constructs the adjacency list, then computes forward reachability from input\n    events (StartEvent + HumanResponseEvent subclasses + any catch_error handler\n    step names) and reverse reachability from output events (StopEvent +\n    InputRequiredEvent).\n    \"\"\"\n    outgoing: dict[GraphNode, list[GraphNode]] = {}\n    event_types: set[type] = set()\n    step_names: set[str] = set()\n\n    for name, cfg in steps.items():\n        step_names.add(name)\n        for ev in cfg.accepted_events:\n            event_types.add(ev)\n            outgoing.setdefault(ev, []).append(name)\n        for rt in cfg.return_types:\n            if rt is type(None):\n                continue\n            event_types.add(rt)\n            outgoing.setdefault(name, []).append(rt)\n\n    # Forward DFS from StartEvent + HumanResponseEvent subclasses +\n    # catch_error handler step names (their sub-graphs are reachable via\n    # runtime routing of StepFailedEvent, not via any event in the graph).\n    seeds: list[GraphNode] = [start_event_class]\n    for ev_type in event_types:\n        if issubclass(ev_type, HumanResponseEvent) and ev_type not in seeds:\n            seeds.append(ev_type)\n    for handler_name in catch_error_steps or []:\n        if handler_name not in seeds:\n            seeds.append(handler_name)\n\n    forward_reachable = _dfs(seeds, outgoing)\n\n    # Reverse DFS from output events\n    incoming: dict[GraphNode, list[GraphNode]] = {}\n    for source, targets in outgoing.items():\n        for target in targets:\n            incoming.setdefault(target, []).append(source)\n\n    output_seeds: list[GraphNode] = [\n        ev_type\n        for ev_type in event_types\n        if issubclass(ev_type, (StopEvent, InputRequiredEvent))\n    ]\n    reverse_reachable = _dfs(output_seeds, incoming)\n\n    return StepGraph(\n        outgoing=outgoing,\n        event_types=event_types,\n        step_names=step_names,\n        forward_reachable=forward_reachable,\n        reverse_reachable=reverse_reachable,\n    )\n\n\ndef _dfs(\n    seeds: list[GraphNode], adjacency: dict[GraphNode, list[GraphNode]]\n) -> set[GraphNode]:\n    \"\"\"Depth-first search returning all reachable nodes from seeds.\"\"\"\n    visited: set[GraphNode] = set()\n    stack = list(seeds)\n    while stack:\n        node = stack.pop()\n        if node in visited:\n            continue\n        visited.add(node)\n        for target in adjacency.get(node, []):\n            if target not in visited:\n                stack.append(target)\n    return visited\n\n\n@dataclass\nclass GraphValidationError:\n    \"\"\"A single graph validation error.\"\"\"\n\n    check: WorkflowGraphCheck\n    message: str\n    hint: str\n    step_names: list[str] = field(default_factory=list)\n\n\ndef validate_graph(\n    steps: dict[str, StepConfig],\n    start_event_class: type,\n    skip_checks: set[WorkflowGraphCheck] | None = None,\n    catch_error_steps: list[str] | None = None,\n) -> list[GraphValidationError]:\n    \"\"\"Validate the graph structure of a workflow, accumulating all errors.\n\n    Builds a ``StepGraph`` from step configs and runs three checks:\n    1. Reachability: all steps are reachable from input events\n    2. Terminal events: events with no consumer must be output events\n    3. Dead ends: every step producing events must reach an output event\n\n    Args:\n        steps: Mapping of step name to StepConfig.\n        start_event_class: The StartEvent subclass for this workflow.\n        skip_checks: Workflow-level checks to skip entirely.\n        catch_error_steps: Names of catch_error handler steps; their sub-graphs\n            are forward-reachable via runtime routing rather than by\n            connection to an event in the main graph.\n\n    Returns:\n        List of GraphValidationError (empty if the graph is valid).\n    \"\"\"\n    skip_checks = skip_checks or set()\n    errors: list[GraphValidationError] = []\n\n    graph = build_step_graph(steps, start_event_class, catch_error_steps)\n\n    # Check 1: Reachability\n    if \"reachability\" not in skip_checks:\n        step_skip = {\n            name\n            for name, cfg in steps.items()\n            if \"reachability\" in cfg.skip_graph_checks\n        }\n        unreachable_steps = sorted(\n            name\n            for name in graph.step_names - step_skip\n            if name not in graph.forward_reachable\n        )\n        if unreachable_steps:\n            names = \", \".join(unreachable_steps)\n            errors.append(\n                GraphValidationError(\n                    check=\"reachability\",\n                    message=f\"Unreachable steps: {names}\",\n                    hint=\"Steps must be reachable from StartEvent or HumanResponseEvent.\",\n                    step_names=unreachable_steps,\n                )\n            )\n\n    # Check 2: Terminal events — events with no step consumer must be output events\n    if \"terminal_event\" not in skip_checks:\n        dangling: list[type] = []\n        for ev_type in graph.event_types:\n            targets = graph.outgoing.get(ev_type, [])\n            if any(t in graph.step_names for t in targets):\n                continue\n            if issubclass(ev_type, (StopEvent, InputRequiredEvent)):\n                continue\n            dangling.append(ev_type)\n        if dangling:\n            names = \", \".join(sorted(t.__name__ for t in dangling))\n            errors.append(\n                GraphValidationError(\n                    check=\"terminal_event\",\n                    message=f\"Events produced but never consumed: {names}\",\n                    hint=\"Only StopEvent and InputRequiredEvent may be terminal.\",\n                    step_names=[],\n                )\n            )\n\n    # Check 3: Dead-end detection\n    if \"dead_end\" not in skip_checks:\n        steps_producing_events = {\n            s\n            for s in graph.step_names\n            if any(isinstance(t, type) for t in graph.outgoing.get(s, []))\n        }\n\n        step_skip = {\n            name for name, cfg in steps.items() if \"dead_end\" in cfg.skip_graph_checks\n        }\n        dead_end_steps = sorted(\n            name\n            for name in steps_producing_events - step_skip\n            if name not in graph.reverse_reachable\n        )\n        if dead_end_steps:\n            names = \", \".join(dead_end_steps)\n            errors.append(\n                GraphValidationError(\n                    check=\"dead_end\",\n                    message=f\"Dead-end steps: {names}\",\n                    hint=\"Steps must have a path to StopEvent or InputRequiredEvent.\",\n                    step_names=dead_end_steps,\n                )\n            )\n\n    return errors\n\n\ndef validate_catch_error_handlers(\n    handlers: Iterable[CatchErrorHandler],\n    step_names: set[str],\n) -> list[str]:\n    \"\"\"Validate structural invariants of ``@catch_error`` handlers.\n\n    Returns a list of error messages; empty when the handler set is valid.\n    Callers are responsible for raising.\n    \"\"\"\n    errors: list[str] = []\n\n    handlers = list(handlers)\n    wildcard_handlers = [h for h in handlers if h.for_steps is None]\n    if len(wildcard_handlers) > 1:\n        names = \", \".join(sorted(h.step_name for h in wildcard_handlers))\n        errors.append(\n            f\"Only one wildcard @catch_error handler is allowed per workflow, \"\n            f\"found {len(wildcard_handlers)}: {names}\"\n        )\n\n    handler_step_names = {h.step_name for h in handlers}\n    claim_owner: dict[str, str] = {}\n    for handler in handlers:\n        if handler.for_steps is None:\n            continue\n        for target in handler.for_steps:\n            if target not in step_names:\n                errors.append(\n                    f\"@catch_error handler '{handler.step_name}' lists \"\n                    f\"unknown step '{target}' in for_steps.\"\n                )\n                continue\n            if target == handler.step_name or target in handler_step_names:\n                errors.append(\n                    f\"@catch_error handler '{handler.step_name}' cannot \"\n                    f\"cover another handler step '{target}'.\"\n                )\n                continue\n            if target in claim_owner:\n                errors.append(\n                    f\"Step '{target}' is claimed by two @catch_error \"\n                    f\"handlers: '{claim_owner[target]}' and '{handler.step_name}'.\"\n                )\n                continue\n            claim_owner[target] = handler.step_name\n\n    return errors\n\n\ndef _ensure_start_event_class(\n    steps: dict[str, StepConfig], workflow_cls_name: str\n) -> type[StartEvent]:\n    \"\"\"Infer and validate the single StartEvent subclass accepted by a workflow.\n\n    Inspects every step's accepted events and returns the unique StartEvent\n    subclass. Raises ``WorkflowConfigurationError`` if zero or more than one\n    are found.\n    \"\"\"\n    start_events_found: set[type[StartEvent]] = set()\n    for cfg in steps.values():\n        for event_type in cfg.accepted_events:\n            if issubclass(event_type, StartEvent):\n                start_events_found.add(event_type)\n\n    num_found = len(start_events_found)\n    if num_found == 0:\n        raise WorkflowConfigurationError(\n            \"At least one Event of type StartEvent must be received by any step. \"\n            f\"(Workflow '{workflow_cls_name}' has no @step that accepts StartEvent.)\"\n        )\n    if num_found > 1:\n        raise WorkflowConfigurationError(\n            f\"Only one type of StartEvent is allowed per workflow, found {num_found}: \"\n            f\"{start_events_found} in workflow '{workflow_cls_name}'.\"\n        )\n    return start_events_found.pop()\n\n\ndef _ensure_stop_event_class(\n    steps: dict[str, StepConfig], workflow_cls_name: str\n) -> type[StopEvent]:\n    \"\"\"Infer and validate the single StopEvent subclass produced by a workflow.\n\n    Inspects every step's return types and returns the unique StopEvent\n    subclass. Raises ``WorkflowConfigurationError`` if zero or more than one\n    are found.\n    \"\"\"\n    stop_events_found: set[type[StopEvent]] = set()\n    for cfg in steps.values():\n        for event_type in cfg.return_types:\n            if issubclass(event_type, StopEvent):\n                stop_events_found.add(event_type)\n\n    num_found = len(stop_events_found)\n    if num_found == 0:\n        raise WorkflowConfigurationError(\n            \"At least one Event of type StopEvent must be returned by any step. \"\n            f\"(Workflow '{workflow_cls_name}' has no @step that returns StopEvent.)\"\n        )\n    if num_found > 1:\n        raise WorkflowConfigurationError(\n            f\"Only one type of StopEvent is allowed per workflow, found {num_found}: \"\n            f\"{stop_events_found} in workflow '{workflow_cls_name}'.\"\n        )\n    return stop_events_found.pop()\n\n\ndef _collect_events(steps: dict[str, StepConfig]) -> list[type[Event]]:\n    \"\"\"Return every ``Event`` subclass touched by the workflow's steps.\n\n    Skips the runtime-injected ``_done`` step so only user-facing events are\n    reported. Walks both accepted and returned types of each step.\n    \"\"\"\n    events_found: set[type[Event]] = set()\n    for cfg in steps.values():\n        for event_type in cfg.return_types:\n            if issubclass(event_type, Event):\n                events_found.add(event_type)\n        for event_type in cfg.accepted_events:\n            if issubclass(event_type, Event):\n                events_found.add(event_type)\n    return list(events_found)\n\n\ndef _collect_catch_error_handlers(\n    steps: dict[str, StepConfig],\n) -> tuple[dict[str, CatchErrorHandler], dict[str, str]]:\n    \"\"\"Discover ``@catch_error`` handlers and build the step->handler routing table.\n\n    Validates the handler set (via :func:`validate_catch_error_handlers`) and\n    each handler's ``max_recoveries``; raises ``WorkflowValidationError`` on\n    any problem.\n\n    Returns ``(catch_error_handlers, handler_for_step)`` where\n    ``catch_error_handlers`` maps handler step name to its descriptor and\n    ``handler_for_step`` maps each covered step name to the handler that owns\n    it (scoped claims first, then the wildcard fills).\n    \"\"\"\n    all_step_names = set(steps.keys())\n    handlers: list[CatchErrorHandler] = []\n    for name, cfg in steps.items():\n        if cfg.role != \"catch_error\":\n            continue\n        max_recoveries = cfg.catch_error_max_recoveries\n        if not isinstance(max_recoveries, int) or max_recoveries < 1:\n            raise WorkflowValidationError(\n                f\"@catch_error handler '{name}' has max_recoveries=\"\n                f\"{max_recoveries!r}; must be an integer >= 1.\"\n            )\n        handlers.append(\n            CatchErrorHandler(\n                step_name=name,\n                for_steps=(\n                    list(cfg.catch_error_for_steps)\n                    if cfg.catch_error_for_steps is not None\n                    else None\n                ),\n                max_recoveries=max_recoveries,\n            )\n        )\n\n    handler_errors = validate_catch_error_handlers(handlers, all_step_names)\n    if handler_errors:\n        raise WorkflowValidationError(\"\\n\".join(handler_errors))\n\n    handler_step_names = {h.step_name for h in handlers}\n    handler_for_step: dict[str, str] = {}\n    for handler in handlers:\n        if handler.for_steps is None:\n            continue\n        for target in handler.for_steps:\n            handler_for_step[target] = handler.step_name\n\n    wildcards = [h for h in handlers if h.for_steps is None]\n    wildcard = wildcards[0] if wildcards else None\n    if wildcard is not None:\n        for step_name in all_step_names:\n            if step_name in handler_step_names:\n                continue\n            if step_name in handler_for_step:\n                continue\n            handler_for_step[step_name] = wildcard.step_name\n\n    return {h.step_name: h for h in handlers}, handler_for_step\n\n\ndef _validate_event_connectivity(\n    steps: dict[str, StepConfig],\n    start_event_class: type[StartEvent],\n) -> bool:\n    \"\"\"Validate event production/consumption across the step graph.\n\n    Checks that:\n    - No user step accepts ``StopEvent``.\n    - Every consumed event is either produced or crosses the workflow\n      boundary (``InputRequiredEvent``/``HumanResponseEvent``/``StopEvent``/\n      ``StepFailedEvent``).\n    - Every produced event is consumed, except for\n      ``InputRequiredEvent``/``HumanResponseEvent``/``StopEvent`` subclasses.\n\n    Returns ``True`` if the workflow uses human-in-the-loop\n    (``InputRequiredEvent`` produced or ``HumanResponseEvent`` consumed).\n    Raises ``WorkflowValidationError`` on any violation.\n    \"\"\"\n    produced_events: set[type] = {start_event_class}\n    consumed_events: set[type] = set()\n    steps_accepting_stop_event: list[str] = []\n\n    for name, cfg in steps.items():\n        for event_type in cfg.accepted_events:\n            if issubclass(event_type, StopEvent):\n                steps_accepting_stop_event.append(name)\n                break\n        for event_type in cfg.accepted_events:\n            consumed_events.add(event_type)\n        for event_type in cfg.return_types:\n            if event_type is type(None):\n                continue\n            produced_events.add(event_type)\n\n    if steps_accepting_stop_event:\n        step_names = \"', '\".join(steps_accepting_stop_event)\n        plural = \"\" if len(steps_accepting_stop_event) == 1 else \"s\"\n        raise WorkflowValidationError(\n            f\"Step{plural} '{step_names}' cannot accept StopEvent. \"\n            \"StopEvent signals the end of the workflow. \"\n            \"Use a different Event type instead.\"\n        )\n\n    unconsumed_events = {\n        x\n        for x in consumed_events - produced_events\n        if not issubclass(\n            x,\n            (InputRequiredEvent, HumanResponseEvent, StopEvent, StepFailedEvent),\n        )\n    }\n    if unconsumed_events:\n        names = \", \".join(ev.__name__ for ev in unconsumed_events)\n        raise WorkflowValidationError(\n            f\"The following events are consumed but never produced: {names}\"\n        )\n\n    unused_events = {\n        x\n        for x in produced_events - consumed_events\n        if not issubclass(x, (InputRequiredEvent, HumanResponseEvent, StopEvent))\n    }\n    if unused_events:\n        names = \", \".join(ev.__name__ for ev in unused_events)\n        raise WorkflowValidationError(\n            f\"The following events are produced but never consumed: {names}\"\n        )\n\n    return (\n        InputRequiredEvent in produced_events or HumanResponseEvent in consumed_events\n    )\n\n\n@dataclass\nclass _ResourceValidationContext:\n    \"\"\"Tracks context for resource validation to provide clear error messages.\"\"\"\n\n    resource: ResourceDescriptor\n    step_name: str\n    param_name: str\n    resource_chain: list[str] = field(default_factory=list)\n\n    def format_location(self) -> str:\n        if len(self.resource_chain) > 1:\n            chain_str = \" -> \".join(self.resource_chain)\n            return (\n                f\"step '{self.step_name}', parameter '{self.param_name}' ({chain_str})\"\n            )\n        return f\"step '{self.step_name}', parameter '{self.param_name}'\"\n\n    def with_dependency(self, dep: ResourceDescriptor) -> _ResourceValidationContext:\n        return _ResourceValidationContext(\n            resource=dep,\n            step_name=self.step_name,\n            param_name=self.param_name,\n            resource_chain=[*self.resource_chain, dep.name],\n        )\n\n\ndef _validate_resource_configs(steps: dict[str, StepConfig]) -> list[str]:\n    \"\"\"Validate every resource config (and nested configs) by loading it.\n\n    Returns a list of human-readable error messages; empty if all configs load\n    cleanly. Callers decide whether to raise.\n    \"\"\"\n    errors: list[str] = []\n    seen: set[str] = set()\n\n    stack: list[_ResourceValidationContext] = []\n    for step_name, cfg in steps.items():\n        for res_def in cfg.resources:\n            res_def.resource.set_type_annotation(res_def.type_annotation)\n            stack.append(\n                _ResourceValidationContext(\n                    resource=res_def.resource,\n                    step_name=step_name,\n                    param_name=res_def.name,\n                    resource_chain=[res_def.resource.name],\n                )\n            )\n\n    while stack:\n        ctx = stack.pop()\n        if ctx.resource.name in seen:\n            continue\n        seen.add(ctx.resource.name)\n\n        for _dep_param, dep, type_ann in ctx.resource.get_dependencies():\n            dep.set_type_annotation(type_ann)\n            stack.append(ctx.with_dependency(dep))\n\n        if isinstance(ctx.resource, _ResourceConfig):\n            try:\n                ctx.resource.call()\n            except Exception as e:\n                errors.append(f\"In {ctx.format_location()}: {e}\")\n\n    return errors\n\n\nasync def _validate_resources(\n    steps: dict[str, StepConfig], resource_manager: ResourceManager\n) -> list[str]:\n    \"\"\"Resolve every resource via ``resource_manager``.\n\n    Surfaces circular dependencies and factory-time failures. Returns a list of\n    error messages; empty if all resources resolve.\n    \"\"\"\n    errors: list[str] = []\n    for step_name, cfg in steps.items():\n        for res_def in cfg.resources:\n            res_def.resource.set_type_annotation(res_def.type_annotation)\n            try:\n                await resource_manager.get(res_def.resource)\n            except Exception as e:\n                errors.append(f\"In step '{step_name}', parameter '{res_def.name}': {e}\")\n    return errors\n\n\n@dataclass\nclass _WorkflowValidationResult:\n    \"\"\"Derived workflow state produced by :func:`_validate_workflow`.\"\"\"\n\n    start_event_class: type[StartEvent]\n    stop_event_class: type[StopEvent]\n    catch_error_handlers: dict[str, CatchErrorHandler]\n    handler_for_step: dict[str, str]\n    uses_hitl: bool\n\n\ndef _validate_workflow(\n    steps: dict[str, StepConfig],\n    workflow_cls_name: str,\n    skip_graph_checks: set[WorkflowGraphCheck],\n) -> _WorkflowValidationResult:\n    \"\"\"Run every structural check on a workflow's step set.\n\n    Orders checks so the most actionable errors surface first (missing steps,\n    then StartEvent, then StopEvent, then event connectivity, then catch_error\n    handlers, then graph reachability/dead-ends).\n\n    Raises ``WorkflowConfigurationError`` or ``WorkflowValidationError`` on any\n    violation. Resource validation is handled separately via\n    :func:`_validate_resource_configs` and :func:`_validate_resources`.\n    \"\"\"\n    if not steps:\n        raise WorkflowConfigurationError(\n            f\"Workflow '{workflow_cls_name}' has no configured steps. \"\n            \"Did you forget to annotate methods with @step or to register \"\n            \"free-function steps via @step(workflow=...)?\"\n        )\n\n    start_event_class = _ensure_start_event_class(steps, workflow_cls_name)\n    stop_event_class = _ensure_stop_event_class(steps, workflow_cls_name)\n\n    uses_hitl = _validate_event_connectivity(steps, start_event_class)\n\n    catch_error_handlers, handler_for_step = _collect_catch_error_handlers(steps)\n\n    graph_errors = validate_graph(\n        steps=steps,\n        start_event_class=start_event_class,\n        skip_checks=skip_graph_checks,\n        catch_error_steps=list(catch_error_handlers.keys()),\n    )\n    if graph_errors:\n        detail = \"\\n\".join(\n            f\"  - [{e.check}] {e.message}\\n    {e.hint}\" for e in graph_errors\n        )\n        raise WorkflowValidationError(f\"Graph validation failed:\\n{detail}\")\n\n    return _WorkflowValidationResult(\n        start_event_class=start_event_class,\n        stop_event_class=stop_event_class,\n        catch_error_handlers=catch_error_handlers,\n        handler_for_step=handler_for_step,\n        uses_hitl=uses_hitl,\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/resource.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport functools\nimport inspect\nimport json\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import (\n    Annotated,\n    Any,\n    Awaitable,\n    Callable,\n    Generic,\n    Iterator,\n    Protocol,\n    TypeVar,\n    cast,\n    get_args,\n    get_origin,\n    get_type_hints,\n    runtime_checkable,\n)\n\nfrom pydantic import (\n    BaseModel,\n    ConfigDict,\n)\n\nT = TypeVar(\"T\")\nB = TypeVar(\"B\", bound=BaseModel)\n\n\ndef _get_factory_type_hints(\n    factory: Callable[..., Any],\n    localns: dict[str, Any] | None = None,\n) -> dict[str, Any]:\n    \"\"\"Resolve type hints for a factory function, avoiding shadowing.\n\n    Filters localns to exclude names that exist in factory's __globals__,\n    so types resolve from the factory's module while allowing closure variables.\n    \"\"\"\n    filtered_localns = localns\n    if filtered_localns:\n        globalns = getattr(factory, \"__globals__\", {})\n        filtered_localns = {\n            k: v for k, v in filtered_localns.items() if k not in globalns\n        }\n\n    try:\n        return get_type_hints(factory, include_extras=True, localns=filtered_localns)\n    except NameError:\n        return {}\n\n\n@runtime_checkable\nclass ResourceDescriptor(Protocol):\n    \"\"\"Common interface for resource descriptors.\n\n    Both _Resource and _ResourceConfig implement this protocol, allowing\n    unified resolution through ResourceManager without isinstance checks.\n    \"\"\"\n\n    @property\n    def name(self) -> str:\n        \"\"\"Unique identifier for caching and cycle detection.\"\"\"\n        ...\n\n    @property\n    def cache(self) -> bool:\n        \"\"\"Whether to cache the resolved value.\"\"\"\n        ...\n\n    async def resolve(self, manager: ResourceManager) -> Any:\n        \"\"\"Resolve the resource, returning the concrete value.\"\"\"\n        ...\n\n    def set_type_annotation(self, type_annotation: Any) -> None:\n        \"\"\"Provide the annotated type for config-backed resources.\"\"\"\n        ...\n\n    def set_localns(self, localns: dict[str, Any] | None) -> None:\n        \"\"\"Store local namespace for resolving deferred type annotations.\"\"\"\n        ...\n\n    def get_dependencies(self) -> list[tuple[str, ResourceDescriptor, type | None]]:\n        \"\"\"Return factory dependencies. Empty for non-factory resources.\"\"\"\n        ...\n\n\nclass _Resource(Generic[T]):\n    \"\"\"Internal wrapper for resource factories.\n\n    Wraps sync/async factories and records metadata such as the qualified name\n    and cache behavior.\n    \"\"\"\n\n    def __init__(self, factory: Callable[..., T | Awaitable[T]], cache: bool) -> None:\n        self._factory = factory\n        self._is_async = inspect.iscoroutinefunction(factory)\n        self.name = getattr(factory, \"__qualname__\", type(factory).__name__)\n        self.cache = cache\n        self._localns: dict[str, Any] | None = None\n\n    async def _resolve_dependencies(\n        self, resource_manager: ResourceManager\n    ) -> dict[str, Any]:\n        \"\"\"Resolve annotated ResourceDescriptor dependencies.\"\"\"\n        resolved: dict[str, Any] = {}\n\n        for param_name, descriptor, type_annotation in self.get_dependencies():\n            descriptor.set_type_annotation(type_annotation)\n            descriptor.set_localns(self._localns)\n            resolved[param_name] = await resource_manager.get(descriptor)\n\n        return resolved\n\n    async def call(self, resource_manager: ResourceManager) -> T:\n        \"\"\"Invoke the underlying factory, awaiting if necessary.\"\"\"\n        args = await self._resolve_dependencies(resource_manager)\n        if self._is_async:\n            result = await cast(Callable[..., Awaitable[T]], self._factory)(**args)\n        else:\n            result = cast(Callable[..., T], self._factory)(**args)\n        return result\n\n    async def resolve(self, manager: ResourceManager) -> T:\n        \"\"\"Resolve the resource via the manager.\n\n        Implements ResourceDescriptor protocol.\n        \"\"\"\n        return await self.call(manager)\n\n    def set_type_annotation(self, type_annotation: Any) -> None:\n        \"\"\"No-op for factory-backed resources.\"\"\"\n        return None\n\n    def set_localns(self, localns: dict[str, Any] | None) -> None:\n        \"\"\"Store local namespace for resolving deferred type annotations.\"\"\"\n        self._localns = localns\n\n    def get_dependencies(self) -> list[tuple[str, ResourceDescriptor, type | None]]:\n        \"\"\"Extract ResourceDescriptor dependencies from factory signature.\"\"\"\n        deps: list[tuple[str, ResourceDescriptor, type | None]] = []\n        params = inspect.signature(self._factory).parameters\n        type_hints = _get_factory_type_hints(self._factory, self._localns)\n\n        for param in params.values():\n            annotation = type_hints.get(param.name, param.annotation)\n            if get_origin(annotation) is Annotated:\n                args = get_args(annotation)\n                if len(args) >= 2:\n                    descriptor = args[1]\n                    if isinstance(descriptor, ResourceDescriptor):\n                        type_annotation = args[0]\n                        deps.append((param.name, descriptor, type_annotation))\n        return deps\n\n\ndef _get_resource_config_data(\n    config_file: str,\n    path_selector: str | None,\n) -> dict[str, Any]:\n    # Resolve to absolute path for cache key to handle different working directories\n    abs_path = str(Path(config_file).resolve())\n    return _get_resource_config_data_cached(abs_path, path_selector)\n\n\n@functools.lru_cache(maxsize=128)\ndef _get_resource_config_data_cached(\n    config_file: str,\n    path_selector: str | None,\n) -> dict[str, Any]:\n    with open(config_file, \"r\") as f:\n        data = json.load(f)\n    if path_selector is not None:\n        keys = path_selector.split(\".\")\n        val: dict[str, Any] = data\n        cumulative_path = \"\"\n        for key in keys:\n            cumulative_path += key + \".\"\n            got = cast(dict[str, Any] | None, val.get(key))\n            if not isinstance(got, dict):\n                raise ValueError(\n                    f\"Expected dictionary for configuration from {config_file} at path {cumulative_path.strip('.')}, got: {type(got)}\"\n                )\n            val = got\n        return val\n    return data\n\n\nclass _ResourceConfig(Generic[B]):\n    \"\"\"\n    Internal wrapper for a pydantic-based resource whose configuration can be read from a JSON file.\n    \"\"\"\n\n    _original_config_file: str\n    _resolved_config_file: str | None\n    path_selector: str | None\n    cls_factory: type[B] | None\n    label: str | None\n    description: str | None\n\n    def __init__(\n        self,\n        config_file: str,\n        path_selector: str | None,\n        cls_factory: type[B] | None = None,\n        label: str | None = None,\n        description: str | None = None,\n    ) -> None:\n        config_path = Path(config_file)\n        if config_path.suffix != \".json\":\n            raise ValueError(\n                \"Only JSON files can be used to load Pydantic-based resources.\"\n            )\n        # Resolved lazily in config_file property\n        self._original_config_file = config_file\n        self._resolved_config_file = None\n        self.path_selector = path_selector\n        self.cls_factory = cls_factory\n        self.label = label\n        self.description = description\n\n    @property\n    def config_file(self) -> str:\n        \"\"\"Return the resolved absolute path, validated on first access.\"\"\"\n        if self._resolved_config_file is None:\n            config_path = Path(self._original_config_file)\n            if not config_path.is_file():\n                raise FileNotFoundError(f\"No such file: {self._original_config_file}\")\n            self._resolved_config_file = str(config_path.resolve())\n        return self._resolved_config_file\n\n    @property\n    def name(self) -> str:\n        base_name = self._original_config_file\n        if self.path_selector is not None:\n            return base_name + \".\" + self.path_selector\n        return base_name\n\n    @property\n    def cache(self) -> bool:\n        \"\"\"ResourceConfig instances are always cached.\"\"\"\n        return True\n\n    def call(self) -> B:\n        sel_data = _get_resource_config_data(\n            config_file=self.config_file, path_selector=self.path_selector\n        )\n        # let validation error bubble up\n        if self.cls_factory is not None:\n            return self.cls_factory.model_validate(sel_data)\n        else:\n            raise ValueError(\n                \"Class factory should be set to a BaseModel subclass before calling\"\n            )\n\n    async def resolve(self, manager: ResourceManager) -> B:\n        \"\"\"Resolve the config resource.\n\n        Implements ResourceDescriptor protocol.\n        Note: cls_factory must be set before calling this method.\n        \"\"\"\n        return self.call()\n\n    def set_type_annotation(self, type_annotation: Any) -> None:\n        \"\"\"Assign the annotated class for config-backed resources when missing.\"\"\"\n        if self.cls_factory is None:\n            self.cls_factory = cast(type[B], type_annotation)\n\n    def set_localns(self, localns: dict[str, Any] | None) -> None:\n        \"\"\"No-op for config-backed resources.\"\"\"\n        pass\n\n    def get_dependencies(self) -> list[tuple[str, ResourceDescriptor, type | None]]:\n        \"\"\"No dependencies for config-backed resources.\"\"\"\n        return []\n\n\ndef ResourceConfig(\n    config_file: str,\n    path_selector: str | None = None,\n    label: str | None = None,\n    description: str | None = None,\n) -> _ResourceConfig:\n    \"\"\"\n    Create a config-backed resource that loads a Pydantic model from a JSON file.\n\n    Args:\n        config_file: JSON file where the configuration is stored.\n        path_selector: Path selector to retrieve a specific value from the JSON map.\n        label: Human-friendly short name for display in visualizations.\n        description: Longer description explaining the purpose and contents of this config.\n\n    Returns:\n        _ResourceConfig: A configured resource representation.\n\n    Example:\n        ```python\n        from typing import Annotated\n        from pydantic import BaseModel\n        from workflows import Workflow, step\n        from workflows.events import StartEvent, StopEvent\n        from workflows.resource import ResourceConfig\n\n\n        class ClassifierConfig(BaseModel):\n            categories: list[str]\n            threshold: float\n\n\n        class MyWorkflow(Workflow):\n            @step\n            async def classify(\n                self,\n                ev: StartEvent,\n                config: Annotated[\n                    ClassifierConfig,\n                    ResourceConfig(\n                        config_file=\"classifier.json\",\n                        label=\"Classifier\",\n                        description=\"Classification categories and threshold\",\n                    ),\n                ],\n            ) -> StopEvent:\n                return StopEvent(result=config.categories)\n        ```\n    \"\"\"\n    return _ResourceConfig(\n        config_file=config_file,\n        path_selector=path_selector,\n        label=label,\n        description=description,\n    )\n\n\nclass ResourceDefinition(BaseModel):\n    \"\"\"Definition for a resource injection requested by a step signature.\n\n    Attributes:\n        name (str): Parameter name in the step function.\n        resource (ResourceDescriptor): Descriptor used to produce the dependency.\n        type_annotation (type | None): The type annotation from Annotated[T, ...].\n    \"\"\"\n\n    model_config = ConfigDict(arbitrary_types_allowed=True)\n    name: str\n    resource: ResourceDescriptor\n    type_annotation: Any = None\n\n\ndef Resource(\n    factory: Callable[..., T],\n    cache: bool = True,\n) -> _Resource:\n    \"\"\"Declare a resource to inject into step functions.\n\n    Args:\n        factory (Callable[..., T] | None): Function returning the resource instance. May be async.\n        cache (bool): If True, reuse the produced resource across steps. Defaults to True.\n\n    Returns:\n        _Resource[T]: A resource descriptor to be used in `typing.Annotated`.\n\n    Examples:\n        ```python\n        from typing import Annotated\n        from workflows.resource import Resource\n\n        def get_memory(**kwargs) -> Memory:\n            return Memory.from_defaults(\"user123\", token_limit=60000)\n\n        class MyWorkflow(Workflow):\n            @step\n            async def first(\n                self,\n                ev: StartEvent,\n                memory: Annotated[Memory, Resource(get_memory)],\n            ) -> StopEvent:\n                await memory.aput(...)\n                return StopEvent(result=\"ok\")\n        ```\n    \"\"\"\n    return _Resource(factory, cache)\n\n\nclass ResourceManager:\n    \"\"\"Manage resource lifecycles and caching across workflow steps.\n\n    Methods:\n        set: Manually set a resource by name.\n        get: Produce or retrieve a resource via its descriptor.\n        get_all: Return the internal name->resource map.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.resources: dict[str, Any] = {}\n        self._resolving: list[str] = []  # Track resources being resolved in order\n        self._resolution_cache: dict[str, Any] = {}\n        self._resolution_depth = 0\n\n    @contextmanager\n    def resolution_scope(self) -> Iterator[None]:\n        \"\"\"Scope non-cached resolution values to a single dependency graph.\"\"\"\n        self._resolution_depth += 1\n        try:\n            yield\n        finally:\n            self._resolution_depth -= 1\n            if self._resolution_depth == 0:\n                self._resolution_cache.clear()\n\n    async def set(self, name: str, val: Any) -> None:\n        \"\"\"Register a resource instance under a name.\"\"\"\n        self.resources.update({name: val})\n\n    async def get(self, resource: ResourceDescriptor) -> Any:\n        if self._resolution_depth == 0:\n            with self.resolution_scope():\n                return await self._get(resource)\n        return await self._get(resource)\n\n    async def _get(self, resource: ResourceDescriptor) -> Any:\n        \"\"\"Return a resource instance, honoring cache settings.\n\n        Works with any ResourceDescriptor implementation (_Resource or _ResourceConfig).\n        \"\"\"\n        # Cycle detection\n        if resource.name in self._resolving:\n            chain = \" -> \".join(self._resolving) + f\" -> {resource.name}\"\n            raise ValueError(f\"Circular resource dependency detected: {chain}\")\n\n        # Check cache first (before marking as resolving)\n        if resource.cache and resource.name in self.resources:\n            return self.resources[resource.name]\n        if resource.name in self._resolution_cache:\n            return self._resolution_cache[resource.name]\n\n        # Mark as resolving for cycle detection\n        self._resolving.append(resource.name)\n        try:\n            val = await resource.resolve(self)\n            if resource.cache:\n                await self.set(resource.name, val)\n            self._resolution_cache[resource.name] = val\n            return val\n        finally:\n            if resource.name in self._resolving:\n                self._resolving.remove(resource.name)\n\n    def get_all(self) -> dict[str, Any]:\n        \"\"\"Return all materialized resources.\"\"\"\n        return self.resources\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/retry_policy.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport random\nimport re\nimport warnings\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta\nfrom typing import Protocol, cast, runtime_checkable\n\ntime_unit_type = int | float | timedelta\n\n\ndef _to_seconds(value: time_unit_type) -> float:\n    return float(value.total_seconds() if isinstance(value, timedelta) else value)\n\n\n@dataclass(frozen=True)\nclass RetryInfo:\n    \"\"\"Snapshot of the currently-executing step's retry state.\n\n    Returned by ``Context.retry_info()``. On the first attempt ``retry_number``\n    is 0, ``elapsed_seconds`` is 0.0, and both ``last_exception`` and\n    ``last_failed_at`` are ``None``. On subsequent retries they describe the\n    most recent prior failure.\n\n    Attributes:\n        retry_number: 0 on the first run, 1 on the first retry, and so on.\n        elapsed_seconds: Seconds since the first attempt began.\n        last_exception: The most recent prior exception, or ``None``.\n            ``__traceback__`` is available in-process but is lost after a\n            replay from persisted state.\n        last_failed_at: Timezone-aware UTC datetime of the most recent prior\n            failure, or ``None``.\n    \"\"\"\n\n    retry_number: int\n    elapsed_seconds: float\n    last_exception: Exception | None\n    last_failed_at: datetime | None\n\n\n@runtime_checkable\nclass RetryPolicy(Protocol):\n    \"\"\"\n    Structural interface for step retry policies.\n\n    Any object with a compatible ``next`` method satisfies this protocol,\n    including policies built with `retry_policy()`, `ConstantDelayRetryPolicy`,\n    `ExponentialBackoffRetryPolicy`, and user-defined policies.\n\n    Most users do not implement this protocol directly. Instead, construct a\n    policy with ``retry_policy(retry=..., wait=..., stop=...)`` and combine\n    retry conditions, wait strategies, and stop conditions with the operators\n    supported by this module.\n\n    Examples:\n        ```python\n        from workflows.retry_policy import (\n            retry_policy,\n            retry_if_exception_type,\n            stop_after_attempt,\n            wait_exponential,\n        )\n\n        policy = retry_policy(\n            retry=retry_if_exception_type((TimeoutError, ConnectionError)),\n            wait=wait_exponential(multiplier=1, exp_base=2, max=30),\n            stop=stop_after_attempt(5),\n        )\n        ```\n\n    See Also:\n        - [step][workflows.decorators.step]\n    \"\"\"\n\n    def next(\n        self,\n        elapsed_time: float,\n        attempts: int,\n        error: Exception,\n        *,\n        seed: int | None = None,\n    ) -> float | None:\n        \"\"\"\n        Decide if another retry should occur and the delay before it.\n\n        Args:\n            elapsed_time: Seconds since the first failure.\n            attempts: Number of attempts made so far.\n            error: The last exception encountered.\n            seed: Optional RNG seed for deterministic jitter (DBOS replay).\n\n        Returns:\n            Seconds to wait before retrying, or ``None`` to stop.\n        \"\"\"\n\n\nclass RetryCondition(Protocol):\n    \"\"\"Predicate that decides whether an exception is retryable.\"\"\"\n\n    def __call__(self, error: BaseException) -> bool: ...\n\n\nclass WaitStrategy(Protocol):\n    \"\"\"Compute the delay in seconds before the next retry attempt.\"\"\"\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float: ...\n\n\nclass StopCondition(Protocol):\n    \"\"\"Predicate that decides whether retries should stop.\"\"\"\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool: ...\n\n\nclass _RetryConditionBase:\n    \"\"\"Base class for retry predicates that support tenacity-style composition.\"\"\"\n\n    def __call__(self, error: BaseException) -> bool:\n        raise NotImplementedError\n\n    def __and__(self, other: RetryCondition) -> retry_all:\n        return retry_all(self, other)\n\n    def __rand__(self, other: RetryCondition) -> retry_all:\n        return retry_all(other, self)\n\n    def __or__(self, other: RetryCondition) -> retry_any:\n        return retry_any(self, other)\n\n    def __ror__(self, other: RetryCondition) -> retry_any:\n        return retry_any(other, self)\n\n\nclass _WaitStrategyBase:\n    \"\"\"Base class for wait strategies that support addition and ``sum()``.\"\"\"\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        raise NotImplementedError\n\n    def __add__(self, other: WaitStrategy) -> wait_combine:\n        return wait_combine(self, other)\n\n    def __radd__(self, other: object) -> object:\n        if other == 0:\n            return self\n        if callable(other):\n            return self.__add__(cast(WaitStrategy, other))\n        return NotImplemented\n\n\nclass _StopConditionBase:\n    \"\"\"Base class for stop conditions that support ``&`` and ``|`` operators.\"\"\"\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool:\n        raise NotImplementedError\n\n    def __and__(self, other: StopCondition) -> stop_all:\n        return stop_all(self, other)\n\n    def __rand__(self, other: StopCondition) -> stop_all:\n        return stop_all(other, self)\n\n    def __or__(self, other: StopCondition) -> stop_any:\n        return stop_any(self, other)\n\n    def __ror__(self, other: StopCondition) -> stop_any:\n        return stop_any(other, self)\n\n\ndef _compile_pattern(match: str | re.Pattern[str] | None) -> re.Pattern[str] | None:\n    if match is None:\n        return None\n    if isinstance(match, str):\n        return re.compile(match)\n    return match\n\n\nclass retry_if_exception(_RetryConditionBase):\n    \"\"\"\n    Retry when the raised exception satisfies a custom predicate.\n\n    Use this when your retry decision depends on exception details that are not\n    covered by the built-in helpers.\n\n    Examples:\n        ```python\n        retry_if_exception(lambda error: \"rate limit\" in str(error).lower())\n        ```\n    \"\"\"\n\n    def __init__(self, predicate: Callable[[BaseException], bool]) -> None:\n        self.predicate = predicate\n\n    def __call__(self, error: BaseException) -> bool:\n        return self.predicate(error)\n\n\nclass retry_if_exception_type(retry_if_exception):\n    \"\"\"\n    Retry only when the exception is an instance of one of the given types.\n\n    This is the most common retry predicate for transient network and provider\n    failures.\n\n    Examples:\n        ```python\n        retry_if_exception_type((TimeoutError, ConnectionError))\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        exception_types: type[BaseException]\n        | tuple[type[BaseException], ...] = Exception,\n    ) -> None:\n        self.exception_types = exception_types\n        super().__init__(lambda error: isinstance(error, exception_types))\n\n\nclass retry_if_not_exception_type(retry_if_exception):\n    \"\"\"\n    Retry unless the exception is an instance of one of the given types.\n\n    This is useful when most failures are retryable except for a small set of\n    known permanent errors.\n\n    Examples:\n        ```python\n        retry_if_not_exception_type((ValueError, PermissionError))\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        exception_types: type[BaseException]\n        | tuple[type[BaseException], ...] = Exception,\n    ) -> None:\n        self.exception_types = exception_types\n        super().__init__(lambda error: not isinstance(error, exception_types))\n\n\nclass retry_unless_exception_type(retry_if_not_exception_type):\n    \"\"\"\n    Retry unless the exception is an instance of one of the given types.\n\n    Tenacity-style alias for `retry_if_not_exception_type`.\n\n    Examples:\n        ```python\n        retry_unless_exception_type(AuthenticationError)\n        ```\n    \"\"\"\n\n    pass\n\n\nclass retry_if_exception_message(_RetryConditionBase):\n    \"\"\"\n    Retry when the exception message matches an exact string or regex pattern.\n\n    Pass either ``message`` for an exact string match or ``match`` for a regular\n    expression. Passing both is an error.\n\n    Examples:\n        ```python\n        retry_if_exception_message(message=\"please retry\")\n        retry_if_exception_message(match=r\"HTTP 5\\\\d\\\\d\")\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        message: str | None = None,\n        match: str | re.Pattern[str] | None = None,\n    ) -> None:\n        if message is None and match is None:\n            raise TypeError(\n                \"retry_if_exception_message() missing 1 required argument \"\n                \"'message' or 'match'\"\n            )\n        if message is not None and match is not None:\n            raise TypeError(\n                \"retry_if_exception_message() takes either 'message' or 'match', not both\"\n            )\n        self.message = message\n        self.match = match\n        self._pattern = _compile_pattern(match)\n\n    def __call__(self, error: BaseException) -> bool:\n        error_message = str(error)\n        if self.message is not None:\n            return error_message == self.message\n        return bool(self._pattern and self._pattern.search(error_message))\n\n\nclass retry_if_not_exception_message(retry_if_exception_message):\n    \"\"\"\n    Retry when the exception message does not match the given string or regex.\n\n    This is useful when a provider uses specific messages to signal permanent\n    failures that should stop retries.\n\n    Examples:\n        ```python\n        retry_if_not_exception_message(match=\"invalid_api_key|permission denied\")\n        ```\n    \"\"\"\n\n    def __call__(self, error: BaseException) -> bool:\n        return not super().__call__(error)\n\n\nclass retry_if_exception_cause_type(_RetryConditionBase):\n    \"\"\"\n    Retry when any exception in the ``__cause__`` chain matches the given type.\n\n    Only explicit exception chaining (``raise X from Y``) is followed. Implicit\n    chaining via ``__context__`` is **not** inspected, matching tenacity's\n    behavior. If you need to match implicitly chained exceptions, use\n    `retry_if_exception` with a custom predicate that walks ``__context__``.\n\n    Examples:\n        ```python\n        retry_if_exception_cause_type(ConnectionError)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        exception_types: type[BaseException]\n        | tuple[type[BaseException], ...] = Exception,\n    ) -> None:\n        self.exception_types = exception_types\n\n    def __call__(self, error: BaseException) -> bool:\n        current: BaseException | None = error\n        while current is not None:\n            cause = current.__cause__\n            if isinstance(cause, self.exception_types):\n                return True\n            current = cause\n        return False\n\n\nclass retry_any(_RetryConditionBase):\n    \"\"\"\n    Retry if any of the provided retry predicates match.\n\n    Equivalent to combining retry predicates with ``|``.\n\n    Examples:\n        ```python\n        retry_any(\n            retry_if_exception_type(ConnectionError),\n            retry_if_exception_message(match=\"rate limit\"),\n        )\n        ```\n    \"\"\"\n\n    def __init__(self, *retries: RetryCondition) -> None:\n        self.retries = retries\n\n    def __call__(self, error: BaseException) -> bool:\n        return any(retry(error) for retry in self.retries)\n\n\nclass retry_all(_RetryConditionBase):\n    \"\"\"\n    Retry if all of the provided retry predicates match.\n\n    Equivalent to combining retry predicates with ``&``.\n\n    Examples:\n        ```python\n        retry_all(\n            retry_if_exception_type(RuntimeError),\n            retry_if_exception_message(match=\"temporary\"),\n        )\n        ```\n    \"\"\"\n\n    def __init__(self, *retries: RetryCondition) -> None:\n        self.retries = retries\n\n    def __call__(self, error: BaseException) -> bool:\n        return all(retry(error) for retry in self.retries)\n\n\nclass retry_always(_RetryConditionBase):\n    \"\"\"\n    Retry condition that always retries.\n\n    This is mainly useful when you want to be explicit in a composed policy.\n\n    Examples:\n        ```python\n        retry_policy(retry=retry_always(), stop=stop_after_attempt(3))\n        ```\n    \"\"\"\n\n    def __call__(self, error: BaseException) -> bool:\n        return True\n\n\nclass retry_never(_RetryConditionBase):\n    \"\"\"\n    Retry condition that never retries.\n\n    This can be useful in tests or to disable one branch of a composed retry\n    expression.\n\n    Examples:\n        ```python\n        retry_never() | retry_if_exception_type(ConnectionError)\n        ```\n    \"\"\"\n\n    def __call__(self, error: BaseException) -> bool:\n        return False\n\n\nclass wait_fixed(_WaitStrategyBase):\n    \"\"\"\n    Wait a fixed number of seconds between attempts.\n\n    Examples:\n        ```python\n        wait_fixed(5)\n        ```\n    \"\"\"\n\n    def __init__(self, wait: time_unit_type) -> None:\n        self.wait = _to_seconds(wait)\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        return self.wait\n\n\nclass wait_none(wait_fixed):\n    \"\"\"\n    Wait strategy that does not delay retries.\n\n    Examples:\n        ```python\n        wait_none()\n        ```\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(0)\n\n\nclass wait_exponential(_WaitStrategyBase):\n    \"\"\"\n    Wait with exponentially increasing delays, clamped between ``min`` and ``max``.\n\n    The delay for attempt ``n`` is ``multiplier * exp_base**n`` before clamping.\n\n    Examples:\n        ```python\n        wait_exponential(multiplier=1, exp_base=2, max=60)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        multiplier: int | float = 1.0,\n        exp_base: int | float = 2.0,\n        max: time_unit_type = 60.0,\n        min: time_unit_type = 0.0,\n    ) -> None:\n        self.multiplier = float(multiplier)\n        self.exp_base = float(exp_base)\n        self.max = _to_seconds(max)\n        self.min = _to_seconds(min)\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        return max(\n            max(0.0, self.min),\n            min(self.multiplier * self.exp_base**attempts, self.max),\n        )\n\n\nclass wait_incrementing(_WaitStrategyBase):\n    \"\"\"\n    Wait an incrementally larger amount after each attempt.\n\n    The delay starts at ``start`` and increases by ``increment`` on each retry,\n    capped by ``max`` and never going below zero.\n\n    Examples:\n        ```python\n        wait_incrementing(start=1, increment=2, max=10)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        start: time_unit_type = 0.0,\n        increment: time_unit_type = 100.0,\n        max: time_unit_type = float(\"inf\"),\n    ) -> None:\n        self.start = _to_seconds(start)\n        self.increment = _to_seconds(increment)\n        self.max = _to_seconds(max)\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        result = self.start + (self.increment * attempts)\n        return max(0.0, min(result, self.max))\n\n\nclass wait_random(_WaitStrategyBase):\n    \"\"\"\n    Wait a random duration uniformly sampled from ``[min, max]``.\n\n    When the workflow runtime provides a ``seed``, the sampled value is\n    deterministic across replayed runs.\n\n    Examples:\n        ```python\n        wait_random(min=0.5, max=1.5)\n        ```\n    \"\"\"\n\n    def __init__(self, min: time_unit_type = 0.0, max: time_unit_type = 1.0) -> None:\n        self.min = _to_seconds(min)\n        self.max = _to_seconds(max)\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        rng = random.Random(seed) if seed is not None else random\n        return rng.uniform(self.min, self.max)\n\n\nclass wait_exponential_jitter(_WaitStrategyBase):\n    \"\"\"\n    Exponential backoff with additive random jitter.\n\n    The deterministic base delay grows exponentially and a random value in\n    ``[0, jitter]`` is added on top.\n\n    Examples:\n        ```python\n        wait_exponential_jitter(initial=1, exp_base=2, max=60, jitter=1)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        initial: float = 1.0,\n        exp_base: float = 2.0,\n        max: float = 60.0,\n        jitter: float = 1.0,\n    ) -> None:\n        self.initial = initial\n        self.exp_base = exp_base\n        self.max = max\n        self.jitter = jitter\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        base = min(self.initial * self.exp_base**attempts, self.max)\n        rng = random.Random(seed) if seed is not None else random\n        return min(base + rng.uniform(0, self.jitter), self.max)\n\n\nclass wait_random_exponential(_WaitStrategyBase):\n    \"\"\"\n    Exponential backoff with full jitter.\n\n    A random delay is sampled between ``min`` and the exponential upper bound\n    for the current attempt.\n\n    Examples:\n        ```python\n        wait_random_exponential(multiplier=1, exp_base=2, max=60)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        multiplier: int | float = 1.0,\n        exp_base: int | float = 2.0,\n        max: time_unit_type = 60.0,\n        min: time_unit_type = 0.0,\n    ) -> None:\n        self.multiplier = float(multiplier)\n        self.exp_base = float(exp_base)\n        self.max = _to_seconds(max)\n        self.min = _to_seconds(min)\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        rng = random.Random(seed) if seed is not None else random\n        upper = max(\n            max(0.0, self.min),\n            min(self.multiplier * self.exp_base**attempts, self.max),\n        )\n        return rng.uniform(self.min, upper)\n\n\ndef wait_full_jitter(\n    multiplier: int | float = 1.0,\n    exp_base: int | float = 2.0,\n    max: time_unit_type = 60.0,\n    min: time_unit_type = 0.0,\n) -> wait_random_exponential:\n    \"\"\"\n    Alias for `wait_random_exponential`.\n\n    Examples:\n        ```python\n        wait_full_jitter(multiplier=1, exp_base=2, max=60)\n        ```\n    \"\"\"\n\n    return wait_random_exponential(\n        multiplier=multiplier,\n        exp_base=exp_base,\n        max=max,\n        min=min,\n    )\n\n\nclass wait_chain(_WaitStrategyBase):\n    \"\"\"\n    Use a different wait strategy for each attempt in order.\n\n    After the provided strategies are exhausted, the last strategy is reused\n    for all subsequent attempts.\n\n    Examples:\n        ```python\n        wait_chain(wait_fixed(1), wait_fixed(2), wait_fixed(5))\n        ```\n    \"\"\"\n\n    def __init__(self, *strategies: WaitStrategy) -> None:\n        if not strategies:\n            raise ValueError(\"wait_chain requires at least one strategy\")\n        self.strategies = strategies\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        idx = min(attempts, len(self.strategies) - 1)\n        return self.strategies[idx](attempts, seed=seed)\n\n\nclass wait_combine(_WaitStrategyBase):\n    \"\"\"\n    Combine multiple wait strategies by summing their delays.\n\n    Equivalent to combining waits with ``+``.\n\n    Examples:\n        ```python\n        wait_combine(wait_fixed(1), wait_random(0, 1))\n        ```\n    \"\"\"\n\n    def __init__(self, *strategies: WaitStrategy) -> None:\n        self.strategies = strategies\n\n    def __call__(self, attempts: int, *, seed: int | None = None) -> float:\n        return sum(strategy(attempts, seed=seed) for strategy in self.strategies)\n\n\nclass stop_after_attempt(_StopConditionBase):\n    \"\"\"\n    Stop after a fixed number of attempts.\n\n    Examples:\n        ```python\n        stop_after_attempt(5)\n        ```\n    \"\"\"\n\n    def __init__(self, max_attempt_number: int) -> None:\n        self.max_attempt_number = max_attempt_number\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool:\n        return attempts >= self.max_attempt_number\n\n\nclass stop_after_delay(_StopConditionBase):\n    \"\"\"\n    Stop after a maximum elapsed time in seconds.\n\n    Examples:\n        ```python\n        stop_after_delay(30)\n        ```\n    \"\"\"\n\n    def __init__(self, max_delay: time_unit_type) -> None:\n        self.max_delay = _to_seconds(max_delay)\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool:\n        return elapsed_time >= self.max_delay\n\n\nclass stop_before_delay(_StopConditionBase):\n    \"\"\"\n    Stop if the next sleep would move the retry past the configured limit.\n\n    Unlike `stop_after_delay`, this condition considers the ``upcoming_sleep``\n    value produced by the wait strategy.\n\n    Examples:\n        ```python\n        stop_before_delay(30)\n        ```\n    \"\"\"\n\n    def __init__(self, max_delay: time_unit_type) -> None:\n        self.max_delay = _to_seconds(max_delay)\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool:\n        return elapsed_time + upcoming_sleep >= self.max_delay\n\n\nclass stop_any(_StopConditionBase):\n    \"\"\"\n    Stop if any of the provided stop predicates match.\n\n    Equivalent to combining stop conditions with ``|``.\n\n    Examples:\n        ```python\n        stop_any(stop_after_attempt(5), stop_after_delay(30))\n        ```\n    \"\"\"\n\n    def __init__(self, *stops: StopCondition) -> None:\n        self.stops = stops\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool:\n        return any(\n            stop(attempts, elapsed_time, upcoming_sleep=upcoming_sleep)\n            for stop in self.stops\n        )\n\n\nclass stop_all(_StopConditionBase):\n    \"\"\"\n    Stop if all of the provided stop predicates match.\n\n    Equivalent to combining stop conditions with ``&``.\n\n    Examples:\n        ```python\n        stop_all(stop_after_attempt(5), stop_after_delay(30))\n        ```\n    \"\"\"\n\n    def __init__(self, *stops: StopCondition) -> None:\n        self.stops = stops\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool:\n        return all(\n            stop(attempts, elapsed_time, upcoming_sleep=upcoming_sleep)\n            for stop in self.stops\n        )\n\n\nclass stop_never(_StopConditionBase):\n    \"\"\"\n    Stop condition that never stops.\n\n    This is typically paired with a retry predicate or workflow timeout that\n    provides the real upper bound.\n\n    Examples:\n        ```python\n        stop_never()\n        ```\n    \"\"\"\n\n    def __call__(\n        self, attempts: int, elapsed_time: float, *, upcoming_sleep: float = 0.0\n    ) -> bool:\n        return False\n\n\nclass _ComposableRetryPolicy:\n    \"\"\"\n    Composable retry policy built from retry conditions, wait strategies, and stop conditions.\n\n    Decomposes retry behavior into three orthogonal concerns:\n\n    - **retry**: Should we retry this error? (default: retry any exception)\n    - **wait**: How long to wait before the next attempt?\n    - **stop**: When to give up?\n\n    Users typically construct this through ``retry_policy(...)`` rather than by\n    referencing this internal class directly.\n    \"\"\"\n\n    def __init__(\n        self,\n        retry: RetryCondition | None = None,\n        wait: WaitStrategy = wait_fixed(5),\n        stop: StopCondition = stop_after_attempt(3),\n    ) -> None:\n        self.retry = retry\n        self.wait = wait\n        self.stop = stop\n\n    def next(\n        self,\n        elapsed_time: float,\n        attempts: int,\n        error: Exception,\n        *,\n        seed: int | None = None,\n    ) -> float | None:\n        if self.retry is not None and not self.retry(error):\n            return None\n\n        delay = self.wait(attempts, seed=seed)\n        if self.stop(attempts, elapsed_time, upcoming_sleep=delay):\n            return None\n        return delay\n\n\ndef retry_policy(\n    retry: RetryCondition | None = None,\n    wait: WaitStrategy = wait_fixed(5),\n    stop: StopCondition = stop_after_attempt(3),\n) -> RetryPolicy:\n    \"\"\"\n    Construct a composable retry policy from retry, wait, and stop components.\n\n    This is the primary way to create retry policies. Combine retry conditions,\n    wait strategies, and stop conditions using operators (``|``, ``&``, ``+``)\n    or the named combinators.\n\n    Examples:\n        ```python\n        from workflows.retry_policy import (\n            retry_policy,\n            retry_if_exception_type,\n            stop_after_attempt,\n            wait_exponential,\n        )\n\n        policy = retry_policy(\n            retry=retry_if_exception_type((TimeoutError, ConnectionError)),\n            wait=wait_exponential(multiplier=1, exp_base=2, max=30),\n            stop=stop_after_attempt(5),\n        )\n        ```\n\n    With no arguments, ``retry_policy()`` retries all exceptions up to 3\n    attempts with a 5-second fixed delay between each.\n\n    Args:\n        retry: Predicate that decides whether an exception is retryable.\n            When ``None``, all exceptions are retried.\n        wait: Strategy that computes the delay before the next attempt.\n            Defaults to ``wait_fixed(5)`` (5 seconds).\n        stop: Predicate that decides when to give up.\n            Defaults to ``stop_after_attempt(3)``.\n\n    Returns:\n        A `RetryPolicy` implementation.\n    \"\"\"\n    return _ComposableRetryPolicy(retry=retry, wait=wait, stop=stop)\n\n\ndef ConstantDelayRetryPolicy(\n    maximum_attempts: int = 3,\n    delay: float = 5,\n) -> RetryPolicy:\n    \"\"\"\n    Retry at a fixed interval up to a maximum number of attempts.\n\n    Deprecated: use ``retry_policy(wait=wait_fixed(delay), stop=stop_after_attempt(n))`` instead.\n\n    Examples:\n        ```python\n        ConstantDelayRetryPolicy(delay=5, maximum_attempts=10)\n        ```\n    \"\"\"\n    warnings.warn(\n        \"ConstantDelayRetryPolicy is deprecated, use \"\n        \"retry_policy(wait=wait_fixed(delay), stop=stop_after_attempt(n)) instead\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n\n    return _ComposableRetryPolicy(\n        wait=wait_fixed(delay),\n        stop=stop_after_attempt(maximum_attempts),\n    )\n\n\ndef ExponentialBackoffRetryPolicy(\n    maximum_attempts: int = 5,\n    initial_delay: float = 1.0,\n    multiplier: float = 2.0,\n    max_delay: float = 60.0,\n    jitter: bool = True,\n) -> RetryPolicy:\n    \"\"\"\n    Retry with exponentially increasing delays, optional jitter, and a cap.\n\n    Deprecated: use ``retry_policy(wait=wait_exponential(...), stop=stop_after_attempt(n))`` instead.\n    For jitter, use ``wait_random_exponential`` or ``wait_exponential_jitter``.\n\n    Examples:\n        ```python\n        ExponentialBackoffRetryPolicy(\n            initial_delay=1,\n            multiplier=2,\n            max_delay=30,\n            maximum_attempts=5,\n            jitter=True,\n        )\n        ```\n    \"\"\"\n    warnings.warn(\n        \"ExponentialBackoffRetryPolicy is deprecated, use \"\n        \"retry_policy(wait=wait_exponential(...), stop=stop_after_attempt(n)) instead\",\n        DeprecationWarning,\n        stacklevel=2,\n    )\n\n    wait: WaitStrategy\n    if jitter:\n        wait = wait_random_exponential(\n            multiplier=initial_delay,\n            exp_base=multiplier,\n            max=max_delay,\n        )\n    else:\n        wait = wait_exponential(\n            multiplier=initial_delay,\n            exp_base=multiplier,\n            max=max_delay,\n        )\n\n    return _ComposableRetryPolicy(\n        wait=wait,\n        stop=stop_after_attempt(maximum_attempts),\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/control_loop.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport hashlib\nimport heapq\nimport inspect\nimport logging\nimport time\nfrom collections.abc import AsyncIterable\nfrom dataclasses import dataclass, replace\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING\n\nfrom workflows.errors import (\n    WorkflowCancelledByUser,\n    WorkflowRuntimeError,\n    WorkflowTimeoutError,\n)\nfrom workflows.events import (\n    Event,\n    IdleReleasedEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StepFailedEvent,\n    StepState,\n    StepStateChanged,\n    StopEvent,\n    UnhandledEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n    WorkflowIdleEvent,\n    WorkflowTimedOutEvent,\n)\nfrom workflows.runtime.types.commands import (\n    CommandCompleteRun,\n    CommandFailWorkflow,\n    CommandHalt,\n    CommandPublishEvent,\n    CommandQueueEvent,\n    CommandRunWorker,\n    CommandScheduleIdleCheck,\n    CommandScheduleWaiterTimeout,\n    WorkflowCommand,\n    indicates_exit,\n)\nfrom workflows.runtime.types.internal_state import (\n    BrokerState,\n    EventAttempt,\n    InProgressState,\n    InternalStepWorkerState,\n)\nfrom workflows.runtime.types.named_task import (\n    PendingPull,\n    PendingStart,\n    PendingWorker,\n    PullTask,\n    WorkerTask,\n)\nfrom workflows.runtime.types.plugin import (\n    InternalRunAdapter,\n    WaitResultTick,\n    consume_current_run,\n)\nfrom workflows.runtime.types.results import (\n    AddCollectedEvent,\n    AddWaiter,\n    DeleteCollectedEvent,\n    DeleteWaiter,\n    RetryAttempt,\n    StepWorkerFailed,\n    StepWorkerResult,\n    StepWorkerState,\n    StepWorkerWaiter,\n)\nfrom workflows.runtime.types.ticks import (\n    TickAddEvent,\n    TickCancelRun,\n    TickIdleCheck,\n    TickIdleRelease,\n    TickPublishEvent,\n    TickStepResult,\n    TickTimeout,\n    TickWaiterTimeout,\n    WorkflowTick,\n)\nfrom workflows.workflow import Workflow\n\n\ndef _is_shutdown_error(e: BaseException) -> bool:\n    if isinstance(e, (asyncio.CancelledError, KeyboardInterrupt)):\n        return True\n    msg = str(e)\n    return (\n        \"cannot schedule new futures after shutdown\" in msg\n        or \"Event loop is closed\" in msg\n    )\n\n\nasync def _single_pull(adapter: InternalRunAdapter) -> WorkflowTick | None:\n    \"\"\"Single-iteration pull: calls wait_receive once and returns the tick.\n\n    Returns None if timeout (shouldn't happen with unbounded wait).\n    \"\"\"\n    wait_result = await adapter.wait_receive(None)\n    if isinstance(wait_result, WaitResultTick):\n        return wait_result.tick\n    return None\n\n\nif TYPE_CHECKING:\n    from workflows.context.context import Context\n    from workflows.runtime.types.step_function import StepWorkerFunction\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass _ControlLoopRunner:\n    \"\"\"\n    Private class to encapsulate the async control loop runtime state and behavior.\n    Keeps the pure transformation functions at module level for testability.\n\n    This control loop uses a sequential, deterministic design:\n    - Scheduled wakeups are tracked in a heap (for timeouts/delays)\n    - External events come via wait_receive\n    - No concurrent timeout tasks, ensuring deterministic ordering for replay\n    \"\"\"\n\n    def __init__(\n        self,\n        workflow: Workflow,\n        adapter: InternalRunAdapter,\n        context: Context,\n        step_workers: dict[str, StepWorkerFunction],\n        init_state: BrokerState,\n    ):\n        self.workflow = workflow\n        self.adapter = adapter\n        self.context = context\n        self.step_workers = step_workers\n        self.state = init_state\n        self.worker_tasks: set[asyncio.Task[TickStepResult]] = set()\n        # Transient tick buffer - drained synchronously at start of each loop iteration\n        self.tick_buffer: list[WorkflowTick] = []\n        # Pending items to be processed (from rehydration or delayed ticks)\n        for tick in self.state.rehydrate_with_ticks():\n            self.tick_buffer.append(tick)\n        # Scheduled wakeups: heap of (wakeup_time, sequence, tick) tuples\n        # The sequence counter ensures deterministic ordering when timestamps are equal,\n        # avoiding TypeError from comparing WorkflowTick objects that don't implement __lt__\n        self.scheduled_wakeups: list[tuple[float, int, WorkflowTick]] = []\n        self._wakeup_sequence = 0\n        # Pull task sequence counter for deterministic journaling\n        self._pull_sequence = 0\n        # Map from worker task to (step_name, worker_id) key\n        self._task_keys: dict[asyncio.Task[TickStepResult], tuple[str, int]] = {}\n        # Whether a TickIdleCheck is currently in tick_buffer\n        self._idle_check_pending = False\n        # Pending worker coroutines not yet started (started by adapter in wait_for_next_task)\n        self._pending_workers: list[PendingStart] = []\n\n    def schedule_tick(self, tick: WorkflowTick, at_time: float) -> None:\n        \"\"\"Schedule a tick to be processed at a specific time.\"\"\"\n        seq = self._wakeup_sequence\n        self._wakeup_sequence += 1\n        heapq.heappush(self.scheduled_wakeups, (at_time, seq, tick))\n\n    def next_wakeup_timeout(self, now: float) -> float | None:\n        \"\"\"Calculate timeout until next scheduled wakeup.\n\n        Returns None if no scheduled wakeups, otherwise returns\n        the number of seconds until the next scheduled tick is due.\n        \"\"\"\n        if not self.scheduled_wakeups:\n            return None\n        next_time, _, _ = self.scheduled_wakeups[0]\n        return max(0, next_time - now)\n\n    def pop_due_ticks(self, now: float) -> list[WorkflowTick]:\n        \"\"\"Pop all ticks that are due (scheduled time <= now).\"\"\"\n        due = []\n        while self.scheduled_wakeups and self.scheduled_wakeups[0][0] <= now:\n            _, _, tick = heapq.heappop(self.scheduled_wakeups)\n            due.append(tick)\n        return due\n\n    def run_worker(self, command: CommandRunWorker) -> None:\n        \"\"\"Queue a worker for a step function.\n\n        Workers are stored as pending coroutines and started by the adapter\n        in wait_for_next_task, which allows the adapter to control startup\n        ordering for deterministic execution.\n        \"\"\"\n\n        async def _run_worker() -> TickStepResult:\n            try:\n                worker = next(\n                    (\n                        w\n                        for w in self.state.workers[command.step_name].in_progress\n                        if w.worker_id == command.id\n                    ),\n                    None,\n                )\n                if worker is None:\n                    raise WorkflowRuntimeError(\n                        f\"Worker {command.id} not found in in_progress. This should not happen.\"\n                    )\n                snapshot = worker.shared_state\n                step_fn: StepWorkerFunction = self.step_workers[command.step_name]\n\n                result = await step_fn(\n                    state=snapshot,\n                    step_name=command.step_name,\n                    event=command.event,\n                    workflow=self.workflow,\n                    retry=RetryAttempt(\n                        retry_number=worker.attempts,\n                        first_attempt_at=worker.first_attempt_at,\n                        last_exception=worker.last_exception,\n                        last_failed_at=worker.last_failed_at,\n                        recovery_counts=dict(worker.recovery_counts),\n                    ),\n                )\n                # Return result for main loop to process\n                return TickStepResult(\n                    step_name=command.step_name,\n                    worker_id=command.id,\n                    event=command.event,\n                    result=result,\n                )\n            except Exception as e:\n                if _is_shutdown_error(e):\n                    logger.debug(\"step worker interrupted by shutdown: %s\", e)\n                else:\n                    logger.error(\n                        \"error running step worker function: %s\", e, exc_info=True\n                    )\n                return TickStepResult(\n                    step_name=command.step_name,\n                    worker_id=command.id,\n                    event=command.event,\n                    result=[\n                        StepWorkerFailed(\n                            exception=e, failed_at=await self.adapter.get_now()\n                        )\n                    ],\n                )\n\n        self._pending_workers.append(\n            PendingWorker(command.step_name, command.id, _run_worker())\n        )\n\n    async def process_command(self, command: WorkflowCommand) -> None | StopEvent:\n        \"\"\"Process a single command returned from tick reduction.\"\"\"\n        if isinstance(command, CommandQueueEvent):\n            event = TickAddEvent(\n                event=command.event,\n                step_name=command.step_name,\n                attempts=command.attempts,\n                first_attempt_at=command.first_attempt_at,\n                last_exception=command.last_exception,\n                last_failed_at=command.last_failed_at,\n                recovery_counts=dict(command.recovery_counts),\n            )\n            if command.delay is not None and command.delay > 0:\n                now = await self.adapter.get_now()\n                self.schedule_tick(event, at_time=now + command.delay)\n            else:\n                self.tick_buffer.append(event)\n            return None\n        elif isinstance(command, CommandRunWorker):\n            self.run_worker(command)\n            return None\n        elif isinstance(command, CommandHalt):\n            await self.cleanup_tasks()\n            if command.exception is not None:\n                raise command.exception\n        elif isinstance(command, CommandCompleteRun):\n            await self.cleanup_tasks()\n            return command.result\n        elif isinstance(command, CommandPublishEvent):\n            await self.adapter.write_to_event_stream(command.event)\n            return None\n        elif isinstance(command, CommandFailWorkflow):\n            await self.cleanup_tasks()\n            raise command.exception\n        elif isinstance(command, CommandScheduleIdleCheck):\n            if not self._idle_check_pending:\n                self.tick_buffer.append(TickIdleCheck())\n                self._idle_check_pending = True\n            return None\n        elif isinstance(command, CommandScheduleWaiterTimeout):\n            now = await self.adapter.get_now()\n            self.schedule_tick(\n                TickWaiterTimeout(\n                    step_name=command.step_name, waiter_id=command.waiter_id\n                ),\n                at_time=now + command.timeout,\n            )\n            return None\n        else:\n            raise ValueError(f\"Unknown command type: {type(command)}\")\n\n    async def cleanup_tasks(self) -> None:\n        \"\"\"Cancel and cleanup all running worker tasks and pending coroutines.\"\"\"\n        # Close pending coroutines that were never started\n        for p in self._pending_workers:\n            p.coro.close()\n        self._pending_workers.clear()\n\n        # Signal adapter to stop waiting\n        try:\n            await self.adapter.close()\n        except Exception:\n            pass\n\n        # Cancel worker tasks\n        for task in self.worker_tasks:\n            task.cancel()\n\n        try:\n            if self.worker_tasks:\n                await asyncio.wait_for(\n                    asyncio.gather(*self.worker_tasks, return_exceptions=True),\n                    timeout=0.5,\n                )\n        except Exception:\n            pass\n\n        self.worker_tasks.clear()\n        self._task_keys.clear()\n\n    async def run(\n        self, start_event: Event | None = None, start_with_timeout: bool = True\n    ) -> StopEvent:\n        \"\"\"\n        Run the control loop until completion.\n\n        This uses a sequential, deterministic design that combines timeout\n        handling with event waiting in a single operation, ensuring\n        deterministic ordering for replay.\n\n        Args:\n            start_event: Optional initial event to process\n            start_with_timeout: Whether to start the timeout timer\n\n        Returns:\n            The final StopEvent from the workflow\n        \"\"\"\n\n        # Queue initial event\n        if start_event is not None:\n            self.tick_buffer.append(TickAddEvent(event=start_event))\n\n        start = await self.adapter.get_now()\n        # Schedule workflow timeout if configured\n        if start_with_timeout and self.workflow._timeout is not None:\n            # Get initial time\n            timeout_time = start + self.workflow._timeout\n            self.schedule_tick(\n                TickTimeout(timeout=self.workflow._timeout),\n                at_time=timeout_time,\n            )\n\n        # Resume any in-progress work\n        self.state, commands = rewind_in_progress(self.state, start)\n        for command in commands:\n            try:\n                await self.process_command(command)\n            except Exception:\n                await self.cleanup_tasks()\n                raise\n\n        # Initialize pull task (single-iteration)\n        pull_task: asyncio.Task[WorkflowTick | None] | None = None\n\n        # Main event loop\n        try:\n            while True:\n                # Yield to let fire-and-forget tasks run (e.g., ctx.send_event)\n                await asyncio.sleep(0)\n\n                # Get current time\n                now = await self.adapter.get_now()\n\n                # optimization, only reload \"now\" if any work was done\n                was_buffered = bool(self.tick_buffer)\n                # Drain and process buffered ticks first (from rehydration, queue_tick, etc.)\n                while self.tick_buffer:\n                    tick = self.tick_buffer.pop(0)\n                    if isinstance(tick, TickIdleCheck):\n                        self._idle_check_pending = False\n                    result = await self._process_tick(tick)\n                    if result is not None:\n                        return result\n\n                # optimization\n                if was_buffered:\n                    now = await self.adapter.get_now()\n\n                # Calculate timeout for next scheduled wakeup\n                timeout = self.next_wakeup_timeout(now)\n\n                # Build pending list: new workers + pull if needed\n                pending: list[PendingStart] = list(self._pending_workers)\n                self._pending_workers.clear()\n\n                if pull_task is None:\n                    pull_sequence = self._pull_sequence\n                    self._pull_sequence += 1\n                    pending.append(\n                        PendingPull(pull_sequence, _single_pull(self.adapter))\n                    )\n                else:\n                    pull_sequence = self._pull_sequence - 1\n\n                # Build running list from existing tasks\n                running: list[WorkerTask | PullTask] = [\n                    WorkerTask(key[0], key[1], task)\n                    for task in self.worker_tasks\n                    for key in [self._task_keys.get(task)]\n                    if key is not None\n                ]\n                if pull_task is not None:\n                    running.append(PullTask(pull_sequence, pull_task))\n\n                result = await self.adapter.wait_for_next_task(\n                    running, pending, timeout\n                )\n\n                if len(result.started) != len(pending):\n                    raise RuntimeError(\n                        f\"Adapter started {len(result.started)} tasks but \"\n                        f\"{len(pending)} were pending. Every pending coroutine \"\n                        f\"must be started.\"\n                    )\n\n                # Merge started tasks into tracking\n                for nt in result.started:\n                    if isinstance(nt, PullTask):\n                        pull_task = nt.task\n                    elif isinstance(nt, WorkerTask):\n                        self.worker_tasks.add(nt.task)\n                        self._task_keys[nt.task] = (nt.step_name, nt.worker_id)\n\n                completed_task = result.completed\n\n                if completed_task is None:\n                    # Timeout - process scheduled ticks\n                    now = await self.adapter.get_now()\n                    for due_tick in self.pop_due_ticks(now):\n                        self.tick_buffer.append(due_tick)\n                    continue\n\n                # Process the single completed task\n                if completed_task is pull_task:\n                    # Pull task completed\n                    try:\n                        pull_tick = completed_task.result()\n                    except asyncio.CancelledError:\n                        pull_task = None\n                    except Exception:\n                        logger.exception(\"Pull task failed\", exc_info=True)\n                        pull_task = None\n                    else:\n                        pull_task = None\n                        if pull_tick is not None:\n                            self.tick_buffer.append(pull_tick)\n                else:\n                    # Worker task completed\n                    self.worker_tasks.discard(completed_task)\n                    self._task_keys.pop(completed_task, None)\n                    try:\n                        tick_result = completed_task.result()\n                    except asyncio.CancelledError:\n                        pass\n                    except Exception:\n                        logger.exception(\n                            \"Worker task failed unexpectedly\", exc_info=True\n                        )\n                    else:\n                        # Check if this worker returned a StopEvent - if so,\n                        # cancel other workers immediately to prevent them from\n                        # writing to the event stream after workflow completion\n                        for res in tick_result.result:\n                            if isinstance(res, StepWorkerResult) and isinstance(\n                                res.result, StopEvent\n                            ):\n                                await self.cleanup_tasks()\n                                break\n                        self.tick_buffer.append(tick_result)\n\n        finally:\n            # Cancel pull task if running\n            if pull_task is not None:\n                pull_task.cancel()\n                try:\n                    await pull_task\n                except (asyncio.CancelledError, Exception):\n                    pass\n            await self.cleanup_tasks()\n\n    async def _process_tick(self, tick: WorkflowTick) -> StopEvent | None:\n        \"\"\"Process a single tick and return StopEvent if workflow completes.\"\"\"\n        try:\n            start = await self.adapter.get_now()\n            self.state, commands = _reduce_tick(\n                tick, self.state, start, run_id=self.adapter.run_id\n            )\n        except Exception:\n            await self.cleanup_tasks()\n            logger.error(\n                \"Unexpected error in internal control loop of workflow. This shouldn't happen. \",\n                exc_info=True,\n            )\n            raise\n\n        await self.adapter.on_tick(tick)\n\n        for command in commands:\n            try:\n                result = await self.process_command(command)\n            except Exception:\n                await self.cleanup_tasks()\n                raise\n\n            if result is not None:\n                return result\n\n        await self.adapter.after_tick(tick)\n        return None\n\n\nasync def control_loop(\n    start_event: Event | None,\n    init_state: BrokerState | None,\n    run_id: str,\n) -> StopEvent:\n    \"\"\"\n    The main async control loop for a workflow run.\n    \"\"\"\n    # Consume the RunContext immediately so the container's strong reference\n    # to the workflow graph is dropped before any step gets a chance to schedule\n    # an asyncio handle whose Context snapshot would otherwise pin it.\n    run = consume_current_run()\n    state = init_state or BrokerState.from_workflow(run.workflow)\n    runner = _ControlLoopRunner(\n        run.workflow, run.run_adapter, run.context, run.steps, state\n    )\n    return await runner.run(start_event=start_event)\n\n\ndef rebuild_state_from_ticks(\n    state: BrokerState,\n    ticks: list[WorkflowTick],\n) -> BrokerState:\n    \"\"\"Rebuild the state from a list of ticks.\n\n    When reconstructing state (e.g., for checkpointing), we must first apply\n    rewind_in_progress() to match what happens at runtime when resuming a workflow.\n    This clears in_progress, moves events back to the queue, and then re-assigns\n    new worker IDs starting from 0.\n\n    Without this, resuming a workflow and then checkpointing again would fail\n    because the original in_progress worker IDs don't match the new worker IDs\n    assigned after rewind.\n    \"\"\"\n    # Apply rewind_in_progress to match what happens at runtime when resuming.\n    # This re-assigns worker IDs so they align with the ticks that were recorded\n    # after the workflow was resumed.\n    state, _ = rewind_in_progress(state, time.time())\n\n    # Replay ticks to rebuild state\n    for tick in ticks:\n        state, _ = _reduce_tick(\n            tick, state, time.time()\n        )  # somewhat broken kludge on the timestamps, need to move these to ticks\n    return state\n\n\nExitCommand = CommandCompleteRun | CommandFailWorkflow | CommandHalt\n\n\n@dataclass\nclass ReplayResult:\n    \"\"\"Result of replaying a tick stream.\n\n    Attributes:\n        state: Rebuilt broker state after applying all ticks.\n        exit_command: The last exit-indicating command emitted during replay,\n            or None if the stream never terminated. Lets callers classify\n            terminal outcome (success / failure / cancel / timeout) using the\n            same command the runtime would have produced, without a second\n            pass over the ticks.\n    \"\"\"\n\n    state: BrokerState\n    exit_command: ExitCommand | None = None\n\n\nasync def replay_ticks_stream(\n    state: BrokerState,\n    ticks: AsyncIterable[WorkflowTick],\n) -> ReplayResult:\n    \"\"\"Replay a tick stream, returning state plus the last exit-indicating command.\n\n    The reducer already emits CommandCompleteRun / CommandFailWorkflow /\n    CommandHalt when it processes terminal ticks; this surfaces them instead\n    of discarding, so callers can classify terminal outcome (success /\n    failure / cancel / timeout) without a second pass over the ticks.\n    \"\"\"\n    state, _ = rewind_in_progress(state, time.time())\n    exit_command: ExitCommand | None = None\n    async for tick in ticks:\n        state, commands = _reduce_tick(tick, state, time.time())\n        for command in commands:\n            if isinstance(\n                command, (CommandCompleteRun, CommandFailWorkflow, CommandHalt)\n            ):\n                # Last wins: a successful retry supersedes earlier failures.\n                exit_command = command\n    return ReplayResult(state=state, exit_command=exit_command)\n\n\nasync def rebuild_state_from_ticks_stream(\n    state: BrokerState,\n    ticks: AsyncIterable[WorkflowTick],\n) -> BrokerState:\n    \"\"\"Streaming variant of :func:`rebuild_state_from_ticks`.\n\n    Thin wrapper over :func:`replay_ticks_stream` that discards the exit\n    command. Prefer ``replay_ticks_stream`` when you need terminal info.\n    \"\"\"\n    return (await replay_ticks_stream(state, ticks)).state\n\n\ndef _reduce_tick(\n    tick: WorkflowTick,\n    init: BrokerState,\n    now_seconds: float,\n    run_id: str | None = None,\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    if isinstance(tick, TickStepResult):\n        state, commands = _process_step_result_tick(tick, init, now_seconds, run_id)\n    elif isinstance(tick, TickAddEvent):\n        state, commands = _process_add_event_tick(tick, init, now_seconds)\n    elif isinstance(tick, TickCancelRun):\n        state, commands = _process_cancel_run_tick(tick, init)\n    elif isinstance(tick, TickIdleRelease):\n        # Return early — idle release does not schedule idle checks\n        return init, [CommandCompleteRun(result=IdleReleasedEvent())]\n    elif isinstance(tick, TickPublishEvent):\n        state, commands = _process_publish_event_tick(tick, init)\n    elif isinstance(tick, TickTimeout):\n        state, commands = _process_timeout_tick(tick, init)\n    elif isinstance(tick, TickWaiterTimeout):\n        state, commands = _process_waiter_timeout_tick(tick, init, now_seconds)\n    elif isinstance(tick, TickIdleCheck):\n        # Return early — idle check ticks don't schedule further idle checks\n        if _check_idle_state(init):\n            return init, [CommandPublishEvent(WorkflowIdleEvent())]\n        return init, []\n    else:\n        raise ValueError(f\"Unknown tick type: {type(tick)}\")\n\n    # After any non-idle-check tick, schedule an idle check if state is quiescent\n    if _check_idle_state(state):\n        commands.append(CommandScheduleIdleCheck())\n\n    return state, commands\n\n\ndef rewind_in_progress(\n    state: BrokerState,\n    now_seconds: float,\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    \"\"\"Rewind the in_progress state, extracting commands to re-initiate the workers\"\"\"\n    state = state.deepcopy()\n    commands: list[WorkflowCommand] = []\n    for step_name, step_state in sorted(state.workers.items(), key=lambda x: x[0]):\n        for in_progress in step_state.in_progress:\n            step_state.queue.insert(\n                0,\n                EventAttempt(\n                    event=in_progress.event,\n                    attempts=in_progress.attempts,\n                    first_attempt_at=in_progress.first_attempt_at,\n                    last_exception=in_progress.last_exception,\n                    last_failed_at=in_progress.last_failed_at,\n                    recovery_counts=dict(in_progress.recovery_counts),\n                ),\n            )\n        step_state.in_progress = []\n        while (\n            len(step_state.queue) > 0\n            and len(step_state.in_progress) < step_state.config.num_workers\n        ):\n            event = step_state.queue.pop(0)\n            commands.extend(\n                _add_or_enqueue_event(event, step_name, step_state, now_seconds)\n            )\n    return state, commands\n\n\ndef _check_idle_state(state: BrokerState) -> bool:\n    \"\"\"Returns True if workflow is idle (no work can advance internally).\n\n    A workflow is idle when:\n    1. The workflow is running (hasn't completed/failed/cancelled)\n    2. All steps have no pending events in their queues\n    3. All steps have no workers currently executing\n    \"\"\"\n    if not state.is_running:\n        return False\n\n    for worker_state in state.workers.values():\n        if worker_state.queue or worker_state.in_progress:\n            return False\n\n    return True\n\n\ndef _process_step_result_tick(\n    tick: TickStepResult,\n    init: BrokerState,\n    now_seconds: float,\n    run_id: str | None = None,\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    \"\"\"\n    processes the results from a step function execution\n    \"\"\"\n    state = init.deepcopy()\n    commands: list[WorkflowCommand] = []\n    worker_state = state.workers[tick.step_name]\n    # get the current execution details and mark it as no longer in progress\n    this_execution = next(\n        (w for w in worker_state.in_progress if w.worker_id == tick.worker_id), None\n    )\n    if this_execution is None:\n        # this should not happen unless there's a logic bug in the control loop\n        raise ValueError(f\"Worker {tick.worker_id} not found in in_progress\")\n    output_event_name: str | None = None\n\n    did_complete_step = bool(\n        [x for x in tick.result if isinstance(x, StepWorkerResult)]\n    )\n    step_no_longer_in_progress = True\n\n    for result in tick.result:\n        if isinstance(result, StepWorkerResult):\n            output_event_name = str(type(result.result))\n            if isinstance(result.result, StopEvent):\n                # huzzah! The workflow has completed\n                commands.append(\n                    CommandPublishEvent(event=result.result)\n                )  # stop event always published to the stream\n                state.is_running = False\n                # Clear collected_events and collected_waiters since workflow is complete\n                for worker in state.workers.values():\n                    worker.collected_events.clear()\n                    worker.collected_waiters.clear()\n                commands.append(CommandCompleteRun(result=result.result))\n            elif isinstance(result.result, Event):\n                # queue any subsequent events\n                # human input required are automatically published to the stream\n                if isinstance(result.result, InputRequiredEvent):\n                    commands.append(CommandPublishEvent(event=result.result))\n                commands.append(\n                    CommandQueueEvent(\n                        event=result.result,\n                        recovery_counts=dict(this_execution.recovery_counts),\n                    )\n                )\n            elif result.result is None:\n                # None means skip\n                pass\n            else:\n                logger.warning(\n                    f\"Unknown result type returned from step function ({tick.step_name}): {type(result.result)}\"\n                )\n        elif isinstance(result, StepWorkerFailed):\n            # Schedule a retry if permitted, otherwise fail the workflow\n            retries = worker_state.config.retry_policy\n            failures = this_execution.attempts + 1\n            elapsed_time = result.failed_at - this_execution.first_attempt_at\n            jitter_seed = (\n                int(\n                    hashlib.sha256(\n                        f\"{run_id}:{tick.step_name}:{failures}\".encode()\n                    ).hexdigest(),\n                    16,\n                )\n                & 0xFFFF_FFFF\n                if run_id is not None\n                else None\n            )\n            if retries is not None:\n                _next_params = inspect.signature(retries.next).parameters\n                _seed_kwarg = {\"seed\": jitter_seed} if \"seed\" in _next_params else {}\n                delay = retries.next(\n                    elapsed_time, failures, result.exception, **_seed_kwarg\n                )\n            else:\n                delay = None\n            if delay is not None:\n                commands.append(\n                    CommandQueueEvent(\n                        event=tick.event,\n                        delay=delay,\n                        step_name=tick.step_name,\n                        attempts=this_execution.attempts + 1,\n                        first_attempt_at=this_execution.first_attempt_at,\n                        last_exception=result.exception,\n                        last_failed_at=result.failed_at,\n                        recovery_counts=dict(this_execution.recovery_counts),\n                    )\n                )\n            else:\n                exception = result.exception\n                total_attempts = this_execution.attempts + 1\n                elapsed = result.failed_at - this_execution.first_attempt_at\n\n                handler_name = state.config.handler_for_step.get(tick.step_name)\n                handler = (\n                    state.config.catch_error_handlers.get(handler_name)\n                    if handler_name is not None\n                    else None\n                )\n                current_count = (\n                    this_execution.recovery_counts.get(handler.step_name, 0)\n                    if handler is not None\n                    else 0\n                )\n                new_count = current_count + 1\n                should_route = (\n                    handler is not None and new_count <= handler.max_recoveries\n                )\n                if should_route and handler is not None:\n                    # Route to the catch-error handler. Keep workflow running so\n                    # the handler can produce either a StopEvent or a new failure.\n                    step_failed_event = StepFailedEvent(\n                        step_name=tick.step_name,\n                        input_event=tick.event,\n                        exception=exception,\n                        attempts=total_attempts,\n                        elapsed_seconds=elapsed,\n                        failed_at=datetime.fromtimestamp(\n                            result.failed_at, tz=timezone.utc\n                        ),\n                    )\n                    commands.append(\n                        CommandQueueEvent(\n                            event=step_failed_event,\n                            step_name=handler.step_name,\n                            recovery_counts={\n                                **this_execution.recovery_counts,\n                                handler.step_name: new_count,\n                            },\n                        )\n                    )\n                else:\n                    # Publish a WorkflowFailedEvent to inform stream consumers about the failure\n                    state.is_running = False\n                    commands.append(\n                        CommandPublishEvent(\n                            event=WorkflowFailedEvent(\n                                step_name=tick.step_name,\n                                exception=exception,\n                                attempts=total_attempts,\n                                elapsed_seconds=elapsed,\n                            )\n                        )\n                    )\n                    commands.append(\n                        CommandFailWorkflow(\n                            step_name=tick.step_name, exception=exception\n                        )\n                    )\n        elif isinstance(result, AddCollectedEvent):\n            # The current state of collected events.\n            collected_events = state.workers[\n                tick.step_name\n            ].collected_events.setdefault(result.event_id, [])\n            # the events snapshot that was sent with the step function execution that yielded this result\n            sent_events = this_execution.shared_state.collected_events.get(\n                result.event_id, []\n            )\n            if len(collected_events) > len(sent_events):\n                # rerun it, and don't append now to ensure serializability\n                # updating the run state\n                step_no_longer_in_progress = False\n                updated_state = replace(\n                    this_execution.shared_state,\n                    collected_events={\n                        x: list(y)\n                        for x, y in state.workers[\n                            tick.step_name\n                        ].collected_events.items()\n                    },\n                )\n                this_execution.shared_state = updated_state\n                commands.append(\n                    CommandRunWorker(\n                        step_name=tick.step_name,\n                        event=result.event,\n                        id=this_execution.worker_id,\n                    )\n                )\n            else:\n                collected_events.append(result.event)\n        elif isinstance(result, DeleteCollectedEvent):\n            if did_complete_step:  # allow retries to grab the events\n                # indicates that a run has successfully collected its events, and they can be deleted from the collected events state\n                state.workers[tick.step_name].collected_events.pop(\n                    result.event_id, None\n                )\n        elif isinstance(result, AddWaiter):\n            # indicates that a run has added a waiter to the collected waiters state\n            existing = next(\n                (\n                    (i)\n                    for i, x in enumerate(worker_state.collected_waiters)\n                    if x.waiter_id == result.waiter_id\n                ),\n                None,\n            )\n            new_waiter = StepWorkerWaiter(\n                waiter_id=result.waiter_id,\n                event=this_execution.event,\n                waiting_for_event=result.event_type,\n                requirements=result.requirements,\n                has_requirements=bool(len(result.requirements)),\n                resolved_event=None,\n            )\n            if existing is not None:\n                worker_state.collected_waiters[existing] = new_waiter\n            else:\n                worker_state.collected_waiters.append(new_waiter)\n                if result.waiter_event:\n                    commands.append(CommandPublishEvent(event=result.waiter_event))\n                if result.timeout is not None:\n                    commands.append(\n                        CommandScheduleWaiterTimeout(\n                            step_name=tick.step_name,\n                            waiter_id=result.waiter_id,\n                            timeout=result.timeout,\n                        )\n                    )\n\n        elif isinstance(result, DeleteWaiter):\n            if did_complete_step:  # allow retries to grab the waiter events\n                # indicates that a run has obtained the waiting event, and it can be deleted from the collected waiters state\n                to_remove = result.waiter_id\n                waiters = state.workers[tick.step_name].collected_waiters\n                item = next(filter(lambda w: w.waiter_id == to_remove, waiters), None)\n                if item is not None:\n                    waiters.remove(item)\n        else:\n            raise ValueError(f\"Unknown result type: {type(result)}\")\n\n    is_completed = len([x for x in commands if indicates_exit(x)]) > 0\n    if step_no_longer_in_progress:\n        commands.insert(\n            0,\n            CommandPublishEvent(\n                StepStateChanged(\n                    step_state=StepState.NOT_RUNNING,\n                    name=tick.step_name,\n                    input_event_name=str(type(tick.event)),\n                    output_event_name=output_event_name,\n                    worker_id=str(tick.worker_id),\n                )\n            ),\n        )\n        worker_state.in_progress.remove(this_execution)\n    # enqueue next events if there are any\n    if not is_completed:\n        while (\n            len(worker_state.queue) > 0\n            and len(worker_state.in_progress) < worker_state.config.num_workers\n        ):\n            event = worker_state.queue.pop(0)\n            subcommands = _add_or_enqueue_event(\n                event, tick.step_name, worker_state, now_seconds\n            )\n            commands.extend(subcommands)\n\n    return state, commands\n\n\ndef _add_or_enqueue_event(\n    event: EventAttempt,\n    step_name: str,\n    state: InternalStepWorkerState,\n    now_seconds: float,\n) -> list[WorkflowCommand]:\n    \"\"\"\n    Small helper to assist in adding an event to a step worker state, or enqueuing it if it's not accepted.\n    Note! This mutates the state, assuming that its already been deepcopied in an outer scope.\n    \"\"\"\n    commands: list[WorkflowCommand] = []\n    # Determine if there is available capacity based on in_progress workers\n    has_space = len(state.in_progress) < state.config.num_workers\n    if has_space:\n        # Assign the smallest available worker id\n        used = set(x.worker_id for x in state.in_progress)\n        id_candidates = [i for i in range(state.config.num_workers) if i not in used]\n        id = id_candidates[0]\n        state_copy = state._deepcopy()\n        shared_state: StepWorkerState = StepWorkerState(\n            step_name=step_name,\n            collected_events=state_copy.collected_events,\n            collected_waiters=state_copy.collected_waiters,\n        )\n        state.in_progress.append(\n            InProgressState(\n                event=event.event,\n                worker_id=id,\n                shared_state=shared_state,\n                attempts=event.attempts or 0,\n                first_attempt_at=event.first_attempt_at or now_seconds,\n                last_exception=event.last_exception,\n                last_failed_at=event.last_failed_at,\n                recovery_counts=dict(event.recovery_counts),\n            )\n        )\n        commands.append(CommandRunWorker(step_name=step_name, event=event.event, id=id))\n        commands.append(\n            CommandPublishEvent(\n                StepStateChanged(\n                    step_state=StepState.RUNNING,\n                    name=step_name,\n                    input_event_name=type(event.event).__name__,\n                    worker_id=str(id),\n                )\n            )\n        )\n    else:\n        commands.append(\n            CommandPublishEvent(\n                StepStateChanged(\n                    step_state=StepState.PREPARING,\n                    name=step_name,\n                    input_event_name=type(event.event).__name__,\n                    worker_id=\"<enqueued>\",\n                )\n            )\n        )\n        state.queue.append(event)\n    return commands\n\n\ndef _process_add_event_tick(\n    tick: TickAddEvent, init: BrokerState, now_seconds: float\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    state = init.deepcopy()\n    # iterate through the steps, and add to steps work queue if it's accepted.\n    commands: list[WorkflowCommand] = []\n    handled = False\n    if isinstance(tick.event, StartEvent):\n        state.is_running = True\n\n    # First, check if the event resolves any waiters. Track which steps were\n    # woken via waiter resolution so we don't also route the event to them\n    # as a normal accepted event (which would cause duplicate processing).\n    waiter_resolved_steps: set[str] = set()\n    for step_name, step_config in state.config.steps.items():\n        wait_conditions = state.workers[step_name].collected_waiters\n        for wait_condition in wait_conditions:\n            is_match = type(tick.event) is wait_condition.waiting_for_event\n            is_match = is_match and all(\n                getattr(tick.event, k, None) == v\n                for k, v in wait_condition.requirements.items()\n            )\n            if is_match:\n                handled = True\n                waiter_resolved_steps.add(step_name)\n                wait_condition.resolved_event = tick.event\n                subcommands = _add_or_enqueue_event(\n                    EventAttempt(event=wait_condition.event),\n                    step_name,\n                    state.workers[step_name],\n                    now_seconds,\n                )\n                commands.extend(subcommands)\n\n    # Then route to accepting steps, skipping any that were already woken\n    # via waiter resolution above.\n    for step_name, step_config in state.config.steps.items():\n        if step_name in waiter_resolved_steps:\n            continue\n        is_accepted = type(tick.event) in step_config.accepted_events\n        if is_accepted and (tick.step_name is None or tick.step_name == step_name):\n            handled = True\n            subcommands = _add_or_enqueue_event(\n                EventAttempt(\n                    event=tick.event,\n                    attempts=tick.attempts,\n                    first_attempt_at=tick.first_attempt_at,\n                    last_exception=tick.last_exception,\n                    last_failed_at=tick.last_failed_at,\n                    recovery_counts=dict(tick.recovery_counts),\n                ),\n                step_name,\n                state.workers[step_name],\n                now_seconds,\n            )\n            commands.extend(subcommands)\n    if not handled:\n        # InputRequiredEvent subclasses are intentionally designed to be handled\n        # externally by human consumers, not by workflow steps. Don't emit\n        # UnhandledEvent for these since they're working as intended.\n        if not isinstance(tick.event, InputRequiredEvent):\n            event_cls = type(tick.event)\n            commands.append(\n                CommandPublishEvent(\n                    UnhandledEvent(\n                        event_type=event_cls.__name__,\n                        qualified_name=f\"{event_cls.__module__}.{event_cls.__name__}\",\n                        step_name=tick.step_name,\n                        idle=_check_idle_state(state),\n                    )\n                )\n            )\n    return state, commands\n\n\ndef _process_cancel_run_tick(\n    tick: TickCancelRun, init: BrokerState\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    state = init.deepcopy()\n    # Retain running state for resumption.\n    return state, [\n        CommandPublishEvent(event=WorkflowCancelledEvent()),\n        CommandHalt(exception=WorkflowCancelledByUser()),\n    ]\n\n\ndef _process_publish_event_tick(\n    tick: TickPublishEvent, init: BrokerState\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    # doesn't affect state. Pass through as publish command\n    return init, [CommandPublishEvent(event=tick.event)]\n\n\ndef _process_timeout_tick(\n    tick: TickTimeout, init: BrokerState\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    state = init.deepcopy()\n    state.is_running = False\n    active_steps = [\n        step_name\n        for step_name, worker_state in init.workers.items()\n        if len(worker_state.in_progress) > 0\n    ]\n    steps_info = (\n        \"Currently active steps: \" + \", \".join(active_steps)\n        if active_steps\n        else \"No steps active\"\n    )\n    return state, [\n        CommandPublishEvent(\n            event=WorkflowTimedOutEvent(\n                timeout=tick.timeout,\n                active_steps=active_steps,\n            )\n        ),\n        CommandHalt(\n            exception=WorkflowTimeoutError(\n                f\"Operation timed out after {tick.timeout} seconds. {steps_info}\"\n            )\n        ),\n    ]\n\n\ndef _process_waiter_timeout_tick(\n    tick: TickWaiterTimeout, init: BrokerState, now_seconds: float\n) -> tuple[BrokerState, list[WorkflowCommand]]:\n    state = init.deepcopy()\n    commands: list[WorkflowCommand] = []\n    if tick.step_name not in state.workers:\n        return state, commands\n    worker_state = state.workers[tick.step_name]\n    waiter = next(\n        (w for w in worker_state.collected_waiters if w.waiter_id == tick.waiter_id),\n        None,\n    )\n    # Only act if the waiter is still pending (not yet resolved by an event)\n    if waiter is None or waiter.resolved_event is not None:\n        return state, commands\n    waiter.timed_out = True\n    subcommands = _add_or_enqueue_event(\n        EventAttempt(event=waiter.event),\n        tick.step_name,\n        worker_state,\n        now_seconds,\n    )\n    commands.extend(subcommands)\n    return state, commands\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/runtime_decorators.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nBase decorator classes for Runtime, InternalRunAdapter, and ExternalRunAdapter.\n\nThese provide a simple forwarding pattern: accept an inner instance, delegate\nevery method to it. Subclasses override only the methods they need to customise.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom contextlib import contextmanager\nfrom typing import Any, AsyncGenerator, Generator\n\nfrom workflows.context.serializers import BaseSerializer\nfrom workflows.context.state_store import StateStore\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.named_task import NamedTask, PendingStart\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    WaitForNextTaskResult,\n    WaitResult,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\nfrom workflows.workflow import Workflow\n\nlogger = logging.getLogger(__name__)\n\n\nclass BaseRuntimeDecorator(Runtime):\n    \"\"\"Decorator base for :class:`Runtime`.\n\n    Wraps an inner runtime and forwards every call to it.  Subclasses can\n    override individual methods to add behaviour (logging, metrics, auth,\n    etc.) without re-implementing the full interface.\n    \"\"\"\n\n    def __init__(self, decorated: Runtime) -> None:\n        super().__init__()\n        self._decorated = decorated\n\n    def register(self, workflow: Workflow) -> RegisteredWorkflow:\n        return self._decorated.register(workflow)\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> ExternalRunAdapter:\n        return self._decorated.run_workflow(\n            run_id,\n            workflow,\n            init_state,\n            start_event=start_event,\n            serialized_state=serialized_state,\n            serializer=serializer,\n        )\n\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        return self._decorated.get_internal_adapter(workflow)\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        return self._decorated.get_external_adapter(run_id)\n\n    async def launch(self) -> None:\n        await super().launch()\n        await self._decorated.launch()\n\n    @property\n    def is_launched(self) -> bool:\n        return self._decorated.is_launched\n\n    async def destroy(self) -> None:\n        await self._decorated.destroy()\n\n    def track_workflow(self, workflow: Workflow) -> None:\n        self._pending.add(workflow)\n        self._decorated.track_workflow(workflow)\n\n    def untrack_workflow(self, workflow: Workflow) -> None:\n        self._pending.discard(workflow)\n        self._decorated.untrack_workflow(workflow)\n\n    def get_registered(self, workflow: Workflow) -> RegisteredWorkflow | None:\n        return self._decorated.get_registered(workflow)\n\n    @contextmanager\n    def registering(self) -> Generator[Runtime, None, None]:\n        with self._decorated.registering() as rt:\n            yield rt\n\n\nclass BaseInternalRunAdapterDecorator(InternalRunAdapter):\n    \"\"\"Decorator base for :class:`InternalRunAdapter`.\n\n    Wraps an inner adapter and forwards every call to it.  Subclasses can\n    override individual methods to intercept or augment behaviour.\n    \"\"\"\n\n    def __init__(self, decorated: InternalRunAdapter) -> None:\n        self._decorated = decorated\n\n    @property\n    def run_id(self) -> str:\n        return self._decorated.run_id\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        await self._decorated.write_to_event_stream(event)\n\n    async def get_now(self) -> float:\n        return await self._decorated.get_now()\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        await self._decorated.send_event(tick)\n\n    async def wait_receive(\n        self,\n        timeout_seconds: float | None = None,\n    ) -> WaitResult:\n        return await self._decorated.wait_receive(timeout_seconds)\n\n    async def close(self) -> None:\n        await self._decorated.close()\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return self._decorated.get_state_store()\n\n    async def finalize_step(self) -> None:\n        await self._decorated.finalize_step()\n\n    def is_replaying(self) -> bool:\n        return self._decorated.is_replaying()\n\n    async def on_tick(self, tick: WorkflowTick) -> None:\n        await self._decorated.on_tick(tick)\n\n    async def after_tick(self, tick: WorkflowTick) -> None:\n        await self._decorated.after_tick(tick)\n\n    async def wait_for_next_task(\n        self,\n        running: list[NamedTask],\n        pending: list[PendingStart],\n        timeout: float | None = None,\n    ) -> WaitForNextTaskResult:\n        return await self._decorated.wait_for_next_task(running, pending, timeout)\n\n\nclass BaseExternalRunAdapterDecorator(ExternalRunAdapter):\n    \"\"\"Decorator base for :class:`ExternalRunAdapter`.\n\n    Wraps an inner adapter and forwards every call to it.  Subclasses can\n    override individual methods to intercept or augment behaviour.\n    \"\"\"\n\n    def __init__(self, decorated: ExternalRunAdapter) -> None:\n        self._decorated = decorated\n\n    @property\n    def run_id(self) -> str:\n        return self._decorated.run_id\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        await self._decorated.send_event(tick)\n\n    def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        return self._decorated.stream_published_events()\n\n    async def close(self) -> None:\n        await self._decorated.close()\n\n    async def get_result(self) -> StopEvent:\n        return await self._decorated.get_result()\n\n    async def cancel(self) -> None:\n        await self._decorated.cancel()\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        return self._decorated.get_state_store()\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/types/commands.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"\nCommands returned by the control loop's tick reducer.\n\nThe control loop follows a reducer pattern:\n  1. Wait for a tick (event, step result, timeout, etc.)\n  2. Reduce the tick with current state -> (new_state, commands)\n  3. Execute commands (which may spawn async tasks or queue new ticks)\n  4. Repeat\n\nCommands represent imperative actions to take after processing a tick,\nsuch as starting workers, queuing events, or completing the workflow.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\n\nfrom workflows.events import Event, StopEvent\n\n\n@dataclass(frozen=True)\nclass CommandRunWorker:\n    step_name: str\n    event: Event\n    id: int\n\n\n@dataclass(frozen=True)\nclass CommandQueueEvent:\n    event: Event\n    step_name: str | None = None\n    delay: float | None = None\n    attempts: int | None = None\n    first_attempt_at: float | None = None\n    last_exception: Exception | None = None\n    last_failed_at: float | None = None\n    recovery_counts: dict[str, int] = field(default_factory=dict)\n\n\n@dataclass(frozen=True)\nclass CommandHalt:\n    exception: Exception\n\n\n@dataclass(frozen=True)\nclass CommandCompleteRun:\n    result: StopEvent\n\n\n@dataclass(frozen=True)\nclass CommandFailWorkflow:\n    step_name: str\n    exception: Exception\n\n\n@dataclass(frozen=True)\nclass CommandPublishEvent:\n    event: Event\n\n\n@dataclass(frozen=True)\nclass CommandScheduleWaiterTimeout:\n    step_name: str\n    waiter_id: str\n    timeout: float\n\n\n@dataclass(frozen=True)\nclass CommandScheduleIdleCheck:\n    \"\"\"Schedule a deferred idle check via TickIdleCheck.\n\n    Returned by the reducer when state looks quiescent after processing a tick.\n    The runner appends a TickIdleCheck to tick_buffer so that idle is confirmed\n    on the next loop iteration, after an asyncio.sleep(0) yield gives in-flight\n    ctx.send_event() calls a chance to drain.\n    \"\"\"\n\n    pass\n\n\nWorkflowCommand = (\n    CommandRunWorker\n    | CommandQueueEvent\n    | CommandHalt\n    | CommandCompleteRun\n    | CommandFailWorkflow\n    | CommandPublishEvent\n    | CommandScheduleIdleCheck\n    | CommandScheduleWaiterTimeout\n)\n\n\ndef indicates_exit(command: WorkflowCommand) -> bool:\n    return (\n        isinstance(command, CommandCompleteRun)\n        or isinstance(command, CommandFailWorkflow)\n        or isinstance(command, CommandHalt)\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/types/internal_state.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport importlib\nfrom dataclasses import dataclass, field\nfrom typing import TYPE_CHECKING, Any\n\nfrom workflows.context.context_types import (\n    SerializedContext,\n    SerializedEventAttempt,\n    SerializedStepWorkerState,\n    SerializedWaiter,\n)\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.decorators import CatchErrorHandler, StepConfig\nfrom workflows.events import Event\nfrom workflows.retry_policy import RetryPolicy\nfrom workflows.runtime.types.results import StepWorkerState, StepWorkerWaiter\nfrom workflows.runtime.types.ticks import TickAddEvent, WorkflowTick\nfrom workflows.workflow import Workflow\n\nif TYPE_CHECKING:\n    from workflows.context.context_types import SerializedContext\n    from workflows.context.serializers import BaseSerializer\n\n\n@dataclass()\nclass BrokerState:\n    \"\"\"\n    Complete state of the workflow broker at a given point in time.\n\n    This is the primary state object passed through the control loop's reducer pattern.\n    Each tick processes this state and returns an updated copy along with commands to execute.\n\n    Attributes:\n        config: Immutable configuration for the workflow and all steps\n        workers: Mutable state for each step's worker pool, queues, and in-progress executions\n    \"\"\"\n\n    is_running: bool\n    config: BrokerConfig\n    workers: dict[str, InternalStepWorkerState]\n\n    def deepcopy(self) -> BrokerState:\n        \"\"\"\n        Deep-ish copy. Copies fields that are considered mutable during updates.\n        \"\"\"\n        return BrokerState(\n            is_running=self.is_running,\n            config=self.config,  # immutable\n            workers={\n                name: worker_state._deepcopy()\n                for name, worker_state in self.workers.items()\n            },\n        )\n\n    @staticmethod\n    def from_workflow(workflow: Workflow) -> BrokerState:\n        return BrokerState(\n            is_running=False,\n            config=BrokerConfig(\n                steps={\n                    name: InternalStepConfig(\n                        accepted_events=step_func._step_config.accepted_events,\n                        retry_policy=step_func._step_config.retry_policy,\n                        num_workers=step_func._step_config.num_workers,\n                    )\n                    for name, step_func in workflow._get_steps().items()\n                },\n                timeout=workflow._timeout,\n                catch_error_handlers=dict(workflow._catch_error_handlers),\n                handler_for_step=dict(workflow._handler_for_step),\n            ),\n            workers={\n                name: InternalStepWorkerState(\n                    queue=[],\n                    config=step_func._step_config,\n                    in_progress=[],\n                    collected_events={},\n                    collected_waiters=[],\n                )\n                for name, step_func in workflow._get_steps().items()\n            },\n        )\n\n    def rehydrate_with_ticks(self) -> list[WorkflowTick]:\n        \"\"\"\n        Rehydrates non-serializable state by re-running commands\n        \"\"\"\n        commands: list[WorkflowTick] = []\n        for step_name, worker_state in sorted(self.workers.items(), key=lambda x: x[0]):\n            for waiter in sorted(\n                worker_state.collected_waiters, key=lambda x: x.waiter_id\n            ):\n                if waiter.has_requirements and not waiter.requirements:\n                    commands.append(\n                        TickAddEvent(event=waiter.event, step_name=step_name)\n                    )\n        return commands\n\n    def to_serialized(self, serializer: BaseSerializer) -> SerializedContext:\n        \"\"\"Serialize the broker state to a SerializedContext.\"\"\"\n\n        workers_dict = {}\n        for step_name, worker_state in self.workers.items():\n            # Serialize queue with retry info\n            queue = [\n                SerializedEventAttempt(\n                    event=serializer.serialize(attempt.event),\n                    attempts=attempt.attempts or 0,\n                    first_attempt_at=attempt.first_attempt_at,\n                    last_exception=attempt.last_exception,\n                    last_failed_at=attempt.last_failed_at,\n                    recovery_counts=dict(attempt.recovery_counts),\n                )\n                for attempt in worker_state.queue\n            ]\n\n            # Serialize in-progress events (just the events, retry info tracked separately)\n            in_progress = [\n                serializer.serialize(ip.event) for ip in worker_state.in_progress\n            ]\n\n            # Serialize collected events\n            collected_events = {\n                buffer_id: [serializer.serialize(ev) for ev in events]\n                for buffer_id, events in worker_state.collected_events.items()\n            }\n\n            # Serialize waiters\n            waiters = [\n                SerializedWaiter(\n                    waiter_id=waiter.waiter_id,\n                    event=serializer.serialize(waiter.event),\n                    waiting_for_event=f\"{waiter.waiting_for_event.__module__}.{waiter.waiting_for_event.__name__}\",\n                    has_requirements=bool(len(waiter.requirements))\n                    or waiter.has_requirements,\n                    resolved_event=serializer.serialize(waiter.resolved_event)\n                    if waiter.resolved_event\n                    else None,\n                )\n                for waiter in worker_state.collected_waiters\n            ]\n\n            workers_dict[step_name] = SerializedStepWorkerState(\n                queue=queue,\n                in_progress=in_progress,\n                collected_events=collected_events,\n                collected_waiters=waiters,\n            )\n\n        return SerializedContext(\n            version=1,\n            state={},  # State is filled separately by the state store\n            is_running=self.is_running,\n            workers=workers_dict,\n        )\n\n    @staticmethod\n    def from_serialized(\n        serialized: SerializedContext,\n        workflow: Workflow,\n        serializer: BaseSerializer,\n    ) -> BrokerState:\n        \"\"\"Deserialize a SerializedContext into a BrokerState.\"\"\"\n\n        serializer = serializer or JsonSerializer()\n\n        # Start with a base state from the workflow\n        base_state = BrokerState.from_workflow(workflow)\n        # Unfortunately, important to preserve this state, since the workflow needs to know this to decide\n        # whether to create a start_event from kwargs (it only constructs and passes a start event if not already running)\n        base_state.is_running = serialized.is_running\n\n        # Restore worker state (queues, collected events, waiters)\n        # We do this regardless of is_running state so workflows can resume from where they left off\n        for step_name, worker_data in serialized.workers.items():\n            if step_name not in base_state.workers:\n                continue\n\n            worker = base_state.workers[step_name]\n\n            # Restore queue with retry info\n            worker.queue = [\n                EventAttempt(\n                    event=serializer.deserialize(attempt.event),\n                    attempts=attempt.attempts,\n                    first_attempt_at=attempt.first_attempt_at,\n                    last_exception=attempt.last_exception,\n                    last_failed_at=attempt.last_failed_at,\n                    recovery_counts=dict(attempt.recovery_counts),\n                )\n                for attempt in worker_data.queue\n            ]\n\n            # in_progress events are moved to the queue on deserialization\n            # They will be restarted when the workflow runs\n            for event_str in worker_data.in_progress:\n                worker.queue.append(\n                    EventAttempt(\n                        event=serializer.deserialize(event_str),\n                        attempts=0,\n                        first_attempt_at=None,\n                    )\n                )\n\n            # Restore collected events\n            worker.collected_events = {\n                buffer_id: [serializer.deserialize(ev) for ev in events]\n                for buffer_id, events in worker_data.collected_events.items()\n            }\n\n            # Restore waiters\n            worker.collected_waiters = []\n            for waiter_data in worker_data.collected_waiters:\n                # Import the event type\n                waiting_for_event = _import_event_type(waiter_data.waiting_for_event)\n\n                worker.collected_waiters.append(\n                    StepWorkerWaiter(\n                        waiter_id=waiter_data.waiter_id,\n                        event=serializer.deserialize(waiter_data.event),\n                        waiting_for_event=waiting_for_event,\n                        requirements={},\n                        has_requirements=waiter_data.has_requirements,\n                        resolved_event=serializer.deserialize(\n                            waiter_data.resolved_event\n                        )\n                        if waiter_data.resolved_event\n                        else None,\n                    )\n                )\n\n        return base_state\n\n\ndef _import_event_type(qualified_name: str) -> type[Event]:\n    \"\"\"Import an event type from a fully qualified name like 'mymodule.MyEvent'.\"\"\"\n    parts = qualified_name.rsplit(\".\", 1)\n    if len(parts) != 2:\n        raise ValueError(f\"Invalid qualified name: {qualified_name}\")\n\n    module_name, class_name = parts\n\n    module = importlib.import_module(module_name)\n    return getattr(module, class_name)\n\n\n@dataclass(frozen=True)\nclass BrokerConfig:\n    \"\"\"\n    configuration for a workflow run.\n\n    This contains all the static configuration that doesn't change during workflow execution.\n\n    Attributes:\n        steps: Configuration for each step indexed by step name\n        timeout: Maximum seconds before the workflow times out, or None for no timeout\n        catch_error_handlers: handler step name -> CatchErrorHandler descriptor\n        handler_for_step: step name -> handler step name that owns it\n    \"\"\"\n\n    steps: dict[str, InternalStepConfig]\n    timeout: float | None\n    catch_error_handlers: dict[str, CatchErrorHandler] = field(default_factory=dict)\n    handler_for_step: dict[str, str] = field(default_factory=dict)\n\n\n@dataclass()\nclass InternalStepConfig:\n    \"\"\"\n    Configuration for a single step in the workflow.\n\n    Attributes:\n        accepted_events: List of Event type classes this step can handle\n        retry_policy: Policy for retrying failed executions, or None for no retries\n        num_workers: Maximum number of concurrent executions of this step\n    \"\"\"\n\n    accepted_events: list[Any]\n    retry_policy: RetryPolicy | None\n    num_workers: int\n\n\n@dataclass()\nclass EventAttempt:\n    \"\"\"\n    Represents an event that is being or will be processed by a step.\n\n    Tracks retry information for events that have failed and are being retried.\n\n    Attributes:\n        event: The event to process\n        attempts: Number of times this event has been attempted (0 for first attempt), or None if not yet attempted\n        first_attempt_at: Unix timestamp of first attempt, or None if not yet attempted\n        last_exception: Most recent exception, if this attempt is a retry.\n        last_failed_at: Unix timestamp of the most recent failure, or None.\n    \"\"\"\n\n    event: Event\n    attempts: int | None = None\n    first_attempt_at: float | None = None\n    last_exception: Exception | None = None\n    last_failed_at: float | None = None\n    recovery_counts: dict[str, int] = field(default_factory=dict)\n\n\n@dataclass()\nclass InternalStepWorkerState:\n    \"\"\"\n    Runtime state for a single step's worker pool.\n\n    This manages the queue of pending events, currently executing workers, and any\n    state needed for ctx.collect_events() and ctx.wait_for_event() operations.\n\n    Attributes:\n        queue: Events waiting to be processed by this step\n        config: Step configuration (includes retry policy, num_workers, etc.)\n        in_progress: Currently executing workers for this step\n        collected_events: Events being collected via ctx.collect_events(), keyed by buffer_id\n        collected_waiters: Active waiters created by ctx.wait_for_event()\n    \"\"\"\n\n    queue: list[EventAttempt]\n    config: StepConfig\n    in_progress: list[InProgressState]\n    collected_events: dict[str, list[Event]]\n    collected_waiters: list[StepWorkerWaiter]\n\n    def _deepcopy(self) -> InternalStepWorkerState:\n        return InternalStepWorkerState(\n            queue=[dataclasses.replace(x) for x in self.queue],\n            config=self.config,\n            in_progress=[x._deepcopy() for x in self.in_progress],\n            collected_events={k: list(v) for k, v in self.collected_events.items()},\n            collected_waiters=[dataclasses.replace(x) for x in self.collected_waiters],\n        )\n\n\n@dataclass()\nclass InProgressState:\n    \"\"\"\n    Represents a single worker execution that is currently in progress.\n\n    Each worker gets a snapshot of the step's shared state at the time it starts.\n    This enables optimistic execution - if the shared state changes during execution\n    (e.g., new collected events arrive), the control loop can detect this and retry\n    the worker with the updated state.\n\n    Attributes:\n        event: The event being processed by this worker\n        worker_id: Numeric ID (0 to num_workers-1) identifying this worker slot\n        shared_state: Snapshot of collected_events and collected_waiters at worker start time\n        attempts: Number of times this event has been attempted (including current attempt)\n        first_attempt_at: Unix timestamp when this event was first attempted\n        last_exception: Most recent exception from the prior attempt, or None if this is the first attempt.\n        last_failed_at: Unix timestamp of the most recent failure, or None.\n    \"\"\"\n\n    event: Event\n    worker_id: int\n    shared_state: StepWorkerState\n    attempts: int\n    first_attempt_at: float\n    last_exception: Exception | None = None\n    last_failed_at: float | None = None\n    recovery_counts: dict[str, int] = field(default_factory=dict)\n\n    def _deepcopy(self) -> InProgressState:\n        return InProgressState(\n            event=self.event,\n            worker_id=self.worker_id,\n            shared_state=self.shared_state._deepcopy(),\n            attempts=self.attempts,\n            first_attempt_at=self.first_attempt_at,\n            last_exception=self.last_exception,\n            last_failed_at=self.last_failed_at,\n            recovery_counts=dict(self.recovery_counts),\n        )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/types/named_task.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"NamedTask associates asyncio tasks with stable string keys for journaling.\"\"\"\n\nfrom __future__ import annotations\n\nfrom asyncio import Task\nfrom collections.abc import Sequence\nfrom dataclasses import dataclass\nfrom typing import Any, Coroutine\n\n# Key prefix for pull tasks\nPULL_PREFIX = \"__pull__\"\n\n\n@dataclass\nclass WorkerTask:\n    \"\"\"An asyncio worker task with structured identity.\"\"\"\n\n    step_name: str\n    worker_id: int\n    task: Task[Any]\n\n    @property\n    def key(self) -> str:\n        return f\"{self.step_name}:{self.worker_id}\"\n\n\n@dataclass\nclass PullTask:\n    \"\"\"An asyncio pull task with sequence identity.\"\"\"\n\n    sequence: int\n    task: Task[Any]\n\n    @property\n    def key(self) -> str:\n        return f\"{PULL_PREFIX}:{self.sequence}\"\n\n\nNamedTask = WorkerTask | PullTask\n\n\ndef all_tasks(named_tasks: Sequence[NamedTask]) -> set[Task[Any]]:\n    \"\"\"Extract all tasks for use with asyncio.wait.\"\"\"\n    return {nt.task for nt in named_tasks}\n\n\ndef find_by_key(named_tasks: Sequence[NamedTask], key: str) -> Task[Any] | None:\n    \"\"\"Find a task by its key, returns None if not found.\"\"\"\n    for nt in named_tasks:\n        if nt.key == key:\n            return nt.task\n    return None\n\n\ndef get_key(named_tasks: Sequence[NamedTask], task: Task[Any]) -> str:\n    \"\"\"Get the key for a task. Raises KeyError if not found.\"\"\"\n    for nt in named_tasks:\n        if nt.task is task:\n            return nt.key\n    raise KeyError(f\"Task {task} not found\")\n\n\ndef pick_highest_priority(\n    named_tasks: Sequence[NamedTask], done: set[Task[Any]]\n) -> Task[Any] | None:\n    \"\"\"Return highest priority completed task from done set.\n\n    Priority is determined by list order - tasks earlier in the list\n    have higher priority. Workers should be listed before pull.\n\n    Returns None if done is empty.\n    Raises ValueError if done is non-empty but no tasks match (indicates bug).\n    \"\"\"\n    if not done:\n        return None\n    for nt in named_tasks:\n        if nt.task in done:\n            return nt.task\n    raise ValueError(\n        f\"No tasks in done set match named_tasks. \"\n        f\"done={done}, named_tasks={[nt.key for nt in named_tasks]}\"\n    )\n\n\n@dataclass\nclass PendingWorker:\n    \"\"\"A worker coroutine that hasn't been started yet.\"\"\"\n\n    step_name: str\n    worker_id: int\n    coro: Coroutine[Any, Any, Any]\n\n    @property\n    def key(self) -> str:\n        return f\"{self.step_name}:{self.worker_id}\"\n\n    def start(self, task: Task[Any]) -> WorkerTask:\n        \"\"\"Convert to a started WorkerTask.\"\"\"\n        return WorkerTask(self.step_name, self.worker_id, task)\n\n\n@dataclass\nclass PendingPull:\n    \"\"\"A pull coroutine that hasn't been started yet.\"\"\"\n\n    sequence: int\n    coro: Coroutine[Any, Any, Any]\n\n    @property\n    def key(self) -> str:\n        return f\"{PULL_PREFIX}:{self.sequence}\"\n\n    def start(self, task: Task[Any]) -> PullTask:\n        \"\"\"Convert to a started PullTask.\"\"\"\n        return PullTask(self.sequence, task)\n\n\nPendingStart = PendingWorker | PendingPull\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/types/plugin.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nA runtime interface to switch out a broker runtime (external library or service that manages durable/distributed step execution).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport weakref\nfrom abc import ABC, abstractmethod\nfrom contextlib import contextmanager\nfrom contextvars import ContextVar, Token\nfrom dataclasses import dataclass\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    AsyncGenerator,\n    Coroutine,\n    Generator,\n    Literal,\n    Protocol,\n)\n\nfrom workflows.context.state_store import StateStore\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.runtime.types.named_task import (\n    NamedTask,\n    PendingStart,\n    all_tasks,\n    pick_highest_priority,\n)\n\nif TYPE_CHECKING:\n    from workflows.context.context import Context\n    from workflows.context.serializers import BaseSerializer\n    from workflows.runtime.types.internal_state import BrokerState\n    from workflows.runtime.types.step_function import StepWorkerFunction\n    from workflows.workflow import Workflow\nfrom workflows.runtime.types.ticks import TickCancelRun, WorkflowTick\n\n# Context variable for implicit runtime scoping\n_current_runtime: ContextVar[Runtime | None] = ContextVar(\n    \"current_runtime\", default=None\n)\n\n\n@dataclass\nclass WaitResultTick:\n    \"\"\"Result containing a received tick.\"\"\"\n\n    tick: WorkflowTick\n    type: Literal[\"tick\"] = \"tick\"\n\n\n@dataclass\nclass WaitResultTimeout:\n    \"\"\"Result indicating timeout expiration.\"\"\"\n\n    type: Literal[\"timeout\"] = \"timeout\"\n\n\nWaitResult = WaitResultTick | WaitResultTimeout\n\n\n@dataclass\nclass WaitForNextTaskResult:\n    \"\"\"Result from wait_for_next_task containing the completed task and newly started tasks.\"\"\"\n\n    completed: asyncio.Task[Any] | None\n    started: list[NamedTask]\n\n\n@dataclass\nclass RegisteredWorkflow:\n    workflow: Workflow\n    workflow_run_fn: WorkflowRunFunction\n    steps: dict[str, StepWorkerFunction]\n\n\nclass InternalRunAdapter(ABC):\n    \"\"\"\n    Adapter interface for use INSIDE a workflow's control loop.\n\n    This adapter is used by the workflow execution engine (broker) to receive\n    ticks from external sources, publish events to listeners, manage timing,\n    and perform durable sleeps.\n\n    The InternalRunAdapter is created by Runtime.new_internal_adapter() for each\n    workflow run and is passed to the control loop function. It provides the\n    internal-facing side of workflow communication:\n    - Receiving ticks from the external mailbox (wait_receive)\n    - Publishing events that external code can stream (write_to_event_stream)\n    - Getting current time with durability support (get_now)\n    - Sleeping with durability support (sleep)\n\n    The run_id is always available and required at construction time.\n    \"\"\"\n\n    @property\n    @abstractmethod\n    def run_id(self) -> str:\n        \"\"\"\n        The unique identifier for this workflow run.\n\n        Always available - required at adapter construction time.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def write_to_event_stream(self, event: Event) -> None:\n        \"\"\"\n        Publish an event to external listeners.\n\n        Called from inside the workflow to emit events that can be observed\n        by external code via the ExternalRunAdapter's stream_published_events().\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def get_now(self) -> float:\n        \"\"\"\n        Get the current time in seconds since epoch.\n\n        Called from within the workflow control loop. For durable workflows,\n        this should return a memoized/replayed value to ensure deterministic\n        replay behavior.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def send_event(self, tick: WorkflowTick) -> None:\n        \"\"\"\n        Send a tick into the workflow's own mailbox from within the control loop.\n\n        Called from inside the workflow (e.g., from step functions via ctx.send_event)\n        to inject events back into the workflow's execution. The tick will be\n        received by wait_receive() on the next iteration.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def wait_receive(\n        self,\n        timeout_seconds: float | None = None,\n    ) -> WaitResult:\n        \"\"\"\n        Wait for next tick OR timeout expiration.\n\n        This is the primary method for the control loop to wait for events.\n        It combines receiving ticks and timeout handling into a single\n        deterministic operation.\n\n        Args:\n            timeout_seconds: Max time to wait. None means wait indefinitely.\n\n        Returns:\n            WaitResultTick if a tick was received\n            WaitResultTimeout if timeout expired before receiving tick\n\n        This is a DURABLE operation for durable runtimes:\n        - On replay, already-elapsed time is accounted for\n        - If timeout already expired in previous run, returns immediately\n        \"\"\"\n        ...\n\n    async def close(self) -> None:\n        \"\"\"\n        Release resources for a completed/failed workflow.\n\n        Called by the control loop's cleanup_tasks() when the workflow is\n        finishing (completion, failure, halt, or unwind). Implementations\n        should wake any blocked wait_receive() calls so the control loop\n        can exit.\n\n        WARNING: This may be destructive — e.g. sending a durable message\n        that prevents the workflow from resuming later. Only called when\n        the workflow's outcome is already determined.\n\n        Default is no-op.\n        \"\"\"\n        pass\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        \"\"\"\n        Get the state store for this workflow run.\n\n        Returns the state store from the runtime, or None if not initialized.\n        Default implementation returns None.\n        \"\"\"\n        return None\n\n    async def finalize_step(self) -> None:\n        \"\"\"\n        Called after a step function completes to perform any adapter-specific cleanup.\n\n        This is called after all background tasks spawned during the step have completed.\n        Adapters can override to perform additional finalization (e.g., flush buffers,\n        sync state). Default is no-op.\n        \"\"\"\n        pass\n\n    def is_replaying(self) -> bool:\n        \"\"\"Whether the adapter is currently replaying recorded operations.\n\n        During replay, side effects like persisting events to external stores\n        should be skipped to avoid duplicates. Default is False (live execution).\n        \"\"\"\n        return False\n\n    async def on_tick(self, tick: WorkflowTick) -> None:\n        \"\"\"\n        Called whenever a tick event is processed by the control loop.\n\n        This method is invoked for both external ticks (sent via send_event)\n        and internal ticks (generated by step completions, timeouts, etc.).\n        Adapters can override to record ticks, persist them, etc.\n        Default is no-op.\n        \"\"\"\n        pass\n\n    async def after_tick(self, tick: WorkflowTick) -> None:\n        \"\"\"Called after a tick's commands have been processed.\n\n        Fires for non-terminal ticks only — terminal commands (CompleteRun,\n        Halt, FailWorkflow) return/raise before this hook runs. Terminal\n        cleanup is handled separately via the store's event persistence path.\n        \"\"\"\n        pass\n\n    async def wait_for_next_task(\n        self,\n        running: list[NamedTask],\n        pending: list[PendingStart],\n        timeout: float | None = None,\n    ) -> WaitForNextTaskResult:\n        \"\"\"Wait for and return the next task that should complete.\n\n        The adapter is responsible for starting pending coroutines as asyncio tasks.\n        This allows adapters to control task startup ordering (e.g., for deterministic\n        function_id acquisition in DBOS).\n\n        Args:\n            running: Already-started tasks from previous iterations.\n            pending: Coroutines to start this iteration.\n            timeout: Timeout in seconds, None for no timeout.\n\n        Returns:\n            WaitForNextTaskResult with the completed task and newly started NamedTasks.\n\n        IMPORTANT: Must return at most ONE completed task per call.\n        \"\"\"\n        started = [p.start(asyncio.create_task(p.coro)) for p in pending]\n        all_named = running + started\n        tasks = all_tasks(all_named)\n        if not tasks:\n            return WaitForNextTaskResult(None, started)\n        done, _ = await asyncio.wait(\n            tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED\n        )\n        completed = pick_highest_priority(all_named, done) if done else None\n        return WaitForNextTaskResult(completed, started)\n\n\nclass ExternalRunAdapter(ABC):\n    \"\"\"\n    Adapter interface for use OUTSIDE a workflow's control loop.\n\n    This adapter is used by external code (e.g., HTTP handlers, client code)\n    to interact with a running workflow - sending events into the workflow\n    and streaming events published by the workflow.\n\n    The ExternalRunAdapter is created by Runtime.new_external_adapter() and\n    provides the external-facing side of workflow communication:\n    - Sending ticks into the workflow mailbox (send_event)\n    - Streaming events published by the workflow (stream_published_events)\n    - Cleaning up resources when done (close)\n\n    The run_id is always available and matches the internal adapter's run_id.\n    \"\"\"\n\n    @property\n    @abstractmethod\n    def run_id(self) -> str:\n        \"\"\"\n        The unique identifier for this workflow run.\n\n        Always available - matches the InternalRunAdapter's run_id.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    async def send_event(self, tick: WorkflowTick) -> None:\n        \"\"\"\n        Send a tick into the workflow mailbox.\n\n        Called from outside the workflow to inject events into the workflow's\n        execution. The tick will be received by the internal adapter's\n        wait_receive() method.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        \"\"\"\n        Stream events published by the workflow.\n\n        Called from outside the workflow to observe events emitted by the\n        workflow via the internal adapter's write_to_event_stream().\n        Returns an async generator that yields events as they are published.\n        \"\"\"\n        ...\n\n    async def close(self) -> None:\n        \"\"\"\n        Clean up adapter resources.\n\n        Called when done interacting with the workflow to release any\n        resources held by this adapter (e.g., close streams, release locks).\n        \"\"\"\n\n        pass\n\n    @abstractmethod\n    async def get_result(self) -> StopEvent:\n        \"\"\"\n        Get the result of the workflow run, if completed. Will raise if the workflow failed or was cancelled\n        \"\"\"\n        ...\n\n    async def cancel(self) -> None:\n        \"\"\"\n        Cancel the workflow run if it is still running.\n        \"\"\"\n        await self.send_event(TickCancelRun())\n\n    def get_state_store(self) -> StateStore[Any] | None:\n        \"\"\"\n        Get the state store for this workflow run.\n\n        Returns the state store if this adapter owns it, or None if state\n        is managed externally. Default implementation returns None.\n        \"\"\"\n        return None\n\n\n@dataclass\nclass RunContext:\n    \"\"\"Payload handed from `create_workflow_run_function` to the control loop\n    across the narrow `ControlLoopFunction` boundary.\n    \"\"\"\n\n    workflow: Workflow\n    run_adapter: InternalRunAdapter\n    context: Context\n    steps: dict[str, StepWorkerFunction]\n\n\n@dataclass\nclass RunContextContainer:\n    \"\"\"Mutable one-shot holder for a `RunContext`.\n\n    The control loop calls `consume()` at entry, which drops the container's\n    reference to the payload. Because `asyncio` snapshots the current `Context`\n    when scheduling handles (e.g. `loop.call_later` for periodic timers like\n    aiohttp's `TCPConnector._cleanup_closed`), any such snapshot would otherwise\n    keep the workflow object graph alive via this container until the timer\n    fires. Clearing the single shared instance breaks that reference chain.\n    \"\"\"\n\n    payload: RunContext | None\n\n    def consume(self) -> RunContext:\n        payload, self.payload = self.payload, None\n        if payload is None:\n            raise RuntimeError(\"RunContext has already been consumed\")\n        return payload\n\n\n_current_run: ContextVar[RunContextContainer | None] = ContextVar(\n    \"current_run\", default=None\n)\n\n\n@contextmanager\ndef run_context(ctx: RunContext) -> Generator[RunContextContainer, None, None]:\n    \"\"\"Set the current run context for the duration of a workflow run.\"\"\"\n    container = RunContextContainer(payload=ctx)\n    token = _current_run.set(container)\n    try:\n        yield container\n    finally:\n        _current_run.reset(token)\n\n\ndef consume_current_run() -> RunContext:\n    \"\"\"Consume the current `RunContext` payload exactly once.\n\n    Drops the container's strong reference to the payload so that any `Context`\n    snapshot taken by `asyncio` (e.g. via `loop.call_later`) cannot pin the\n    workflow graph through it.\n    \"\"\"\n    container = _current_run.get()\n    if container is None:\n        raise RuntimeError(\"Not in a workflow run context\")\n    return container.consume()\n\n\nclass WorkflowSet:\n    \"\"\"Identity-based weak set for tracking Workflow instances.\n\n    Uses id() as the key and weakref.ref with cleanup callbacks to\n    avoid hashability requirements and memory leaks.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._refs: dict[int, weakref.ref[Workflow]] = {}\n\n    def add(self, workflow: Workflow) -> None:\n        obj_id = id(workflow)\n        if obj_id in self._refs:\n            return\n\n        def _cleanup(ref: weakref.ref[Workflow], _id: int = obj_id) -> None:\n            self._refs.pop(_id, None)\n\n        self._refs[obj_id] = weakref.ref(workflow, _cleanup)\n\n    def discard(self, workflow: Workflow) -> None:\n        self._refs.pop(id(workflow), None)\n\n    def __contains__(self, workflow: Workflow) -> bool:\n        ref = self._refs.get(id(workflow))\n        if ref is None:\n            return False\n        return ref() is not None\n\n    def __iter__(self) -> Generator[Workflow, None, None]:\n        for ref in list(self._refs.values()):\n            obj = ref()\n            if obj is not None:\n                yield obj\n\n    def __len__(self) -> int:\n        return sum(1 for _ in self)\n\n    def __bool__(self) -> bool:\n        return any(ref() is not None for ref in self._refs.values())\n\n\nclass Runtime(ABC):\n    \"\"\"\n    Abstract base class for workflow execution runtimes.\n\n    Runtimes control how workflows are registered, launched, and executed.\n    The default BasicRuntime uses asyncio; Other's plug into their own durability and distributed execution models.\n\n    Lifecycle:\n    1. Create runtime instance\n    2. Create workflow instances (auto-register with runtime via registering())\n    3. Call launch() to start workers/register with backend\n    4. Run workflows\n    5. Call destroy() to clean up\n\n    Use registering() context manager for implicit workflow registration.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._pending: WorkflowSet = WorkflowSet()\n        self._launched: bool = False\n\n    @property\n    def is_launched(self) -> bool:\n        return self._launched\n\n    _token: Token[Runtime | None]\n\n    def get_or_register(self, workflow: Workflow) -> RegisteredWorkflow:\n        \"\"\"Get the registered workflow if available, otherwise register it.\"\"\"\n        registered = self.get_registered(workflow)\n        if registered is None:\n            registered = self.register(workflow)\n        return registered\n\n    @abstractmethod\n    def register(self, workflow: Workflow) -> RegisteredWorkflow:\n        \"\"\"\n        Register a workflow with the runtime.\n\n        Called at launch() time for each tracked workflow. Runtimes can\n        wrap the control_loop and steps to fit in their registration/decoration model.\n\n        Returns RegisteredWorkflow with wrapped functions\n        \"\"\"\n        ...\n\n    @abstractmethod\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: BaseSerializer | None = None,\n    ) -> ExternalRunAdapter:\n        \"\"\"\n        Launch a workflow run.\n\n        The runtime creates and owns the state store based on serialized_state.\n        Returns the external adapter for the workflow run.\n\n        Args:\n            run_id: Unique identifier for this workflow run.\n            registered: The registered workflow to run.\n            init_state: Initial broker state (queues, workers, etc).\n            start_event: Optional start event to begin the workflow.\n            serialized_state: Serialized state store data to restore from.\n            serializer: Serializer to use for deserializing state.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        \"\"\"\n        Get the internal adapter for a workflow run.\n\n        Called on each workflow.run() to instantiate an interface for the workflow run internals to communicate with the runtime.\n\n        Args:\n            workflow: The workflow instance being run. Used by runtimes to access workflow metadata (e.g., state type).\n        \"\"\"\n        ...\n\n    @abstractmethod\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        \"\"\"\n        Get the external adapter for a workflow run.\n\n        Called after launching a workflow run, or when getting a handle for an existing workflow run.\n        Used to send events into the workflow and stream published events.\n\n        The run_id must match the internal adapter's run_id for the same run.\n        The external adapter is used by client code interacting with the workflow.\n        \"\"\"\n        ...\n\n    async def launch(self) -> None:\n        \"\"\"\n        Launch the runtime and register all tracked workflows.\n\n        For many runtime's, this must be called before running workflows.\n        \"\"\"\n        self._launched = True\n        for wf in self._pending:\n            self.register(wf)\n            wf._runtime_locked = True\n        self._pending = WorkflowSet()\n\n    async def destroy(self) -> None:\n        \"\"\"\n        Clean up runtime resources.\n\n        Called when done with the runtime. Stops workers, closes connections.\n        \"\"\"\n        self._launched = False\n\n    def launch_sync(self) -> None:\n        \"\"\"Synchronous convenience wrapper for :meth:`launch`.\"\"\"\n        asyncio.run(self.launch())\n\n    def destroy_sync(self) -> None:\n        \"\"\"Synchronous convenience wrapper for :meth:`destroy`.\"\"\"\n        asyncio.run(self.destroy())\n\n    def track_workflow(self, workflow: Workflow) -> None:\n        \"\"\"\n        Track a workflow instance for registration at launch time.\n\n        Called by Workflow.__init__ to register with the runtime.\n        \"\"\"\n        self._pending.add(workflow)\n\n    def untrack_workflow(self, workflow: Workflow) -> None:\n        \"\"\"Remove a workflow from this runtime's tracking set.\"\"\"\n        self._pending.discard(workflow)\n\n    def get_registered(self, workflow: Workflow) -> RegisteredWorkflow | None:\n        \"\"\"\n        Get the registered workflow if available.\n\n        Returns the pre-registered workflow from launch(), or None if not tracked.\n        \"\"\"\n        return None\n\n    @contextmanager\n    def registering(self) -> Generator[Runtime, None, None]:\n        \"\"\"\n        Context manager for implicit workflow registration.\n\n        Workflows created inside this block will automatically set this runtime as their runtime.\n        \"\"\"\n        token = _current_runtime.set(self)\n        try:\n            yield self\n        finally:\n            _current_runtime.reset(token)\n\n\nclass SnapshottableAdapter(ABC):\n    \"\"\"\n    Mixin interface that adds snapshot/replay capabilities to adapters.\n\n    This is a standalone mixin (not inheriting from InternalRunAdapter or\n    ExternalRunAdapter) that can be combined with adapter implementations\n    to add init_state and replay capabilities for state reconstruction.\n\n    Use `as_snapshottable_adapter()` to check if an adapter supports snapshotting.\n    \"\"\"\n\n    @property\n    @abstractmethod\n    def init_state(self) -> BrokerState:\n        \"\"\"\n        Get the initial state of the adapter.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    def replay(self) -> list[WorkflowTick]:\n        \"\"\"\n        Return the recorded ticks for replay.\n\n        Returns all ticks that were recorded via on_tick(), in the order\n        they were received. Used for debugging and workflow replay.\n        \"\"\"\n        ...\n\n\ndef as_snapshottable_adapter(\n    adapter: ExternalRunAdapter | InternalRunAdapter,\n) -> SnapshottableAdapter | None:\n    \"\"\"\n    Check if an internal adapter supports snapshotting.\n\n    Returns the adapter cast to SnapshottableAdapter if it implements\n    the snapshotting interface, or None otherwise.\n    \"\"\"\n    if isinstance(adapter, SnapshottableAdapter):\n        return adapter\n    return None\n\n\nclass V2RuntimeCompatibilityShim(ABC):\n    \"\"\"\n    This interface will be deleted in V3. Temporary shim to support deprecated v2 functionality\n    \"\"\"\n\n    @abstractmethod\n    def get_result_or_none(self) -> StopEvent | None:\n        \"\"\"\n        Get the result of the workflow run, if completed. Will raise if the workflow failed or was cancelled, otherwise return None if still running\n        \"\"\"\n        ...\n\n    @property\n    @abstractmethod\n    def is_running(self) -> bool:\n        \"\"\"\n        Check if the workflow run is still running.\n        \"\"\"\n        ...\n\n    @abstractmethod\n    def abort(self) -> None:\n        \"\"\"\n        Forcefully abort the workflow execution (ungraceful hard cancel).\n\n        This immediately terminates execution by cancelling the underlying task.\n        Unlike cancel() which sends a graceful cancellation signal:\n        - In-flight step work is cancelled immediately\n        - No WorkflowCancelledEvent is emitted\n        - The workflow does not finalize gracefully\n\n        This is deprecated v2 behavior - prefer cancel_run() for graceful cancellation.\n        \"\"\"\n        ...\n\n\ndef as_v2_runtime_compatibility_shim(\n    adapter: ExternalRunAdapter,\n) -> V2RuntimeCompatibilityShim | None:\n    \"\"\"\n    Check if an adapter supports the V2 runtime compatibility shim.\n    \"\"\"\n    if isinstance(adapter, V2RuntimeCompatibilityShim):\n        return adapter\n    return None\n\n\nclass ControlLoopFunction(Protocol):\n    \"\"\"\n    Protocol for a function that starts and runs the internal control loop for a workflow run.\n    Runtime decorators to the control loop function must maintain this signature.\n    \"\"\"\n\n    def __call__(\n        self,\n        start_event: Event | None,\n        init_state: BrokerState | None,\n        run_id: str,\n    ) -> Coroutine[None, None, StopEvent]: ...\n\n\nclass WorkflowRunFunction(Protocol):\n    \"\"\"\n    Protocol for a function that runs a workflow. Wraps a control loop function with glue to the runtime.\n    \"\"\"\n\n    def __call__(\n        self,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        tags: dict[str, Any] | None = None,\n    ) -> Coroutine[None, None, StopEvent]: ...\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/types/results.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport weakref\nfrom contextvars import ContextVar\nfrom dataclasses import dataclass\nfrom typing import (\n    Any,\n    Generic,\n    Literal,\n    TypeVar,\n)\n\nfrom pydantic import BaseModel, ConfigDict, model_serializer, model_validator\nfrom workflows.events import (\n    Event,\n    SerializableEvent,\n    SerializableEventType,\n    SerializableException,\n    SerializableOptionalEvent,\n)\n\nEventType = TypeVar(\"EventType\", bound=Event)\n\n#################################################################\n# State Passed to step functions and returned by step functions #\n#################################################################\n\n\n@dataclass(frozen=True)\nclass RetryAttempt:\n    \"\"\"Per-invocation state handed to a step worker for the currently-processed event.\n\n    Bundles the counters the runtime needs to surface via ``Context.retry_info()``\n    and to reconstruct :class:`workflows.retry_policy.RetryInfo`. ``retry_number``\n    is 0-based (0 = first run, 1 = first retry). ``last_exception`` /\n    ``last_failed_at`` are ``None`` on the first attempt. ``recovery_counts``\n    carries the per-``@catch_error``-handler invocation counts on the running\n    event's lineage so ``ctx.send_event`` can tag emitted events and nested\n    failures route to the same handlers.\n    \"\"\"\n\n    retry_number: int = 0\n    first_attempt_at: float = 0.0\n    last_exception: Exception | None = None\n    last_failed_at: float | None = None\n    recovery_counts: dict[str, int] = dataclasses.field(default_factory=dict)\n\n\n@dataclass(frozen=True)\nclass StepWorkerContext:\n    \"\"\"\n    Base state passed to step functions and returned by step functions.\n    \"\"\"\n\n    # immutable state of the step events at start of the step function execution\n    state: StepWorkerState\n    # add commands here to mutate the internal worker state after step execution\n    returns: Returns\n    retry: RetryAttempt = dataclasses.field(default_factory=RetryAttempt)\n\n\n@dataclass(frozen=True)\nclass StepWorkerState:\n    \"\"\"\n    State passed to step functions and returned by step functions.\n    \"\"\"\n\n    step_name: str\n    collected_events: dict[str, list[Event]]\n    collected_waiters: list[StepWorkerWaiter]\n\n    def _deepcopy(self) -> StepWorkerState:\n        return StepWorkerState(\n            step_name=self.step_name,\n            collected_events={k: list(v) for k, v in self.collected_events.items()},\n            collected_waiters=[dataclasses.replace(x) for x in self.collected_waiters],\n        )\n\n\n@dataclass()\nclass StepWorkerWaiter(Generic[EventType]):\n    \"\"\"\n    Any current waiters for events that are or are not resolved. Upon resolution, step should provide a delete waiter command.\n    \"\"\"\n\n    # the waiter id\n    waiter_id: str\n    # original event to replay once the condition is met\n    event: Event\n    # the type of event that is being waited for\n    waiting_for_event: type[EventType]\n    # the requirements for the waiting event to consider it met\n    requirements: dict[str, Any]\n    # requirements are not required to be serializable. Flag used during deserialization to re-ping the step function for the requirements\n    has_requirements: bool\n    # set to true when the waiting event has been resolved, such that the step can retrieve it\n    resolved_event: EventType | None\n    # set to true when the waiter has timed out, such that the step raises asyncio.TimeoutError\n    timed_out: bool = False\n\n\n@dataclass()\nclass Returns:\n    \"\"\"\n    Mutate to add return values to the step function. These are only executed after the\n    step function has completed (including errors!)\n    \"\"\"\n\n    return_values: list[StepFunctionResult]\n\n\nclass WaitingForEvent(Exception, Generic[EventType]):\n    \"\"\"\n    Raised when a step function is called, waiting for an event, but the event is not yet available.\n    Handled by the step worker to instead add a waiter rather than failing. Step is retried with the original event\n    once the waiting event is available.\n    \"\"\"\n\n    def __init__(self, add: AddWaiter[EventType]):\n        self.add = add\n        super().__init__(f\"Waiting for event {add.event_type}\")\n\n    add: AddWaiter[EventType]\n\n\nStepWorkerStateContextVar = ContextVar[StepWorkerContext](\"step_worker\")\n\n# Holds a weakref to the Context (in internal-face state) for the currently\n# executing step.  A weakref is used so that asyncio timer-handle context\n# snapshots do not pin the Workflow in memory (see RunContextContainer for\n# the analogous fix at the run level).  The strong reference lives as a local\n# variable in as_step_worker_function(); the weakref here is only a lookup handle.\nInternalContextVar: ContextVar[weakref.ref[Any]] = ContextVar(\"internal_context\")\n\n\n###################################\n# Data returned by step functions #\n###################################\n\n\nclass StepWorkerResult(BaseModel):\n    \"\"\"Returned after a step function has been successfully executed.\"\"\"\n\n    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)\n    type: Literal[\"result\"] = \"result\"\n    result: SerializableOptionalEvent = None\n\n\nclass StepWorkerFailed(BaseModel):\n    \"\"\"Returned after a step function has failed.\"\"\"\n\n    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)\n    type: Literal[\"failed\"] = \"failed\"\n    exception: SerializableException\n    failed_at: float\n\n\nclass DeleteWaiter(BaseModel):\n    \"\"\"Returned after a waiter condition has been successfully resolved.\"\"\"\n\n    model_config = ConfigDict(frozen=True)\n    type: Literal[\"delete_waiter\"] = \"delete_waiter\"\n    waiter_id: str\n\n\nclass DeleteCollectedEvent(BaseModel):\n    \"\"\"Returned after a collected event has been successfully resolved.\"\"\"\n\n    model_config = ConfigDict(frozen=True)\n    type: Literal[\"delete_collected\"] = \"delete_collected\"\n    event_id: str\n\n\nclass AddCollectedEvent(BaseModel):\n    \"\"\"Returned after a collected event has been added, and is not yet resolved.\"\"\"\n\n    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)\n    type: Literal[\"add_collected\"] = \"add_collected\"\n    event_id: str\n    event: SerializableEvent\n\n\nclass AddWaiter(BaseModel, Generic[EventType]):\n    \"\"\"Returned after a waiter has been added, and is not yet resolved.\"\"\"\n\n    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)\n    type: Literal[\"add_waiter\"] = \"add_waiter\"\n    waiter_id: str\n    waiter_event: SerializableOptionalEvent = None\n    requirements: dict[str, Any] = {}\n    timeout: float | None = None\n    event_type: SerializableEventType\n    has_requirements: bool = False\n\n    @model_serializer(mode=\"wrap\")\n    def _serialize(self, handler: Any) -> dict[str, Any]:\n        data = handler(self)\n        # Always serialize requirements as {} and record whether they existed\n        data[\"has_requirements\"] = bool(self.requirements)\n        data[\"requirements\"] = {}\n        return data\n\n    @model_validator(mode=\"wrap\")  # type: ignore[ty:invalid-argument-type]\n    @classmethod\n    def _validate(cls, data: Any, handler: Any) -> AddWaiter:\n        if isinstance(data, dict):\n            # Strip has_requirements before validation (it's computed)\n            data = dict(data)\n            data.pop(\"has_requirements\", None)\n        return handler(data)\n\n\n# A step function result \"command\" communicates back to the workflow how the step function was resolved\n# e.g. are we collecting events, waiting for an event, or just returning a result?\nStepFunctionResult = (\n    StepWorkerResult\n    | StepWorkerFailed\n    | AddCollectedEvent\n    | DeleteCollectedEvent\n    | AddWaiter[Event]\n    | DeleteWaiter\n)\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/types/step_function.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport inspect\nimport time\nimport uuid\nimport weakref\nfrom contextvars import copy_context\nfrom typing import TYPE_CHECKING, Any, Awaitable, Callable, Protocol, TypeVar\n\nfrom llama_index_instrumentation import get_dispatcher\nfrom llama_index_instrumentation.base import BaseEvent\nfrom llama_index_instrumentation.dispatcher import (\n    active_instrument_tags,\n    instrument_tags,\n)\nfrom llama_index_instrumentation.events.span import SpanDropEvent\nfrom llama_index_instrumentation.span import active_span_id\nfrom workflows._event_summary import summarize_event\nfrom workflows.decorators import P, StepConfig\nfrom workflows.errors import WorkflowCancelledByUser, WorkflowRuntimeError\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.runtime.control_loop import control_loop\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import (\n    ControlLoopFunction,\n    RunContext,\n    WorkflowRunFunction,\n    run_context,\n)\nfrom workflows.runtime.types.results import (\n    InternalContextVar,\n    RetryAttempt,\n    Returns,\n    StepFunctionResult,\n    StepWorkerContext,\n    StepWorkerFailed,\n    StepWorkerResult,\n    StepWorkerState,\n    StepWorkerStateContextVar,\n    WaitingForEvent,\n)\nfrom workflows.workflow import Workflow\n\nif TYPE_CHECKING:\n    from workflows.context.context import Context\n\n_dispatcher = get_dispatcher(__name__)\n\nStepReturnT = TypeVar(\"StepReturnT\", bound=Event | None)\n\n\nclass SpanCancelledEvent(BaseEvent):\n    \"\"\"Instrumentation event emitted when a span exits due to cancellation.\"\"\"\n\n    reason: str\n\n    @classmethod\n    def class_name(cls) -> str:\n        return \"SpanCancelledEvent\"\n\n\nclass WorkflowStepOutputEvent(BaseEvent):\n    \"\"\"Instrumentation event emitted with output summary when a step returns.\"\"\"\n\n    output: str\n\n    @classmethod\n    def class_name(cls) -> str:\n        return \"step.output\"\n\n\nclass WorkflowRunOutputEvent(BaseEvent):\n    \"\"\"Instrumentation event emitted with output summary when a workflow run completes.\"\"\"\n\n    output: str\n\n    @classmethod\n    def class_name(cls) -> str:\n        return \"workflow.output\"\n\n\ndef _emit_output_event(event: BaseEvent) -> None:\n    \"\"\"Fire an instrumentation event, silently ignoring failures.\"\"\"\n    try:\n        _dispatcher.event(event)\n    except Exception:\n        pass\n\n\ndef _run_with_tags(tags: dict[str, Any], func: Callable[[], Any]) -> Any:\n    \"\"\"Run a callable inside an instrument_tags context (for sync/executor use).\"\"\"\n    with instrument_tags(tags):\n        return func()\n\n\nclass StepWorkerFunction(Protocol):\n    def __call__(\n        self,\n        state: StepWorkerState,\n        step_name: str,\n        event: Event,\n        workflow: Workflow,\n        retry: RetryAttempt = RetryAttempt(),\n    ) -> Awaitable[list[StepFunctionResult]]: ...\n\n\nasync def partial(\n    func: Callable[..., Any],\n    step_config: StepConfig,\n    event: Event,\n    context: Context,\n    workflow: Workflow,\n) -> Callable[[], Any]:\n    kwargs: dict[str, Any] = {}\n    kwargs[step_config.event_name] = event\n    if step_config.context_parameter:\n        # Convert to internal face for step execution\n        kwargs[step_config.context_parameter] = context\n    with workflow._resource_manager.resolution_scope():\n        for resource_def in step_config.resources:\n            descriptor = resource_def.resource\n            descriptor.set_type_annotation(resource_def.type_annotation)\n            # Unified resolution through ResourceManager\n            resource_value = await workflow._resource_manager.get(resource=descriptor)\n            kwargs[resource_def.name] = resource_value\n    return functools.partial(func, **kwargs)\n\n\ndef as_step_worker_functions(workflow: Workflow) -> dict[str, StepWorkerFunction]:\n    step_funcs = workflow._get_steps()\n    step_workers: dict[str, StepWorkerFunction] = {\n        name: as_step_worker_function(getattr(func, \"__func__\", func))\n        for name, func in step_funcs.items()\n    }\n    return step_workers\n\n\ndef as_step_worker_function(\n    func: Callable[P, Awaitable[StepReturnT]],\n) -> StepWorkerFunction:\n    \"\"\"\n    Wrap a step function, setting context variables and handling exceptions to instead\n    return the appropriate StepFunctionResult.\n    \"\"\"\n\n    # Keep original function reference for free-function steps; for methods we\n    # will resolve the currently-bound method from the provided workflow at call time.\n    original_func: Callable[..., Awaitable[StepReturnT]] = func\n\n    # Avoid functools.wraps here because it would set __wrapped__ to the bound\n    # method (when present), which would strongly reference the workflow\n    # instance and prevent garbage collection under high churn.\n    async def wrapper(\n        state: StepWorkerState,\n        step_name: str,\n        event: Event,\n        workflow: Workflow,\n        retry: RetryAttempt = RetryAttempt(),\n    ) -> list[StepFunctionResult]:\n        from workflows.context.context import Context\n\n        internal_context = Context._create_internal(workflow=workflow)\n        returns = Returns(return_values=[])\n\n        token = StepWorkerStateContextVar.set(\n            StepWorkerContext(\n                state=state,\n                returns=returns,\n                retry=retry,\n            )\n        )\n        ctx_token = InternalContextVar.set(weakref.ref(internal_context))\n\n        try:\n            config = workflow._get_steps()[step_name]._step_config\n            # Resolve callable at call time:\n            # - If the workflow has an attribute with the step name, use it\n            #   (this yields a bound method for instance-defined steps).\n            # - Otherwise, fall back to the original function (free function step).\n            try:\n                call_func = getattr(workflow, step_name)\n            except AttributeError:\n                call_func = original_func\n            # For async steps, intercept WaitingForEvent and CancelledError before\n            # they reach dispatcher.span() to prevent them from being recorded as\n            # error spans.\n            captured_waiting: WaitingForEvent | None = None\n            captured_cancelled: BaseException | None = None\n            if asyncio.iscoroutinefunction(call_func):\n\n                @functools.wraps(call_func)\n                async def span_safe_call(*args: Any, **kwargs: Any) -> Any:\n                    nonlocal captured_waiting, captured_cancelled\n                    try:\n                        step_result = await call_func(*args, **kwargs)\n                        if step_result is not None and isinstance(step_result, Event):\n                            _emit_output_event(\n                                WorkflowStepOutputEvent(\n                                    output=summarize_event(step_result)\n                                )\n                            )\n                        return step_result\n                    except WaitingForEvent as e:\n                        captured_waiting = e\n                        return None\n                    except asyncio.CancelledError as e:\n                        _dispatcher.event(SpanCancelledEvent(reason=\"step cancelled\"))\n                        captured_cancelled = e\n                        return None\n\n                span_target = span_safe_call\n            else:\n\n                @functools.wraps(call_func)\n                def span_safe_sync_call(*args: Any, **kwargs: Any) -> Any:\n                    step_result = call_func(*args, **kwargs)\n                    if step_result is not None and isinstance(step_result, Event):\n                        _emit_output_event(\n                            WorkflowStepOutputEvent(output=summarize_event(step_result))\n                        )\n                    return step_result\n\n                span_target = span_safe_sync_call\n\n            # Prepare input event tags — these become span attributes when the\n            # span is entered (inside partial_func), not when the wrapper is created.\n            try:\n                input_tags = {\n                    \"llamaindex.step.input_event\": type(event).__name__,\n                    \"llamaindex.step.input_summary\": summarize_event(event),\n                }\n            except Exception:\n                input_tags = {}\n            merged_tags = {**active_instrument_tags.get(), **input_tags}\n\n            partial_func = await partial(\n                func=workflow._dispatcher.span(span_target),\n                step_config=config,\n                event=event,\n                context=internal_context,\n                workflow=workflow,\n            )\n\n            try:\n                # coerce to coroutine function\n                if not asyncio.iscoroutinefunction(call_func):\n                    # run_in_executor doesn't accept **kwargs, so we need to use partial\n                    copy = copy_context()\n\n                    result: StepReturnT = (\n                        await asyncio.get_event_loop().run_in_executor(\n                            None,\n                            lambda: copy.run(\n                                lambda: _run_with_tags(merged_tags, partial_func)\n                            ),\n                        )\n                    )\n                else:\n                    with instrument_tags(merged_tags):\n                        result = await partial_func()\n                    if captured_cancelled is not None:\n                        raise captured_cancelled\n                    if captured_waiting is not None:\n                        raise captured_waiting\n                if result is not None and not isinstance(result, Event):\n                    msg = f\"Step function {step_name} returned {type(result).__name__} instead of an Event instance.\"\n                    raise WorkflowRuntimeError(msg)\n                returns.return_values.append(StepWorkerResult(result=result))\n            except WaitingForEvent as e:\n                await asyncio.sleep(0)\n                returns.return_values.append(e.add)\n            except Exception as e:\n                returns.return_values.append(\n                    StepWorkerFailed(exception=e, failed_at=time.time())\n                )\n\n            await internal_context._finalize_step()\n            return returns.return_values\n        finally:\n            try:\n                InternalContextVar.reset(ctx_token)\n            except Exception:\n                pass\n            try:\n                StepWorkerStateContextVar.reset(token)\n            except Exception:\n                pass\n\n    # Manually set minimal metadata without retaining bound instance references.\n    try:\n        unbound_for_wrapped = getattr(func, \"__func__\", func)\n        wrapper.__name__ = getattr(func, \"__name__\", wrapper.__name__)\n        wrapper.__qualname__ = getattr(func, \"__qualname__\", wrapper.__qualname__)\n        # Point __wrapped__ to the unbound function when available to avoid\n        # strong refs to the instance via a bound method object.\n        setattr(wrapper, \"__wrapped__\", unbound_for_wrapped)\n    except Exception:\n        # Best-effort; lack of these attributes is non-fatal.\n        pass\n\n    return wrapper\n\n\ndef create_workflow_run_function(\n    workflow: Workflow, control_loop_fn: ControlLoopFunction = control_loop\n) -> WorkflowRunFunction:\n    async def run_workflow(\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        tags: dict[str, Any] | None = None,\n    ) -> StopEvent:\n        from workflows.context.context import Context\n        from workflows.context.internal_context import InternalContext\n\n        registered = workflow._runtime.get_or_register(workflow)\n        # Set run_id context before creating internal context\n        internal_ctx = Context._create_internal(workflow=workflow)\n        internal_adapter = workflow._runtime.get_internal_adapter(workflow)\n\n        # Restore propagation context (otel trace, instrument tags, etc.)\n        # before creating any spans so they parent correctly.\n        get_dispatcher().restore_propagation_context(tags or {})\n\n        # defer execution to make sure the task can be captured and passed\n        # to the handler as async exception, protecting against exceptions from before_start\n        await asyncio.sleep(0)\n\n        run_ctx = RunContext(\n            workflow=workflow,\n            run_adapter=internal_adapter,\n            context=internal_ctx,\n            steps=registered.steps,\n        )\n        # Create a wrapping span so that all step spans have a parent.\n        # The caller's span context is captured via propagation context (tags)\n        # and restored above, so this span parents correctly under the caller.\n        cls_name = workflow.__class__.__name__\n        span_id = f\"{cls_name}.run-{uuid.uuid4()}\"\n        outer_parent_span_id = active_span_id.get()\n        span_token = active_span_id.set(span_id)\n\n        bound_args = inspect.signature(run_workflow).bind(init_state, start_event, tags)\n\n        # Set start event info as instrument tags for the run span\n        if start_event is not None:\n            try:\n                run_input_tags = {\n                    \"llamaindex.start_event\": summarize_event(start_event),\n                }\n            except Exception:\n                run_input_tags = {}\n        else:\n            run_input_tags = {}\n\n        with instrument_tags({**active_instrument_tags.get(), **run_input_tags}):\n            _dispatcher.span_enter(\n                id_=span_id,\n                bound_args=bound_args,\n                instance=workflow,\n                parent_id=outer_parent_span_id,\n            )\n\n        try:\n            try:\n                with run_context(run_ctx):\n                    result = await control_loop_fn(\n                        start_event,\n                        init_state,\n                        internal_adapter.run_id,\n                    )\n\n                    _emit_output_event(\n                        WorkflowRunOutputEvent(output=summarize_event(result))\n                    )\n\n                    _dispatcher.span_exit(\n                        id_=span_id,\n                        bound_args=bound_args,\n                        instance=workflow,\n                        result=result,\n                    )\n\n                    return result\n            finally:\n                # Cancel any background tasks from InternalContext on completion or cancellation\n                if isinstance(internal_ctx._face, InternalContext):\n                    internal_ctx._face.cancel_background_tasks()\n        except WorkflowCancelledByUser:\n            # User-initiated cancellation is not an error — exit the span\n            # cleanly so it shows as OK rather than ERROR in traces.\n            _dispatcher.event(SpanCancelledEvent(reason=\"workflow cancelled by user\"))\n            _dispatcher.span_exit(\n                id_=span_id,\n                bound_args=bound_args,\n                instance=workflow,\n                result=None,\n            )\n            raise\n        except BaseException as e:\n            _dispatcher.event(SpanDropEvent(span_id=span_id, err_str=str(e)))\n            _dispatcher.span_drop(\n                id_=span_id, bound_args=bound_args, instance=workflow, err=e\n            )\n            raise\n        finally:\n            try:\n                active_span_id.reset(span_token)\n            except ValueError:\n                pass\n\n    return run_workflow\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/types/ticks.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nTicks (events) that drive the control loop.\n\nThe control loop waits for ticks to arrive, then processes them through a reducer\nto produce updated state and commands. Ticks represent all the different kinds of\nevents that can occur during workflow execution:\n  - New events added to the workflow\n  - Step function execution completing\n  - Timeout occurring\n  - User cancellation\n  - External event publishing requests\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Annotated, Literal\n\nfrom pydantic import BaseModel, ConfigDict, Discriminator, Field, TypeAdapter\nfrom workflows.events import SerializableEvent, SerializableOptionalException\nfrom workflows.runtime.types.results import StepFunctionResult\n\n\nclass TickStepResult(BaseModel):\n    \"\"\"When processed, executes a step function and publishes the result\"\"\"\n\n    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)\n    type: Literal[\"step_result\"] = \"step_result\"\n    step_name: str\n    worker_id: int\n    event: SerializableEvent\n    result: list[Annotated[StepFunctionResult, Discriminator(\"type\")]]\n\n\nclass TickAddEvent(BaseModel):\n    \"\"\"When sent, adds an event to the workflow's event queue\"\"\"\n\n    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)\n    type: Literal[\"add_event\"] = \"add_event\"\n    event: SerializableEvent\n    step_name: str | None = None\n    attempts: int | None = None\n    first_attempt_at: float | None = None\n    last_exception: SerializableOptionalException = None\n    last_failed_at: float | None = None\n    recovery_counts: dict[str, int] = Field(default_factory=dict)\n\n\nclass TickCancelRun(BaseModel):\n    \"\"\"When processed, cancels the workflow run\"\"\"\n\n    model_config = ConfigDict(frozen=True)\n    type: Literal[\"cancel_run\"] = \"cancel_run\"\n\n\nclass TickIdleRelease(BaseModel):\n    \"\"\"When processed, cleanly releases the workflow due to idleness\"\"\"\n\n    model_config = ConfigDict(frozen=True)\n    type: Literal[\"idle_release\"] = \"idle_release\"\n\n\nclass TickPublishEvent(BaseModel):\n    \"\"\"When sent, publishes an event to workflow consumers, e.g. a UI or a callback\"\"\"\n\n    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)\n    type: Literal[\"publish_event\"] = \"publish_event\"\n    event: SerializableEvent\n\n\nclass TickTimeout(BaseModel):\n    \"\"\"When processed, times the workflow out, cancelling it\"\"\"\n\n    model_config = ConfigDict(frozen=True)\n    type: Literal[\"timeout\"] = \"timeout\"\n    timeout: float\n\n\nclass TickWaiterTimeout(BaseModel):\n    \"\"\"When processed, marks a specific waiter as timed out and replays the step.\"\"\"\n\n    model_config = ConfigDict(frozen=True)\n    type: Literal[\"waiter_timeout\"] = \"waiter_timeout\"\n    step_name: str\n    waiter_id: str\n\n\nclass TickIdleCheck(BaseModel):\n    \"\"\"Scheduled after state appears idle, to re-check after async events drain.\n\n    Appended to tick_buffer when the reducer sees quiescent state. Processed\n    on the next loop iteration after asyncio.sleep(0), giving in-flight\n    ctx.send_event() calls a chance to deliver via the pull task.\n    \"\"\"\n\n    model_config = ConfigDict(frozen=True)\n    type: Literal[\"idle_check\"] = \"idle_check\"\n\n\nWorkflowTick = Annotated[\n    TickStepResult\n    | TickAddEvent\n    | TickCancelRun\n    | TickPublishEvent\n    | TickTimeout\n    | TickWaiterTimeout\n    | TickIdleCheck\n    | TickIdleRelease,\n    Discriminator(\"type\"),\n]\n\nWorkflowTickAdapter: TypeAdapter[WorkflowTick] = TypeAdapter(WorkflowTick)\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/runtime/verbose.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nVerbose runtime decorator that logs tick-level workflow activity in real time.\n\nIntercepts both ``write_to_event_stream`` (for step state changes) and\n``on_tick`` (for all other tick types: events added, publishes, timeouts,\ncancellation, idle releases).\n\nOutput destination is auto-detected: if the ``\"workflows.verbose\"`` logger is\nconfigured to emit DEBUG or INFO messages, those levels are used (in that\npriority order).  Otherwise output falls back to :func:`print`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Callable\n\nfrom workflows._event_summary import summarize_event\nfrom workflows.events import Event, StepState, StepStateChanged, StopEvent\nfrom workflows.runtime.runtime_decorators import (\n    BaseInternalRunAdapterDecorator,\n    BaseRuntimeDecorator,\n)\nfrom workflows.runtime.types.plugin import InternalRunAdapter, Runtime, WorkflowTick\nfrom workflows.runtime.types.results import StepWorkerResult\nfrom workflows.runtime.types.ticks import (\n    TickAddEvent,\n    TickCancelRun,\n    TickIdleRelease,\n    TickPublishEvent,\n    TickStepResult,\n    TickTimeout,\n    TickWaiterTimeout,\n)\nfrom workflows.workflow import Workflow\n\nverbose_logger = logging.getLogger(\"workflows.verbose\")\n\n\ndef _clean_event_name(raw: str) -> str:\n    \"\"\"Extract a short class name from ``str(type(...))`` format.\n\n    Handles both ``\"<class 'pkg.module.Cls'>\"`` and plain ``\"Cls\"`` strings.\n    Returns ``None`` when the underlying type is ``NoneType``.\n    \"\"\"\n    if raw.startswith(\"<class '\") and raw.endswith(\"'>\"):\n        raw = raw[8:-2].rsplit(\".\", 1)[-1]\n    return raw\n\n\ndef _resolve_output() -> Callable[[str], None]:\n    \"\"\"Pick the best output sink based on current logging configuration.\"\"\"\n    if verbose_logger.isEnabledFor(logging.DEBUG):\n        return verbose_logger.debug\n    if verbose_logger.isEnabledFor(logging.INFO):\n        return verbose_logger.info\n    return print\n\n\nclass _VerboseInternalRunAdapter(BaseInternalRunAdapterDecorator):\n    \"\"\"Intercepts write_to_event_stream and on_tick to print/log workflow activity.\"\"\"\n\n    def __init__(\n        self,\n        decorated: InternalRunAdapter,\n        output: Callable[[str], None],\n    ) -> None:\n        super().__init__(decorated)\n        self._output = output\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        if isinstance(event, StepStateChanged):\n            prefix = f\"[{event.name}:{event.worker_id}]\"\n            if event.step_state == StepState.RUNNING:\n                self._output(f\"{prefix} started from {event.input_event_name}\")\n            elif event.step_state == StepState.NOT_RUNNING:\n                name = (\n                    _clean_event_name(event.output_event_name)\n                    if event.output_event_name\n                    else None\n                )\n                if name and name != \"NoneType\":\n                    self._output(f\"{prefix} complete with {name}\")\n                else:\n                    self._output(f\"{prefix} complete with no result\")\n            elif event.step_state == StepState.PREPARING:\n                self._output(f\"[{event.name}] enqueued (waiting for capacity)\")\n        await super().write_to_event_stream(event)\n\n    async def on_tick(self, tick: WorkflowTick) -> None:\n        if isinstance(tick, TickAddEvent):\n            summary = summarize_event(tick.event)\n            target = f\" -> {tick.step_name}\" if tick.step_name else \"\"\n            self._output(f\"[tick] add: {summary}{target}\")\n        elif isinstance(tick, TickPublishEvent):\n            self._output(f\"[tick] publish: {summarize_event(tick.event)}\")\n        elif isinstance(tick, TickTimeout):\n            self._output(f\"[tick] timeout: {tick.timeout}s\")\n        elif isinstance(tick, TickWaiterTimeout):\n            self._output(\n                f\"[tick] waiter timeout: step {tick.step_name} waiter {tick.waiter_id}\"\n            )\n        elif isinstance(tick, TickCancelRun):\n            self._output(\"[tick] cancelled\")\n        elif isinstance(tick, TickIdleRelease):\n            self._output(\"[tick] idle release\")\n        elif isinstance(tick, TickStepResult):\n            for result in tick.result:\n                if isinstance(result, StepWorkerResult) and isinstance(\n                    result.result, StopEvent\n                ):\n                    self._output(f\"[result] {summarize_event(result.result)}\")\n        await super().on_tick(tick)\n\n\nclass VerboseDecorator(BaseRuntimeDecorator):\n    \"\"\"Runtime decorator that prints step starts and completions.\n\n    Output destination is auto-detected at construction time based on the\n    ``\"workflows.verbose\"`` logger's effective level.  If DEBUG or INFO\n    messages would be emitted, the logger is used; otherwise falls back to\n    :func:`print`.\n    \"\"\"\n\n    def __init__(self, decorated: Runtime) -> None:\n        super().__init__(decorated)\n        self._output = _resolve_output()\n\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        inner = self._decorated.get_internal_adapter(workflow)\n        return _VerboseInternalRunAdapter(inner, self._output)\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/server/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Re-export server components from the optional llama-agents-server package.\"\"\"\n\nimport warnings\n\nwarnings.warn(\n    \"Importing from 'workflows.server' is deprecated. \"\n    \"Install 'llama-agents-server' and use \"\n    \"'from llama_agents.server import ...' instead.\",\n    DeprecationWarning,\n    stacklevel=2,\n)\n\ntry:\n    from llama_agents.server import (\n        AbstractWorkflowStore,\n        HandlerQuery,\n        PersistentHandler,\n        SqliteWorkflowStore,\n        WorkflowServer,\n    )\nexcept ImportError as e:\n    raise ImportError(\n        \"workflows.server requires the 'server' extra. \"\n        \"Install with: pip install 'llama-index-workflows[server]'\"\n    ) from e\n\n__all__ = [\n    \"WorkflowServer\",\n    \"AbstractWorkflowStore\",\n    \"HandlerQuery\",\n    \"PersistentHandler\",\n    \"SqliteWorkflowStore\",\n]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/server/sqlite/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Re-export sqlite components from the optional llama-agents-server package.\"\"\"\n\ntry:\n    from llama_agents.server import (\n        SqliteWorkflowStore,\n    )\nexcept ImportError as e:\n    raise ImportError(\n        \"workflows.server.sqlite requires the 'server' extra. \"\n        \"Install with: pip install 'llama-index-workflows[server]'\"\n    ) from e\n\n__all__ = [\"SqliteWorkflowStore\"]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/testing/__init__.py",
    "content": "from .runner import WorkflowTestRunner\n\n__all__ = [\"WorkflowTestRunner\"]\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/testing/runner.py",
    "content": "from collections import Counter\nfrom dataclasses import dataclass\nfrom typing import Any, Optional\n\nfrom workflows import Context, Workflow\nfrom workflows.events import Event, EventType, StartEvent\n\n\n@dataclass\nclass WorkflowTestResult:\n    \"\"\"\n    Container for workflow test results\n\n    Attributes:\n        collected (list[Event]): List of collected events\n        event_type (dict[EventType, int]): Dictionary that maps each event type with its number of occurencies within the collected events\n        result (Any): Final output of the workflow run\n    \"\"\"\n\n    collected: list[Event]\n    event_types: dict[EventType, int]\n    result: Any\n    ctx: Context\n\n\nclass WorkflowTestRunner:\n    \"\"\"\n    Utility class that can be used to test workflows end-to-end.\n\n    Attributes:\n        _workflow (Workflow): The workflow to be tested\n    \"\"\"\n\n    def __init__(\n        self,\n        workflow: \"Workflow\",\n    ):\n        self._workflow = workflow\n\n    async def run(\n        self,\n        start_event: StartEvent = StartEvent(),\n        ctx: Optional[\"Context\"] = None,\n        expose_internal: bool = True,\n        exclude_events: list[EventType] | None = None,\n    ) -> WorkflowTestResult:\n        \"\"\"\n        Run a workflow end-to-end and collect the events that are streamed during its execution.\n\n        Args:\n            start_event (StartEvent): The input event for the workflow\n            expose_internal (bool): Whether or not to expose internal events. Defaults to True if not set.\n            exclude_events. (list[EventType]): A list of event types to exclude from the collected events. Defaults to None if not set.\n\n        Returns:\n            WorkflowTestResult\n\n        Example:\n            ```\n            wf = GreetingWorkflow()\n            runner = WorkflowTestRunner(wf)\n            test_result = runner.run(start_even=StartEvent(message=\"hello\"), expose_internal = True, exclude_events = [StepStateChanged])\n            assert test_result.collected == 22\n            assert test_result.event_types.get(StepStateChanged, 0) == 8\n            assert str(test_result.result) == \"hello Adam!\"\n            ```\n        \"\"\"\n        handler = self._workflow.run(start_event=start_event, ctx=ctx)\n        collected_events: list[Event] = []\n        async for event in handler.stream_events(expose_internal=expose_internal):\n            if exclude_events and type(event) in exclude_events:\n                continue\n            collected_events.append(event)\n        result = await handler\n        event_freqs: dict[EventType, int] = dict(\n            Counter([type(ev) for ev in collected_events])\n        )\n        assert handler.ctx is not None\n        return WorkflowTestResult(\n            collected=collected_events,\n            result=result,\n            event_types=event_freqs,\n            ctx=handler.ctx,\n        )\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/types.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom typing import Any, TypeVar\n\nfrom .events import StopEvent\n\nStopEventT = TypeVar(\"StopEventT\", bound=StopEvent)\n# TODO: When releasing 1.0, remove support for Any\n# and enforce usage of StopEventT\nRunResultT = StopEventT | Any\n\"\"\"\nType aliases for workflow results.\n\n- `StopEventT`: Generic bound to [StopEvent][workflows.events.StopEvent]\n- `RunResultT`: Result type returned by a workflow run. Today it allows either\n  a `StopEventT` subclass or `Any` for backward compatibility; future versions\n  may restrict this to `StopEventT` only.\n\"\"\"\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/utils.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport inspect\nimport secrets\nimport string\nfrom typing import (\n    TYPE_CHECKING,\n    Annotated,\n    Any,\n    Callable,\n    Optional,\n    cast,\n    get_args,\n    get_origin,\n    get_type_hints,\n)\n\nif TYPE_CHECKING:\n    from workflows.decorators import StepFunction\n\nfrom types import UnionType\nfrom typing import Union\n\nfrom pydantic import BaseModel\n\nfrom .errors import WorkflowValidationError\nfrom .events import Event, EventType\nfrom .resource import ResourceDefinition, ResourceDescriptor\n\nBUSY_WAIT_DELAY = 0.01\n\n\nclass StepSignatureSpec(BaseModel):\n    \"\"\"A Pydantic model representing the signature of a step function or method.\"\"\"\n\n    accepted_events: dict[str, list[EventType]]\n    return_types: list[Any]\n    context_parameter: str | None\n    context_state_type: Any | None\n    resources: list[Any]\n\n\ndef inspect_signature(\n    fn: Callable, localns: dict[str, Any] | None = None\n) -> StepSignatureSpec:\n    \"\"\"\n    Given a function, ensure the signature is compatible with a workflow step.\n\n    Args:\n        fn (Callable): The function to inspect.\n\n    Returns:\n        StepSignatureSpec: A specification object containing:\n            - accepted_events: Dictionary mapping parameter names to their event types\n            - return_types: List of return type annotations\n            - context_parameter: Name of the context parameter if present\n\n    Raises:\n        TypeError: If fn is not a callable object\n\n    \"\"\"\n    if not callable(fn):\n        raise TypeError(f\"Expected a callable object, got {type(fn).__name__}\")\n\n    sig = inspect.signature(fn)\n    type_hints = _resolve_type_hints(fn, include_extras=True, localns=localns)\n\n    accepted_events: dict[str, list[EventType]] = {}\n    context_parameter = None\n    context_state_type = None\n    resources = []\n\n    # Inspect function parameters\n    for name, t in sig.parameters.items():\n        # Ignore self and cls\n        if name in (\"self\", \"cls\"):\n            continue\n\n        annotation = type_hints.get(name, t.annotation)\n\n        # Handle Context[StateType] annotations\n        if get_origin(annotation) is not None:\n            origin = get_origin(annotation)\n            args = get_args(annotation)\n\n            # Check if this is Context[StateType]\n            if hasattr(origin, \"__name__\") and origin.__name__ == \"Context\":\n                context_parameter = name\n                # Extract state type from generic parameter\n                if args:\n                    context_state_type = args[0]\n                continue\n\n        # Handle Annotated types for resources\n        if get_origin(annotation) is Annotated:\n            args = get_args(annotation)\n            type_annotation = args[0] if args else None\n            descriptor = args[1] if len(args) > 1 else None\n            if descriptor is not None and isinstance(descriptor, ResourceDescriptor):\n                # Pass localns to resource for nested annotation resolution\n                descriptor.set_localns(localns)\n                resources.append(\n                    ResourceDefinition(\n                        name=name, resource=descriptor, type_annotation=type_annotation\n                    )\n                )\n            continue\n\n        # Get name and type of the Context param (without state type)\n        if hasattr(annotation, \"__name__\") and annotation.__name__ == \"Context\":\n            context_parameter = name\n            continue\n\n        # Collect name and types of the event param\n        param_types = _get_param_types(t, type_hints)\n        if all(\n            param_t == Event\n            or (inspect.isclass(param_t) and issubclass(param_t, Event))\n            for param_t in param_types\n        ):\n            accepted_events[name] = param_types\n            continue\n\n    return StepSignatureSpec(\n        accepted_events=accepted_events,\n        return_types=_get_return_types(fn, localns=localns),\n        context_parameter=context_parameter,\n        context_state_type=context_state_type,\n        resources=resources,\n    )\n\n\ndef validate_step_signature(spec: StepSignatureSpec) -> None:\n    \"\"\"\n    Validate that a step signature specification meets workflow requirements.\n\n    Args:\n        spec (StepSignatureSpec): The signature specification to validate.\n\n    Raises:\n        WorkflowValidationError: If the signature is invalid for a workflow step.\n\n    \"\"\"\n    num_of_events = len(spec.accepted_events)\n    if num_of_events == 0:\n        msg = \"Step signature must have at least one parameter annotated as type Event\"\n        raise WorkflowValidationError(msg)\n    elif num_of_events > 1:\n        msg = f\"Step signature must contain exactly one parameter of type Event but found {num_of_events}.\"\n        raise WorkflowValidationError(msg)\n\n    if not spec.return_types:\n        msg = \"Return types of workflows step functions must be annotated with their type.\"\n        raise WorkflowValidationError(msg)\n\n\ndef get_steps_from_class(_class: object) -> dict[str, StepFunction]:\n    \"\"\"\n    Given a class, return the list of its methods that were defined as steps.\n\n    Args:\n        _class (object): The class to inspect for step methods.\n\n    Returns:\n        dict[str, Callable]: A dictionary mapping step names to their corresponding methods.\n\n    \"\"\"\n    from workflows.decorators import StepFunction\n\n    step_methods: dict[str, StepFunction] = {}\n    all_methods = inspect.getmembers(_class, predicate=inspect.isfunction)\n\n    for name, method in all_methods:\n        if hasattr(method, \"_step_config\"):\n            step_methods[name] = cast(StepFunction, method)\n\n    return step_methods\n\n\ndef get_steps_from_instance(workflow: object) -> dict[str, StepFunction]:\n    \"\"\"\n    Given a workflow instance, return the list of its methods that were defined as steps.\n\n    Args:\n        workflow (object): The workflow instance to inspect.\n\n    Returns:\n        dict[str, Callable]: A dictionary mapping step names to their corresponding methods.\n\n    \"\"\"\n    from workflows.decorators import StepFunction\n\n    step_methods: dict[str, StepFunction] = {}\n    all_methods = inspect.getmembers(workflow, predicate=inspect.ismethod)\n\n    for name, method in all_methods:\n        if hasattr(method, \"_step_config\"):\n            step_methods[name] = cast(StepFunction, method)\n\n    return step_methods\n\n\ndef _get_param_types(param: inspect.Parameter, type_hints: dict) -> list[Any]:\n    \"\"\"\n    Extract and process the types of a parameter.\n\n    This helper function handles Union and Optional types, returning a list of the actual types.\n    For Union[A, None] (Optional[A]), it returns [A].\n\n    Args:\n        param (inspect.Parameter): The parameter to analyze.\n        type_hints (dict): The resolved type hints for the function.\n\n    Returns:\n        list[Any]: A list of extracted types, excluding None from Unions/Optionals.\n\n    \"\"\"\n    typ = type_hints.get(param.name, param.annotation)\n    if typ is inspect.Parameter.empty:\n        return [Any]\n    if get_origin(typ) in (Union, Optional, UnionType):\n        return [t for t in get_args(typ) if t is not type(None)]\n    return [typ]\n\n\ndef _get_return_types(\n    func: Callable, localns: dict[str, Any] | None = None\n) -> list[Any]:\n    \"\"\"\n    Extract the return type hints from a function.\n\n    Handles Union, Optional, and List types.\n    \"\"\"\n    type_hints = _resolve_type_hints(func, localns=localns)\n    return_hint = type_hints.get(\"return\")\n    if return_hint is None:\n        return []\n\n    origin = get_origin(return_hint)\n    if origin in (Union, UnionType):\n        # Optional is Union[type, None] so it's covered here\n        return [t for t in get_args(return_hint) if t is not type(None)]\n    else:\n        return [return_hint]\n\n\ndef _resolve_type_hints(\n    func: Callable,\n    *,\n    include_extras: bool = False,\n    localns: dict[str, Any] | None = None,\n) -> dict[str, Any]:\n    try:\n        return get_type_hints(func, include_extras=include_extras, localns=localns)\n    except NameError as exc:\n        missing_name = getattr(exc, \"name\", None)\n        missing_msg = f\" Missing name: {missing_name}.\" if missing_name else \"\"\n        func_name = getattr(func, \"__qualname__\", type(func).__name__)\n        msg = (\n            \"Failed to resolve type annotations for \"\n            f\"{func_name}.{missing_msg} \"\n            \"If you are using 'from __future__ import annotations' or string \"\n            \"annotations, ensure referenced names are available in module scope \"\n            \"or in the scope where the @step decorator is applied.\"\n        )\n        raise WorkflowValidationError(msg) from exc\n\n\ndef is_free_function(qualname: str) -> bool:\n    \"\"\"\n    Determines whether a certain qualified name points to a free function.\n\n    A free function is either a module-level function or a nested function.\n    This implementation follows PEP-3155 for handling nested function detection.\n\n    Args:\n        qualname (str): The qualified name to analyze.\n\n    Returns:\n        bool: True if the name represents a free function, False otherwise.\n\n    Raises:\n        ValueError: If the qualified name is empty.\n\n    \"\"\"\n    if not qualname:\n        msg = \"The qualified name cannot be empty\"\n        raise ValueError(msg)\n\n    toks = qualname.split(\".\")\n    if len(toks) == 1:\n        # e.g. `my_function`\n        return True\n    elif \"<locals>\" not in toks:\n        # e.g. `MyClass.my_method`\n        return False\n    else:\n        return toks[-2] == \"<locals>\"\n\n\n_alphabet = string.ascii_letters + string.digits  # A-Z, a-z, 0-9\n\n\ndef _nanoid(size: int = 10) -> str:\n    \"\"\"Returns a unique identifier with the format 'kY2xP9hTnQ'.\"\"\"\n    return \"\".join(secrets.choice(_alphabet) for _ in range(size))\n"
  },
  {
    "path": "packages/llama-index-workflows/src/workflows/workflow.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    get_args,\n)\n\nfrom llama_index_instrumentation import get_dispatcher\nfrom pydantic import ValidationError\n\nif TYPE_CHECKING:  # pragma: no cover\n    from .context import Context\n    from .runtime.types.plugin import Runtime\nfrom .decorators import CatchErrorHandler, StepConfig, StepFunction, WorkflowGraphCheck\nfrom .errors import (\n    WorkflowRuntimeError,\n    WorkflowValidationError,\n)\nfrom .events import Event, StartEvent\nfrom .handler import WorkflowHandler\nfrom .resource import ResourceManager\nfrom .types import RunResultT\nfrom .utils import get_steps_from_class, get_steps_from_instance\n\ndispatcher = get_dispatcher(__name__)\nlogger = logging.getLogger(__name__)\n\n\nclass WorkflowMeta(type):\n    def __init__(cls, name: str, bases: tuple[type, ...], dct: dict[str, Any]) -> None:\n        super().__init__(name, bases, dct)\n        cls._step_functions: dict[str, StepFunction] = {}\n\n\nclass Workflow(metaclass=WorkflowMeta):\n    \"\"\"\n    Event-driven orchestrator to define and run application flows using typed steps.\n\n    A `Workflow` is composed of `@step`-decorated callables that accept and emit\n    typed [Event][workflows.events.Event]s. Steps can be declared as instance\n    methods or as free functions registered via the decorator.\n\n    Key features:\n    - Validation of step signatures and event graph before running\n    - Typed start/stop events\n    - Streaming of intermediate events\n    - Optional human-in-the-loop events\n    - Retry policies per step\n    - Resource injection\n\n    Examples:\n        Basic usage:\n\n        ```python\n        from workflows import Workflow, step\n        from workflows.events import StartEvent, StopEvent\n\n        class MyFlow(Workflow):\n            @step\n            async def start(self, ev: StartEvent) -> StopEvent:\n                return StopEvent(result=\"done\")\n\n        result = await MyFlow(timeout=60).run(topic=\"Pirates\")\n        ```\n\n        Custom start/stop events and streaming:\n\n        ```python\n        handler = MyFlow().run()\n        async for ev in handler.stream_events():\n            ...\n        result = await handler\n        ```\n\n    See Also:\n        - [step][workflows.decorators.step]\n        - [Event][workflows.events.Event]\n        - [Context][workflows.context.context.Context]\n        - [WorkflowHandler][workflows.handler.WorkflowHandler]\n        - [RetryPolicy][workflows.retry_policy.RetryPolicy]\n    \"\"\"\n\n    # Populated by the metaclass; declared here for type checkers.\n    _step_functions: dict[str, StepFunction]\n    _step_functions_version: int = 0\n\n    _runtime: Runtime\n    _workflow_name: str | None\n\n    def __init__(\n        self,\n        timeout: float | None = 45.0,\n        disable_validation: bool = False,\n        verbose: bool = False,\n        resource_manager: ResourceManager | None = None,\n        num_concurrent_runs: int | None = None,\n        runtime: Runtime | None = None,\n        workflow_name: str | None = None,\n        skip_graph_checks: set[WorkflowGraphCheck] | None = None,\n    ) -> None:\n        \"\"\"\n        Initialize a workflow instance.\n\n        Args:\n            timeout (float | None): Max seconds to wait for completion. `None`\n                disables the timeout.\n            disable_validation (bool): Skip pre-run validation of the event graph\n                (not recommended).\n            verbose (bool): If True, print step activity.\n            resource_manager (ResourceManager | None): Custom resource manager\n                for dependency injection.\n            num_concurrent_runs (int | None): Limit on concurrent `run()` calls.\n            runtime (Runtime | None): Optional runtime to use for this workflow.\n                If not provided, uses the current context-scoped runtime or\n                falls back to basic_runtime.\n            workflow_name (str | None): Optional explicit name for this workflow.\n                If not provided, a module-qualified name is computed from\n                the class's `__module__` and `__qualname__` attributes.\n            skip_graph_checks (set[str] | None): Optional set of graph validation\n                checks to skip (e.g. \"reachability\", \"terminal_event\"). Use to\n                allow intentional patterns that would otherwise fail validation.\n        \"\"\"\n        # Inline imports: every module below imports ``Workflow`` transitively,\n        # so deferring to call time breaks the cycle.\n        from workflows.plugins._context import get_current_runtime\n        from workflows.runtime.verbose import VerboseDecorator\n\n        from .representation.validate import (\n            _collect_events,\n            _ensure_start_event_class,\n            _ensure_stop_event_class,\n        )\n\n        # Configuration\n        self._timeout = timeout\n        self._verbose = verbose\n        self._disable_validation = disable_validation\n        self._num_concurrent_runs = num_concurrent_runs\n        # Store explicit name (None means use computed name)\n        self._workflow_name = workflow_name\n\n        step_configs = self._step_configs()\n        cls_name = self.__class__.__name__\n        # Detect StartEvent issues before StopEvent for clearer guidance\n        self._start_event_class = _ensure_start_event_class(step_configs, cls_name)\n        self._stop_event_class = _ensure_stop_event_class(step_configs, cls_name)\n        # Populated by _validate(); empty until a successful validation runs.\n        self._catch_error_handlers: dict[str, CatchErrorHandler] = {}\n        self._handler_for_step: dict[str, str] = {}\n        self._events = _collect_events(step_configs)\n        # Resource management\n        self._resource_manager = resource_manager or ResourceManager()\n        # Instrumentation\n        self._dispatcher = dispatcher\n        self._runtime_locked = False\n        # Validation cache: set after first successful _validate(); skip re-validation on run() until invalidated.\n        # _validated_version tracks which _step_functions_version was validated so add_step() invalidates the cache.\n        self._validation_result: bool | None = None\n        self._validated_version: int = -1\n        checks = skip_graph_checks or set()\n        valid_checks = set(get_args(WorkflowGraphCheck))\n        unknown = checks - valid_checks\n        if unknown:\n            raise WorkflowValidationError(\n                f\"Unknown graph check names: {', '.join(sorted(unknown))}. \"\n                f\"Valid names are: {', '.join(sorted(valid_checks))}\"\n            )\n        self._skip_graph_checks: set[WorkflowGraphCheck] = checks\n\n        # Runtime registration: explicit > context-scoped > basic_runtime\n        self._runtime = runtime if runtime is not None else get_current_runtime()\n        if self._verbose:\n            self._runtime = VerboseDecorator(self._runtime)\n        # Register with runtime for tracking (no-op for BasicRuntime)\n        self._runtime.track_workflow(self)\n\n    def _validate_valid_step_message(self, step: str, message: Event) -> None:\n        \"\"\"Validate that a step name exists in the workflow.\"\"\"\n        if step not in self._get_steps():\n            raise WorkflowRuntimeError(f\"Step {step} does not exist\")\n\n        step_func = self._get_steps()[step]\n        step_config = step_func._step_config\n        if type(message) not in step_config.accepted_events:\n            raise WorkflowRuntimeError(\n                f\"Step {step} does not accept event of type {type(message)}\"\n            )\n\n    @property\n    def runtime(self) -> Runtime:\n        \"\"\"The runtime this workflow is registered with.\"\"\"\n        return self._runtime\n\n    def _switch_runtime(self, new_runtime: Runtime) -> None:\n        if new_runtime is self._runtime:\n            return\n        if self._runtime_locked:\n            raise RuntimeError(\n                \"Cannot reassign runtime after workflow has been launched\"\n            )\n        old = self._runtime\n        old.untrack_workflow(self)\n        self._runtime = new_runtime\n        new_runtime.track_workflow(self)\n\n    @property\n    def workflow_name(self) -> str:\n        \"\"\"\n        The workflow name.\n\n        If an explicit name was provided at construction, returns that.\n        Otherwise, returns a module-qualified name based on the class's\n        __module__ and __qualname__ attributes.\n\n        Examples:\n            - Explicit: `Workflow(workflow_name=\"my-workflow\")` → `\"my-workflow\"`\n            - Module-level class: `\"mymodule.MyWorkflow\"`\n            - Nested class: `\"mymodule.Outer.Inner\"`\n            - Function-scoped: `\"mymodule.func.<locals>.LocalWorkflow\"`\n        \"\"\"\n        if self._workflow_name is not None:\n            return self._workflow_name\n        cls = self.__class__\n        return f\"{cls.__module__}.{cls.__qualname__}\"\n\n    def _switch_workflow_name(self, name: str) -> None:\n        if self._runtime_locked and name != self._workflow_name:\n            raise RuntimeError(\n                \"Cannot change workflow_name after workflow has been launched\"\n            )\n        self._workflow_name = name\n\n    def _step_configs(self) -> dict[str, StepConfig]:\n        \"\"\"Return ``{step_name: StepConfig}`` for every registered step.\"\"\"\n        return {name: func._step_config for name, func in self._get_steps().items()}\n\n    @property\n    def start_event_class(self) -> type[StartEvent]:\n        \"\"\"The `StartEvent` subclass accepted by this workflow.\n\n        Determined by inspecting step input types.\n        \"\"\"\n        return self._start_event_class\n\n    @property\n    def events(self) -> list[type[Event]]:\n        \"\"\"Returns all known events emitted by this workflow.\n\n        Determined by inspecting step input/output types.\n        \"\"\"\n        return self._events\n\n    @property\n    def stop_event_class(self) -> type[RunResultT]:\n        \"\"\"The `StopEvent` subclass produced by this workflow.\n\n        Determined by inspecting step return annotations.\n        \"\"\"\n        return self._stop_event_class\n\n    @classmethod\n    def _get_steps_from_class(cls) -> dict[str, StepFunction]:\n        \"\"\"Returns all the steps, whether defined as methods or free functions.\"\"\"\n        return {**get_steps_from_class(cls), **cls._step_functions}\n\n    @classmethod\n    def add_step(cls, func: StepFunction) -> None:\n        \"\"\"\n        Adds a free function as step for this workflow instance.\n\n        It raises an exception if a step with the same name was already added to the workflow.\n        \"\"\"\n        step_config: StepConfig | None = getattr(func, \"_step_config\", None)\n        if not step_config:\n            msg = f\"Step function {func.__name__} is missing the `@step` decorator.\"\n            raise WorkflowValidationError(msg)\n\n        if func.__name__ in cls._get_steps_from_class():\n            msg = f\"A step {func.__name__} is already part of this workflow, please choose another name.\"\n            raise WorkflowValidationError(msg)\n\n        cls._step_functions[func.__name__] = func\n        cls._step_functions_version += 1\n\n    def _get_steps(self) -> dict[str, StepFunction]:\n        \"\"\"Returns all the steps, whether defined as methods or free functions.\"\"\"\n        return {**get_steps_from_instance(self), **self.__class__._step_functions}\n\n    def _get_start_event_instance(\n        self, start_event: StartEvent | None, **kwargs: Any\n    ) -> StartEvent:\n        if start_event is not None:\n            # start_event was used wrong\n            if not isinstance(start_event, StartEvent):\n                msg = \"The 'start_event' argument must be an instance of 'StartEvent'.\"\n                raise ValueError(msg)\n\n            # start_event is ok but point out that additional kwargs will be ignored in this case\n            if kwargs:\n                msg = (\n                    \"Keyword arguments are not supported when 'run()' is invoked with the 'start_event' parameter.\"\n                    f\" These keyword arguments will be ignored: {kwargs}\"\n                )\n                logger.warning(msg)\n            return start_event\n\n        # Old style start event creation, with kwargs used to create an instance of self._start_event_class\n        try:\n            return self._start_event_class(**kwargs)\n        except ValidationError as e:\n            ev_name = self._start_event_class.__name__\n            msg = f\"Failed creating a start event of type '{ev_name}' with the keyword arguments: {kwargs}\"\n            logger.debug(e)\n            raise WorkflowRuntimeError(msg)\n\n    def run(\n        self,\n        ctx: Context | None = None,\n        start_event: StartEvent | None = None,\n        **kwargs: Any,\n    ) -> WorkflowHandler:\n        \"\"\"Run the workflow and return a handler for results and streaming.\n\n        This schedules the workflow execution in the background and returns a\n        [WorkflowHandler][workflows.handler.WorkflowHandler] that can be awaited\n        for the final result or used to stream intermediate events.\n\n        You may pass either a concrete `start_event` instance or keyword\n        arguments that will be used to construct the inferred\n        [StartEvent][workflows.events.StartEvent] subclass.\n\n        Args:\n            ctx (Context | None): Optional context to resume or share state\n                across runs. If omitted, a fresh context is created.\n            start_event (StartEvent | None): Optional explicit start event.\n            **kwargs (Any): Keyword args to initialize the start event when\n                `start_event` is not provided.\n\n        Returns:\n            WorkflowHandler: A future-like object to await the final result and\n            stream events.\n\n        Raises:\n            WorkflowValidationError: If validation fails and validation is\n                enabled.\n            WorkflowRuntimeError: If the start event cannot be created from kwargs.\n            WorkflowTimeoutError: If execution exceeds the configured timeout.\n\n        Examples:\n            ```python\n            # Create and run with kwargs\n            handler = MyFlow().run(topic=\"Pirates\")\n\n            # Stream events\n            async for ev in handler.stream_events():\n                ...\n\n            # Await final result\n            result = await handler\n            ```\n\n            If you subclassed the start event, you can also directly pass it in:\n\n            ```python\n            result = await my_workflow.run(start_event=MyStartEvent(topic=\"Pirates\"))\n            ```\n        \"\"\"\n        from workflows.context import Context\n\n        if not self._runtime_locked:\n            # don't allow switching runtime after a workflow has been launched\n            self._runtime_locked = True\n\n        # Validate the workflow\n        self._validate()\n\n        # Extract run_id before passing remaining kwargs to start event\n        run_id = kwargs.pop(\"run_id\", None)\n\n        # If a previous context is provided, pass its serialized form\n        ctx = ctx if ctx is not None else Context(self)\n        # TODO(v3) - remove dependency on is running for choosing whether to send a StartEvent.\n        # Is not an easily synchronously queryable property.\n        start_event_instance: StartEvent | None = (\n            None\n            if ctx.is_running\n            else self._get_start_event_instance(start_event, **kwargs)\n        )\n        return ctx._workflow_run(\n            workflow=self, start_event=start_event_instance, run_id=run_id\n        )\n\n    def validate(\n        self,\n        *,\n        validate_resource_configs: bool = True,\n        validate_resources: bool = False,\n    ) -> bool:\n        \"\"\"\n        Validate the workflow to ensure it's well-formed.\n\n        This method validates the event graph and optionally validates resources:\n        - Event production/consumption (set-based checks)\n        - Graph structure: all steps reachable from an input event (StartEvent or HumanResponseEvent),\n          and only output events (StopEvent, InputRequiredEvent) may be terminal\n        - Resource configs (JSON files with Pydantic validation) are validated by default\n        - Resource factories are not validated by default (may require env vars)\n        - Circular resource dependencies are caught when validate_resources=True\n\n        Validation result is cached after the first successful run(); subsequent run() calls\n        skip re-validation. Calling validate() explicitly always re-runs all checks.\n\n        Args:\n            validate_resource_configs: If True (default), validate that resource\n                config files exist and contain valid data for their Pydantic models.\n            validate_resources: If False (default), skip resolving resource factories\n                during validation. Set to True to also validate that resource\n                factories can be resolved and detect circular dependencies\n                (may require environment variables or external connections).\n\n        Returns:\n            True if the workflow uses human-in-the-loop, False otherwise.\n        \"\"\"\n        return self._validate(\n            validate_resource_configs=validate_resource_configs,\n            validate_resources=validate_resources,\n            force=True,  # Explicit validate() call should always run\n        )\n\n    def _validate(\n        self,\n        *,\n        validate_resource_configs: bool = True,\n        validate_resources: bool = False,\n        force: bool = False,\n    ) -> bool:\n        if self._disable_validation and not force:\n            return False\n        stale = self._validated_version != self.__class__._step_functions_version\n        if not force and not stale and self._validation_result is not None:\n            return self._validation_result\n\n        # Inline import: ``representation`` transitively imports ``Workflow``.\n        from .representation.validate import (\n            _validate_resource_configs,\n            _validate_resources,\n            _validate_workflow,\n        )\n\n        step_configs = self._step_configs()\n        result = _validate_workflow(\n            step_configs, self.__class__.__name__, self._skip_graph_checks\n        )\n        self._start_event_class = result.start_event_class\n        self._stop_event_class = result.stop_event_class\n        self._catch_error_handlers = result.catch_error_handlers\n        self._handler_for_step = result.handler_for_step\n\n        if validate_resource_configs:\n            if errors := _validate_resource_configs(step_configs):\n                raise WorkflowValidationError(\n                    \"Resource config validation failed:\\n\"\n                    + \"\\n\".join(f\"  - {e}\" for e in errors)\n                )\n\n        if validate_resources:\n            errors = asyncio.run(\n                _validate_resources(step_configs, self._resource_manager)\n            )\n            if errors:\n                raise WorkflowValidationError(\n                    \"Resource validation failed:\\n\"\n                    + \"\\n\".join(f\"  - {e}\" for e in errors)\n                )\n\n        self._validation_result = result.uses_hitl\n        self._validated_version = self.__class__._step_functions_version\n        return self._validation_result\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/conftest.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom typing import Any, AsyncGenerator\n\nimport pytest\nfrom pydantic import Field\nfrom workflows.context import Context\nfrom workflows.context.external_context import ExternalContext\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.plugins.basic import (\n    BasicRuntime,\n)\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.workflow import Workflow\n\n\nclass OneTestEvent(Event):\n    test_param: str = Field(default=\"test\")\n\n\nclass AnotherTestEvent(Event):\n    another_test_param: str = Field(default=\"another_test\")\n\n\nclass LastEvent(Event):\n    pass\n\n\nclass DummyWorkflow(Workflow):\n    @step\n    async def start_step(self, ev: StartEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    @step\n    async def middle_step(self, ev: OneTestEvent) -> LastEvent:\n        return LastEvent()\n\n    @step\n    async def end_step(self, ev: LastEvent) -> StopEvent:\n        return StopEvent(result=\"Workflow completed\")\n\n\n@pytest.fixture()\ndef workflow() -> Workflow:\n    return DummyWorkflow()\n\n\n@pytest.fixture()\ndef events() -> list:\n    return [OneTestEvent, AnotherTestEvent]\n\n\n@pytest.fixture()\nasync def ctx(workflow: Workflow) -> AsyncGenerator[Context[Any], None]:\n    runtime = BasicRuntime()\n\n    _ = runtime._get_or_create_queues(\n        run_id=\"test-run\",\n        init_state=BrokerState.from_workflow(workflow),\n    )\n    ctx = Context._create_external(\n        workflow=workflow,\n        external_adapter=runtime.get_external_adapter(\"test-run\"),\n    )\n    assert isinstance(ctx._face, ExternalContext)\n    try:\n        yield ctx\n    finally:\n        await ctx._face.shutdown()\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/context/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-index-workflows/tests/context/test_context.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport gc\nimport json\nimport weakref\nfrom typing import cast\n\nimport pytest\nfrom pydantic import BaseModel\nfrom workflows.context import Context\nfrom workflows.context.context import (\n    _warn_cancel_before_start,\n    _warn_cancel_in_step,\n    _warn_get_result,\n    _warn_is_running_in_step,\n)\nfrom workflows.context.external_context import ExternalContext\nfrom workflows.context.internal_context import InternalContext\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.context.state_store import (\n    DictState,\n    InMemoryStateStore,\n    deserialize_state_from_dict,\n)\nfrom workflows.decorators import step\nfrom workflows.errors import ContextStateError, WorkflowRuntimeError\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.plugins.basic import (\n    AsyncioAdapterQueues,\n    BasicRuntime,\n    setting_run_id,\n)\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.ticks import TickAddEvent\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.workflow import Workflow\n\nfrom ..conftest import (  # type: ignore[import]\n    AnotherTestEvent,\n    LastEvent,\n    OneTestEvent,\n)\n\n\n@pytest.fixture()\ndef internal_ctx(workflow: Workflow) -> Context:\n    \"\"\"Create a context directly in internal face for testing store operations.\"\"\"\n    # Set up a runtime with state store for this workflow\n    runtime = BasicRuntime()\n    run_id = \"test-run\"\n    init_state = BrokerState.from_workflow(workflow)\n    # Create queues with state store so get_internal_adapter() returns adapter with store\n    queues = AsyncioAdapterQueues(\n        run_id=run_id,\n        init_state=init_state,\n        state_store=InMemoryStateStore(DictState()),\n    )\n    runtime._queues[run_id] = queues\n    workflow._runtime = runtime\n    with setting_run_id(run_id):\n        return Context._create_internal(workflow=workflow)\n\n\n@pytest.mark.asyncio\nasync def test_collect_events() -> None:\n    ev1 = OneTestEvent()\n    ev2 = AnotherTestEvent()\n\n    class TestWorkflow(Workflow):\n        @step\n        async def step1(self, _: StartEvent) -> OneTestEvent:\n            return ev1\n\n        @step\n        async def step2(self, _: StartEvent) -> AnotherTestEvent:\n            return ev2\n\n        @step\n        async def step3(\n            self, ctx: Context, ev: OneTestEvent | AnotherTestEvent\n        ) -> StopEvent | None:\n            events = ctx.collect_events(ev, [OneTestEvent, AnotherTestEvent])\n            if events is None:\n                return None\n            return StopEvent(result=events)\n\n    r = await WorkflowTestRunner(TestWorkflow()).run()\n    assert r.result == [ev1, ev2]\n\n\n@pytest.mark.asyncio\nasync def test_collect_events_empty_expected_list() -> None:\n    \"\"\"\n    Test that collect_events returns an empty list (not None) when the\n    expected list is empty. This edge case should immediately return []\n    since there are no events to collect.\n    \"\"\"\n\n    class TestWorkflow(Workflow):\n        @step\n        async def start_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            # Pass an empty list of expected events\n            events = ctx.collect_events(ev, [])\n            # Should return an empty list, not None\n            return StopEvent(result=events)\n\n    r = await WorkflowTestRunner(TestWorkflow()).run()\n    assert r.result == []\n\n\n@pytest.mark.asyncio\nasync def test_collect_events_with_extra_event_type() -> None:\n    \"\"\"\n    Test that collect_events properly handles when an event of a different type\n    arrives first, before the expected events.\n\n    This validates that when collect_events is called with an event that's NOT\n    in the expected types list, it returns None and waits for matching events.\n    \"\"\"\n\n    class TestWorkflow(Workflow):\n        @step\n        async def start_step(\n            self, ctx: Context, ev: StartEvent\n        ) -> OneTestEvent | AnotherTestEvent | LastEvent:\n            await ctx.store.set(\"num_to_collect\", 2)\n            await ctx.store.set(\"calls\", 0)\n            # Send a LastEvent first (not in the expected collection types)\n            ctx.send_event(LastEvent())\n            # Then send the events we want to collect\n            ctx.send_event(OneTestEvent(test_param=\"first\"))\n            ctx.send_event(AnotherTestEvent(another_test_param=\"second\"))\n            return None  # type: ignore\n\n        @step\n        async def collector(\n            self, ctx: Context, ev: OneTestEvent | AnotherTestEvent | LastEvent\n        ) -> StopEvent | None:\n            # Track how many times this step is called\n            calls = await ctx.store.get(\"calls\")\n            await ctx.store.set(\"calls\", calls + 1)\n\n            # Try to collect OneTestEvent and AnotherTestEvent\n            # LastEvent is NOT in this list\n            events = ctx.collect_events(ev, [OneTestEvent, AnotherTestEvent])\n            if events is None:\n                # This happens when we receive LastEvent or haven't received all events yet\n                return None\n\n            # Verify we got the right events\n            assert len(events) == 2\n            assert isinstance(events[0], OneTestEvent)\n            assert isinstance(events[1], AnotherTestEvent)\n            assert events[0].test_param == \"first\"\n            assert events[1].another_test_param == \"second\"\n\n            return StopEvent(result=\"collected\")\n\n    r = await WorkflowTestRunner(TestWorkflow()).run()\n    assert r.result == \"collected\"\n\n    # Verify the collector was called multiple times (once for each event)\n    ctx = r.ctx\n    assert ctx is not None\n    ctx_dict = ctx.to_dict()\n    # State is serialized as JSON strings under state_data._data\n    calls = json.loads(ctx_dict[\"state\"][\"state_data\"][\"_data\"][\"calls\"])\n    # Should be called at least 3 times: once for LastEvent (returns None),\n    # once for OneTestEvent (returns None), once for AnotherTestEvent (returns result)\n    assert calls >= 3\n\n\n@pytest.mark.asyncio\nasync def test_get_default(internal_ctx: Context) -> None:\n    assert await internal_ctx.store.get(\"test_key\", default=42) == 42\n\n\n@pytest.mark.asyncio\nasync def test_get(internal_ctx: Context) -> None:\n    await internal_ctx.store.set(\"foo\", 42)\n    assert await internal_ctx.store.get(\"foo\") == 42\n\n\n@pytest.mark.asyncio\nasync def test_get_not_found(internal_ctx: Context) -> None:\n    with pytest.raises(ValueError):\n        await internal_ctx.store.get(\"foo\")\n\n\n@pytest.mark.asyncio\nasync def test_send_event_step_is_none() -> None:\n    \"\"\"Test that external events create TickAddEvent with step_name=None.\n\n    Uses a workflow that waits for the external event so we can verify\n    the tick is logged before the workflow completes.\n    \"\"\"\n    ev = Event(foo=\"bar\")\n\n    class WaitingWorkflow(Workflow):\n        @step\n        async def wait_for_external(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            # Wait for an external Event to arrive\n            result = await ctx.wait_for_event(Event, requirements={\"foo\": \"bar\"})\n            return StopEvent(result=result.foo)\n\n    wf = WaitingWorkflow()\n    handler = wf.run()\n    try:\n        # Send the external event\n        handler.ctx.send_event(ev)\n        external_face = handler.ctx._face\n        assert isinstance(external_face, ExternalContext)\n\n        # Wait for event to appear in tick log (up to 1 second)\n        expected_tick = TickAddEvent(event=ev, step_name=None)\n        for _ in range(100):\n            if expected_tick in external_face._tick_log:\n                break\n            await asyncio.sleep(0.01)\n\n        assert expected_tick in external_face._tick_log\n\n        # Let workflow complete\n        result = await handler\n        assert result == \"bar\"\n    finally:\n        external_face = handler.ctx._face\n        assert isinstance(external_face, ExternalContext)\n        await external_face.shutdown()\n\n\n@pytest.mark.asyncio\nasync def test_send_event_to_non_existent_step(ctx: Context) -> None:\n    with pytest.raises(\n        WorkflowRuntimeError, match=\"Step does_not_exist does not exist\"\n    ):\n        ctx.send_event(Event(), \"does_not_exist\")\n\n\n@pytest.mark.asyncio\nasync def test_send_event_to_wrong_step(ctx: Context) -> None:\n    with pytest.raises(\n        WorkflowRuntimeError,\n        match=\"Step middle_step does not accept event of type <class 'workflows.events.Event'>\",\n    ):\n        ctx.send_event(Event(), \"middle_step\")\n\n\n@pytest.mark.asyncio\nasync def test_empty_inprogress_when_workflow_done(workflow: Workflow) -> None:\n    result = await WorkflowTestRunner(workflow).run()\n    ctx = result.ctx\n\n    # there shouldn't be any in progress events\n    assert ctx is not None\n    assert isinstance(ctx._face, ExternalContext)\n    # After workflow completion, in_progress should be empty for all steps\n    state = ctx._face._state\n    for step_name, worker_state in state.workers.items():\n        assert len(worker_state.in_progress) == 0, (\n            f\"Step {step_name} has {len(worker_state.in_progress)} in-progress events\"\n        )\n\n\n@pytest.mark.asyncio\nasync def test_wait_for_event_in_workflow() -> None:\n    class TestWorkflow(Workflow):\n        @step\n        async def step1(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            result = await ctx.wait_for_event(\n                Event,\n                waiter_event=Event(msg=\"foo\"),\n                waiter_id=\"test_id\",\n            )\n            return StopEvent(result=result.msg)\n\n    workflow = TestWorkflow()\n    handler = workflow.run()\n    assert handler.ctx\n    async for ev in handler.stream_events():\n        if isinstance(ev, Event) and ev.msg == \"foo\":\n            handler.ctx.send_event(Event(msg=\"bar\"))\n            break\n\n    result = await handler\n    assert result == \"bar\"\n\n\nclass CustomState(BaseModel):\n    pass\n\n\n@pytest.mark.asyncio\nasync def test_wait_for_event_in_workflow_serialization() -> None:\n    \"\"\"Ensure hitl works with serialization and custom state.\"\"\"\n\n    class TestWorkflow(Workflow):\n        @step\n        async def step1(self, ctx: Context[CustomState], ev: StartEvent) -> StopEvent:\n            result = await ctx.wait_for_event(\n                Event,\n                waiter_event=Event(msg=\"foo\"),\n                waiter_id=\"test_id\",\n            )\n            return StopEvent(result=result.msg)\n\n    workflow = TestWorkflow()\n    handler = workflow.run()\n    ctx_dict = None\n\n    assert handler.ctx\n    async for ev in handler.stream_events():\n        if isinstance(ev, Event) and ev.msg == \"foo\":\n            ctx_dict = handler.ctx.to_dict()\n            # Check that at least one worker has waiters\n            assert ctx_dict[\"version\"] == 1\n            total_waiters = sum(\n                len(worker_data[\"collected_waiters\"])\n                for worker_data in ctx_dict[\"workers\"].values()\n            )\n            assert total_waiters == 1\n            await handler.cancel_run()\n            break\n\n    # Roundtrip the context\n    assert ctx_dict is not None\n    # verify creating a new context has the correct state\n    new_ctx = Context.from_dict(workflow, ctx_dict)\n    new_handler = workflow.run(ctx=new_ctx)\n    assert isinstance(new_handler.ctx._face, ExternalContext)\n    # Check that the waiters are properly restored\n    state = new_handler.ctx._face._state\n    total_waiters = sum(\n        len(worker.collected_waiters) for worker in state.workers.values()\n    )\n    assert total_waiters == 1\n\n    # Continue the workflow\n    assert new_handler.ctx\n    new_handler.ctx.send_event(Event(msg=\"bar\"))\n    result = await new_handler\n    assert result == \"bar\"\n    assert isinstance(new_handler.ctx._face, ExternalContext)\n    # After workflow completion, there should be no more waiters\n    state = new_handler.ctx._face._state\n    total_waiters = sum(\n        len(worker.collected_waiters) for worker in state.workers.values()\n    )\n    assert total_waiters == 0\n\n\nclass Waiter1(Event):\n    msg: str\n\n\nclass Waiter2(Event):\n    msg: str\n\n\nclass ResultEvent(Event):\n    result: str\n\n\nclass WaitingWorkflow(Workflow):\n    @step\n    async def spawn_waiters(self, ctx: Context, ev: StartEvent) -> Waiter1 | Waiter2:\n        ctx.send_event(Waiter1(msg=\"foo\"))\n        ctx.send_event(Waiter2(msg=\"bar\"))\n        return None  # type: ignore\n\n    @step\n    async def waiter_one(self, ctx: Context, ev: Waiter1) -> ResultEvent:\n        ctx.write_event_to_stream(InputRequiredEvent(prefix=\"waiter_one\"))  # type: ignore\n\n        new_ev: HumanResponseEvent = await ctx.wait_for_event(\n            HumanResponseEvent,\n            requirements={\"waiter_id\": \"waiter_one\"},\n        )\n        return ResultEvent(result=new_ev.response)\n\n    @step\n    async def waiter_two(self, ctx: Context, ev: Waiter2) -> ResultEvent:\n        ctx.write_event_to_stream(InputRequiredEvent(prefix=\"waiter_two\"))  # type: ignore\n\n        new_ev: HumanResponseEvent = await ctx.wait_for_event(\n            HumanResponseEvent,\n            requirements={\"waiter_id\": \"waiter_two\"},\n        )\n        return ResultEvent(result=new_ev.response)\n\n    @step\n    async def collect_waiters(self, ctx: Context, ev: ResultEvent) -> StopEvent:\n        events: list[ResultEvent] | None = ctx.collect_events(  # type: ignore\n            ev, [ResultEvent, ResultEvent]\n        )\n        if events is None:\n            return None  # type: ignore\n\n        return StopEvent(result=[e.result for e in events])\n\n\n@pytest.mark.asyncio\nasync def test_wait_for_multiple_events_in_workflow() -> None:\n    workflow = WaitingWorkflow()\n    handler = workflow.run()\n    assert handler.ctx\n\n    async for ev in handler.stream_events():\n        if isinstance(ev, InputRequiredEvent) and ev.prefix == \"waiter_one\":\n            handler.ctx.send_event(\n                HumanResponseEvent(response=\"foo\", waiter_id=\"waiter_one\")  # type: ignore\n            )\n        elif isinstance(ev, InputRequiredEvent) and ev.prefix == \"waiter_two\":\n            handler.ctx.send_event(\n                HumanResponseEvent(response=\"bar\", waiter_id=\"waiter_two\")  # type: ignore\n            )\n\n    result = await handler\n    # Order is non-deterministic since waiters run concurrently\n    assert sorted(result) == [\"bar\", \"foo\"]\n    assert not handler.ctx.is_running\n\n    # serialize and resume\n    ctx_dict = handler.ctx.to_dict()\n    ctx = Context.from_dict(workflow, ctx_dict)\n    handler = workflow.run(ctx=ctx)\n    assert handler.ctx\n\n    async for ev in handler.stream_events():\n        if isinstance(ev, InputRequiredEvent) and ev.prefix == \"waiter_one\":\n            handler.ctx.send_event(\n                HumanResponseEvent(response=\"fizz\", waiter_id=\"waiter_one\")  # type: ignore\n            )\n        elif isinstance(ev, InputRequiredEvent) and ev.prefix == \"waiter_two\":\n            handler.ctx.send_event(\n                HumanResponseEvent(response=\"buzz\", waiter_id=\"waiter_two\")  # type: ignore\n            )\n\n    result = await handler\n    # Order is non-deterministic since waiters run concurrently\n    assert sorted(result) == [\"buzz\", \"fizz\"]\n\n\n@pytest.mark.asyncio\nasync def test_clear(internal_ctx: Context) -> None:\n    await internal_ctx.store.set(\"test_key\", 42)\n    await internal_ctx.store.clear()\n    res = await internal_ctx.store.get(\"test_key\", default=None)\n    assert res is None\n\n\n@pytest.mark.asyncio\nasync def test_running_steps_before_run_raises(workflow: Workflow) -> None:\n    \"\"\"Calling running_steps() before workflow.run() should raise ContextStateError.\"\"\"\n    ctx = Context(workflow)\n    with pytest.raises(ContextStateError, match=\"requires a running workflow\"):\n        await ctx.running_steps()\n\n\n@pytest.mark.asyncio\nasync def test_store_access_outside_step_works() -> None:\n    \"\"\"Accessing ctx.store from handler code (outside step) should work.\"\"\"\n\n    class SimpleWorkflow(Workflow):\n        @step\n        async def only(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = SimpleWorkflow()\n    handler = wf.run()\n\n    # Access store from external context (handler code) should work\n    store = handler.ctx.store\n    assert isinstance(store, InMemoryStateStore)\n\n    # Verify reads and writes work\n    await store.set(\"key\", \"value\")\n    assert await store.get(\"key\") == \"value\"\n\n    await handler\n\n\n@pytest.mark.asyncio\nasync def test_store_access_before_run_works() -> None:\n    \"\"\"Accessing ctx.store before workflow.run() should return a staging store.\"\"\"\n\n    class SimpleWorkflow(Workflow):\n        @step\n        async def only(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = SimpleWorkflow()\n    ctx = Context(wf)\n\n    store = ctx.store\n    assert isinstance(store, InMemoryStateStore)\n    await store.set(\"key\", \"value\")\n    assert await store.get(\"key\") == \"value\"\n\n\n@pytest.mark.asyncio\nasync def test_store_seed_before_run_visible_in_step() -> None:\n    \"\"\"State seeded via ctx.store before run should be visible inside steps.\"\"\"\n\n    class SeededWorkflow(Workflow):\n        @step\n        async def read_seed(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            val = await ctx.store.get(\"seeded_key\")\n            return StopEvent(result=val)\n\n    wf = SeededWorkflow()\n    ctx = Context(wf)\n    await ctx.store.set(\"seeded_key\", \"hello\")\n\n    result = await wf.run(ctx=ctx)\n    assert result == \"hello\"\n\n\n@pytest.mark.asyncio\nasync def test_store_seed_on_deserialized_context() -> None:\n    \"\"\"Seeding state on a deserialized context should merge with existing state.\"\"\"\n\n    class StatefulWorkflow(Workflow):\n        @step\n        async def first_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            # Just pass through; state is set externally\n            return StopEvent(result=\"done\")\n\n    class ReadWorkflow(Workflow):\n        @step\n        async def check(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            old = await ctx.store.get(\"existing\")\n            new = await ctx.store.get(\"added\")\n            return StopEvent(result=f\"{old}-{new}\")\n\n    wf = StatefulWorkflow()\n\n    # First run to create some state\n    ctx = Context(wf)\n    await ctx.store.set(\"existing\", \"orig\")\n    handler = wf.run(ctx=ctx)\n    await handler\n\n    # Serialize and restore (use ReadWorkflow for second run)\n    ctx_dict = ctx.to_dict()\n    read_wf = ReadWorkflow()\n    restored_ctx = Context.from_dict(read_wf, ctx_dict)\n\n    # Seed additional state before next run\n    await restored_ctx.store.set(\"added\", \"new\")\n\n    result = await read_wf.run(ctx=restored_ctx)\n    assert result == \"orig-new\"\n\n\n@pytest.mark.asyncio\nasync def test_store_continuation_with_pre_run_seeding() -> None:\n    \"\"\"Continuation runs with pre-run seeding should carry state through.\"\"\"\n\n    class CountWorkflow(Workflow):\n        @step\n        async def increment(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            count = await ctx.store.get(\"count\", default=0)\n            count += 1\n            await ctx.store.set(\"count\", count)\n            return StopEvent(result=count)\n\n    wf = CountWorkflow()\n    ctx = Context(wf)\n\n    # Seed initial count\n    await ctx.store.set(\"count\", 10)\n\n    # First run: 10 + 1 = 11\n    result = await wf.run(ctx=ctx)\n    assert result == 11\n\n    # Second run (continuation): 11 + 1 = 12\n    result = await wf.run(ctx=ctx)\n    assert result == 12\n\n\n@pytest.mark.asyncio\nasync def test_to_dict_before_run_raises(workflow: Workflow) -> None:\n    \"\"\"Calling to_dict() before workflow.run() should raise ContextStateError.\"\"\"\n    ctx = Context(workflow)\n    with pytest.raises(ContextStateError, match=\"requires a running workflow\"):\n        ctx.to_dict()\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_before_run_raises(workflow: Workflow) -> None:\n    \"\"\"Calling stream_events() before workflow.run() should raise ContextStateError.\"\"\"\n    ctx = Context(workflow)\n    with pytest.raises(ContextStateError, match=\"requires a running workflow\"):\n        ctx.stream_events()\n\n\n# ============================================================================\n# Warning Tests\n# ============================================================================\n\n\n@pytest.mark.asyncio\nasync def test_cancel_before_start_warns(workflow: Workflow) -> None:\n    \"\"\"Calling cancel before run() should emit warning.\"\"\"\n    # Clear the lru_cache to ensure warning fires\n    _warn_cancel_before_start.cache_clear()\n\n    ctx = Context(workflow)\n    with pytest.warns(UserWarning, match=\"cancel.*called before workflow started\"):\n        ctx._workflow_cancel_run()\n\n\n@pytest.mark.asyncio\nasync def test_send_event_before_start_raises(workflow: Workflow) -> None:\n    \"\"\"Sending event before run() should raise ContextStateError.\"\"\"\n    ctx = Context(workflow)\n    with pytest.raises(\n        ContextStateError, match=\"send_event.*called before workflow started\"\n    ):\n        ctx.send_event(Event())\n\n\n@pytest.mark.asyncio\nasync def test_is_running_in_step_warns() -> None:\n    \"\"\"Calling is_running from within a step should emit deprecation warning.\"\"\"\n    # Clear the lru_cache to ensure warning fires\n    _warn_is_running_in_step.cache_clear()\n\n    is_running_value = None\n\n    class TestWorkflow(Workflow):\n        @step\n        async def check_running(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            nonlocal is_running_value\n            is_running_value = ctx.is_running\n            return StopEvent(result=\"done\")\n\n    wf = TestWorkflow()\n    with pytest.warns(DeprecationWarning, match=\"is_running called from within a step\"):\n        await wf.run()\n\n    # Should still return True despite the warning\n    assert is_running_value is True\n\n\n@pytest.mark.asyncio\nasync def test_cancel_in_step_warns() -> None:\n    \"\"\"Calling cancel from within a step should emit warning.\"\"\"\n    # Clear the lru_cache to ensure warning fires\n    _warn_cancel_in_step.cache_clear()\n\n    class TestWorkflow(Workflow):\n        @step\n        async def cancel_self(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            ctx._workflow_cancel_run()\n            return StopEvent(result=\"done\")\n\n    wf = TestWorkflow()\n    with pytest.warns(UserWarning, match=\"cancel.*called from within a step\"):\n        await wf.run()\n\n\n@pytest.mark.asyncio\nasync def test_get_result_before_complete_raises() -> None:\n    \"\"\"Calling get_result() while workflow still running should raise WorkflowRuntimeError.\"\"\"\n    # Clear the lru_cache to ensure deprecation warning fires\n    _warn_get_result.cache_clear()\n\n    step_started = asyncio.Event()\n    step_continue = asyncio.Event()\n\n    class SlowWorkflow(Workflow):\n        @step\n        async def slow(self, ev: StartEvent) -> StopEvent:\n            step_started.set()\n            await step_continue.wait()\n            return StopEvent(result=\"done\")\n\n    wf = SlowWorkflow()\n    handler = wf.run()\n\n    # Wait for step to start\n    await step_started.wait()\n\n    # Try to get result before workflow completes - should raise\n    with pytest.warns(DeprecationWarning):  # get_result is deprecated\n        with pytest.raises(WorkflowRuntimeError, match=\"is not complete\"):\n            handler.ctx.get_result()\n\n    # Let workflow complete\n    step_continue.set()\n    await handler\n\n\n@pytest.mark.asyncio\nasync def test_get_result_pre_context_raises(workflow: Workflow) -> None:\n    \"\"\"Calling get_result() before run() should raise ContextStateError.\"\"\"\n    # Clear the lru_cache to ensure deprecation warning fires\n    _warn_get_result.cache_clear()\n\n    ctx = Context(workflow)\n\n    with pytest.warns(DeprecationWarning):  # get_result is deprecated\n        with pytest.raises(ContextStateError, match=\"requires a running workflow\"):\n            ctx.get_result()\n\n\n# ============================================================================\n# deserialize_state_from_dict Tests\n# ============================================================================\n\n\nclass TypedTestState(BaseModel):\n    \"\"\"Typed state for deserialize_state_from_dict testing.\"\"\"\n\n    counter: int = 0\n    name: str = \"default\"\n\n\n@pytest.mark.asyncio\nasync def test_deserialize_state_from_dict_with_dict_state() -> None:\n    \"\"\"Test deserializing DictState from to_dict() format.\"\"\"\n    serializer = JsonSerializer()\n\n    # Create state and serialize it\n    store = InMemoryStateStore(DictState())\n    await store.set(\"counter\", 42)\n    await store.set(\"name\", \"test-value\")\n    serialized = store.to_dict(serializer)\n\n    # Deserialize\n    result = deserialize_state_from_dict(serialized, serializer)\n\n    assert isinstance(result, DictState)\n    assert result[\"counter\"] == 42\n    assert result[\"name\"] == \"test-value\"\n\n\ndef test_deserialize_state_from_dict_with_typed_state() -> None:\n    \"\"\"Test deserializing typed Pydantic model from to_dict() format.\"\"\"\n    serializer = JsonSerializer()\n\n    # Create typed state and serialize it\n    initial = TypedTestState(counter=100, name=\"typed-test\")\n    store = InMemoryStateStore(initial)\n    serialized = store.to_dict(serializer)\n\n    # Deserialize\n    result = deserialize_state_from_dict(serialized, serializer)\n\n    assert isinstance(result, TypedTestState)\n    assert result.counter == 100\n    assert result.name == \"typed-test\"\n\n\ndef test_deserialize_state_from_dict_empty_dict_state() -> None:\n    \"\"\"Test deserializing empty DictState.\"\"\"\n    serializer = JsonSerializer()\n\n    serialized = {\n        \"state_data\": {\"_data\": {}},\n        \"state_type\": \"DictState\",\n        \"state_module\": \"workflows.context.state_store\",\n    }\n\n    result = deserialize_state_from_dict(serialized, serializer)\n\n    assert isinstance(result, DictState)\n    assert len(list(result.items())) == 0\n\n\n# ============================================================================\n# Context.get_step_context() Tests\n# ============================================================================\n\n\n@pytest.mark.asyncio\nasync def test_get_step_context_outside_step_raises() -> None:\n    \"\"\"Calling Context.get_step_context() outside a step should raise WorkflowRuntimeError.\"\"\"\n    with pytest.raises(WorkflowRuntimeError, match=\"may only be called from within\"):\n        Context.get_step_context()\n\n\n@pytest.mark.asyncio\nasync def test_get_step_context_inside_step() -> None:\n    \"\"\"Context.get_step_context() should return the step's context inside a step.\"\"\"\n    captured_ctx = None\n\n    class TestWorkflow(Workflow):\n        @step\n        async def my_step(self, ev: StartEvent) -> StopEvent:\n            nonlocal captured_ctx\n            captured_ctx = Context.get_step_context()\n            return StopEvent(result=\"done\")\n\n    wf = TestWorkflow()\n    result = await wf.run()\n    assert result == \"done\"\n    assert captured_ctx is not None\n    # The returned context should be in internal face state\n    assert isinstance(captured_ctx._face, InternalContext)\n\n\n@pytest.mark.asyncio\nasync def test_get_step_context_matches_ctx_parameter() -> None:\n    \"\"\"Context.get_step_context() should return the same Context as the ctx parameter.\"\"\"\n    ctx_from_param = None\n    ctx_from_get_step_context = None\n\n    class TestWorkflow(Workflow):\n        @step\n        async def my_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            nonlocal ctx_from_param, ctx_from_get_step_context\n            ctx_from_param = ctx\n            ctx_from_get_step_context = Context.get_step_context()\n            return StopEvent(result=\"done\")\n\n    wf = TestWorkflow()\n    await wf.run()\n    assert ctx_from_param is ctx_from_get_step_context\n\n\n@pytest.mark.asyncio\nasync def test_get_step_context_supports_wait_for_event() -> None:\n    \"\"\"Context.get_step_context() should return a context that supports wait_for_event.\"\"\"\n\n    class ResumeEvent(Event):\n        value: str = \"resumed\"\n\n    class TestWorkflow(Workflow):\n        @step\n        async def waiting_step(self, ev: StartEvent) -> StopEvent:\n            ctx = Context.get_step_context()\n            result = await ctx.wait_for_event(\n                ResumeEvent,\n                waiter_event=InputRequiredEvent(),\n            )\n            return StopEvent(result=result.value)\n\n    wf = TestWorkflow()\n    handler = wf.run()\n\n    async for ev in handler.stream_events():\n        if isinstance(ev, InputRequiredEvent):\n            handler.ctx.send_event(ResumeEvent(value=\"hello\"))\n\n    result = await handler\n    assert result == \"hello\"\n\n\n@pytest.mark.asyncio\nasync def test_get_step_context_does_not_pin_workflow() -> None:\n    \"\"\"InternalContextVar should not pin the Workflow via timer handle context snapshots.\"\"\"\n    handles: list[asyncio.TimerHandle] = []\n\n    class TinyWorkflow(Workflow):\n        @step\n        async def only(self, ev: StartEvent) -> StopEvent:\n            ctx = Context.get_step_context()\n            assert ctx is not None\n            # Schedule a long-lived timer that snapshots the current ContextVars\n            handles.append(asyncio.get_running_loop().call_later(3600, lambda: None))\n            return StopEvent(result=\"done\")\n\n    refs: list[weakref.ReferenceType[Workflow]] = []\n    try:\n        for _ in range(5):\n            wf = TinyWorkflow()\n            refs.append(cast(weakref.ReferenceType[Workflow], weakref.ref(wf)))\n            await WorkflowTestRunner(wf).run()\n            del wf\n\n        for _ in range(3):\n            gc.collect()\n\n        assert all(r() is None for r in refs), (\n            f\"{sum(r() is not None for r in refs)} workflows pinned by \"\n            \"InternalContextVar in TimerHandle context\"\n        )\n    finally:\n        for h in handles:\n            h.cancel()\n\n\ndef test_deserialize_state_from_dict_defaults_to_dict_state() -> None:\n    \"\"\"Test that missing state_type defaults to DictState.\"\"\"\n    serializer = JsonSerializer()\n\n    serialized = {\"state_data\": {\"_data\": {}}}\n\n    result = deserialize_state_from_dict(serialized, serializer)\n\n    assert isinstance(result, DictState)\n\n\n# ============================================================================\n# Serialized State Format Tests (parse_in_memory_state)\n# ============================================================================\n\n\ndef test_parse_in_memory_state_old_format_no_store_type() -> None:\n    \"\"\"Test that old format (no store_type) parses as InMemorySerializedState.\"\"\"\n    from workflows.context.state_store import (\n        InMemorySerializedState,\n        parse_in_memory_state,\n    )\n\n    # Old format without store_type field\n    old_format = {\n        \"state_type\": \"DictState\",\n        \"state_module\": \"workflows.context.state_store\",\n        \"state_data\": {\"_data\": {\"counter\": 42}},\n    }\n\n    result = parse_in_memory_state(old_format)\n\n    assert isinstance(result, InMemorySerializedState)\n    assert result.store_type == \"in_memory\"\n    assert result.state_type == \"DictState\"\n    assert result.state_module == \"workflows.context.state_store\"\n    assert result.state_data == {\"_data\": {\"counter\": 42}}\n\n\ndef test_parse_in_memory_state_explicit_in_memory() -> None:\n    \"\"\"Test that explicit store_type='in_memory' parses as InMemorySerializedState.\"\"\"\n    from workflows.context.state_store import (\n        InMemorySerializedState,\n        parse_in_memory_state,\n    )\n\n    serialized = {\n        \"store_type\": \"in_memory\",\n        \"state_type\": \"CustomState\",\n        \"state_module\": \"myapp.models\",\n        \"state_data\": {\"name\": \"test\", \"value\": 123},\n    }\n\n    result = parse_in_memory_state(serialized)\n\n    assert isinstance(result, InMemorySerializedState)\n    assert result.store_type == \"in_memory\"\n    assert result.state_type == \"CustomState\"\n    assert result.state_module == \"myapp.models\"\n    assert result.state_data == {\"name\": \"test\", \"value\": 123}\n\n\ndef test_parse_in_memory_state_rejects_sql_store_type() -> None:\n    \"\"\"Test that store_type='sql' raises ValueError.\"\"\"\n    from workflows.context.state_store import parse_in_memory_state\n\n    serialized = {\n        \"store_type\": \"sql\",\n        \"run_id\": \"run-12345\",\n        \"state_type\": \"WorkflowState\",\n        \"state_module\": \"myapp.states\",\n        \"schema\": \"public\",\n    }\n\n    with pytest.raises(ValueError, match=\"Cannot parse store_type 'sql'\"):\n        parse_in_memory_state(serialized)\n\n\ndef test_parse_in_memory_state_unknown_store_type_raises() -> None:\n    \"\"\"Test that unknown store_type raises ValueError.\"\"\"\n    from workflows.context.state_store import parse_in_memory_state\n\n    serialized = {\n        \"store_type\": \"redis\",  # Unknown store type\n        \"state_type\": \"SomeState\",\n        \"state_module\": \"some.module\",\n    }\n\n    with pytest.raises(ValueError, match=\"Cannot parse store_type 'redis'\"):\n        parse_in_memory_state(serialized)\n\n\n# ============================================================================\n# InMemoryStateStore Serialization Tests\n# ============================================================================\n\n\ndef test_in_memory_state_store_to_dict_includes_store_type() -> None:\n    \"\"\"Test that to_dict() includes store_type='in_memory'.\"\"\"\n    store = InMemoryStateStore(DictState())\n    serializer = JsonSerializer()\n\n    result = store.to_dict(serializer)\n\n    assert result[\"store_type\"] == \"in_memory\"\n    assert \"state_type\" in result\n    assert \"state_module\" in result\n    assert \"state_data\" in result\n\n\ndef test_in_memory_state_store_from_dict_rejects_sql_format() -> None:\n    \"\"\"Test that from_dict() rejects SQL format with clear error.\"\"\"\n    sql_format = {\n        \"store_type\": \"sql\",\n        \"run_id\": \"run-12345\",\n        \"state_type\": \"DictState\",\n        \"state_module\": \"workflows.context.state_store\",\n    }\n    serializer = JsonSerializer()\n\n    with pytest.raises(ValueError, match=\"Cannot parse store_type 'sql'\"):\n        InMemoryStateStore.from_dict(sql_format, serializer)\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/context/test_context_preservation.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"Tests for context state preservation when passing ctx to workflow.run().\n\nThese tests verify that when a Context is passed to Workflow.run(ctx=ctx),\nthe original context object is updated with run state and can be used for\nsubsequent operations.\n\nSee: thoughts/shared/plans/2026-01-23-context-state-preservation-fix.md\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\nimport pytest\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.errors import ContextStateError, WorkflowRuntimeError\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\nclass CounterWorkflow(Workflow):\n    \"\"\"Simple workflow that increments a counter in state.\"\"\"\n\n    @step\n    async def count(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        count = await ctx.store.get(\"count\", default=0)\n        count += 1\n        await ctx.store.set(\"count\", count)\n        return StopEvent(result=count)\n\n\n@pytest.mark.asyncio\nasync def test_original_ctx_is_handler_ctx() -> None:\n    \"\"\"ctx passed to run() should be the same object as handler.ctx\"\"\"\n    wf = CounterWorkflow()\n    ctx = Context(wf)\n    handler = wf.run(ctx=ctx)\n    await handler\n    assert ctx is handler.ctx\n\n\n@pytest.mark.asyncio\nasync def test_original_ctx_to_dict_works() -> None:\n    \"\"\"ctx.to_dict() should work after run (not just handler.ctx.to_dict())\"\"\"\n    wf = CounterWorkflow()\n    ctx = Context(wf)\n    handler = wf.run(ctx=ctx)\n    await handler\n    ctx_dict = ctx.to_dict()  # Should NOT raise\n    assert \"state\" in ctx_dict\n\n\n@pytest.mark.asyncio\nasync def test_sequential_runs_accumulate_state() -> None:\n    \"\"\"Three sequential runs should produce 1, 2, 3\"\"\"\n    wf = CounterWorkflow()\n    ctx = Context(wf)\n    r1 = await wf.run(ctx=ctx)  # count: 0 -> 1\n    r2 = await wf.run(ctx=ctx)  # count: 1 -> 2\n    r3 = await wf.run(ctx=ctx)  # count: 2 -> 3\n    assert (r1, r2, r3) == (1, 2, 3)\n\n\n@pytest.mark.asyncio\nasync def test_concurrent_runs_same_context_raises() -> None:\n    \"\"\"Starting a second run while first is running should raise\"\"\"\n\n    class SlowWorkflow(Workflow):\n        @step\n        async def slow_step(self, ev: StartEvent) -> StopEvent:\n            await asyncio.sleep(0.1)\n            return StopEvent(result=\"done\")\n\n    wf = SlowWorkflow()\n    ctx = Context(wf)\n    handler1 = wf.run(ctx=ctx)\n    with pytest.raises(ContextStateError, match=\"already running\"):\n        wf.run(ctx=ctx)\n    await handler1  # Clean up\n\n\n@pytest.mark.asyncio\nasync def test_concurrent_runs_different_contexts_ok() -> None:\n    \"\"\"Different contexts can run concurrently\"\"\"\n\n    class SlowWorkflow(Workflow):\n        @step\n        async def slow_step(self, ev: StartEvent) -> StopEvent:\n            await asyncio.sleep(0.01)\n            return StopEvent(result=\"done\")\n\n    wf = SlowWorkflow()\n    ctx1, ctx2 = Context(wf), Context(wf)\n    h1 = wf.run(ctx=ctx1)\n    h2 = wf.run(ctx=ctx2)  # Should NOT raise\n    await h1\n    await h2\n\n\n@pytest.mark.asyncio\nasync def test_from_dict_then_sequential_runs() -> None:\n    \"\"\"Restored context should work for sequential runs\"\"\"\n    wf = CounterWorkflow()\n    ctx1 = Context(wf)\n    await wf.run(ctx=ctx1)  # count -> 1\n    ctx2 = Context.from_dict(wf, ctx1.to_dict())\n    r = await wf.run(ctx=ctx2)  # count -> 2\n    assert r == 2\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_twice_raises() -> None:\n    \"\"\"Streaming events twice on same handler should raise\"\"\"\n    wf = CounterWorkflow()\n    handler = wf.run()\n\n    async for _ in handler.stream_events():\n        pass\n\n    with pytest.raises(WorkflowRuntimeError, match=\"already been consumed\"):\n        async for _ in handler.stream_events():\n            pass\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/context/test_serializers.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport pytest\nfrom workflows.context import Context\nfrom workflows.errors import ContextSerdeError\nfrom workflows.workflow import Workflow\n\n\ndef test_serialization_roundtrip(ctx: Context, workflow: Workflow) -> None:\n    assert Context.from_dict(workflow, ctx.to_dict())\n\n\ndef test_deserialization_invalid(ctx: Context, workflow: Workflow) -> None:\n    old_payload = {\n        \"globals\": {},\n        \"streaming_queue\": \"[]\",\n        \"queues\": {\"test_id\": \"[]\"},\n        \"events_buffer\": {},\n        \"in_progress\": \"This should be a dict\",\n        \"accepted_events\": [],\n        \"broker_log\": [],\n        \"waiter_id\": \"test_id\",\n        \"is_running\": False,\n    }\n    with pytest.raises(ContextSerdeError):\n        Context.from_dict(workflow, old_payload)\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/context/test_utils.py",
    "content": "import pytest\nfrom workflows.context.utils import (\n    get_qualified_name,\n    import_module_from_qualified_name,\n)\n\n\ndef test_get_qualified_name() -> None:\n    with pytest.raises(\n        AttributeError,\n        match=\"Object foo does not have required attributes: 'str' object has no attribute '__module__'\",\n    ):\n        get_qualified_name(\"foo\")\n\n\ndef test_import_module_from_qualified_name_wrong_name() -> None:\n    with pytest.raises(\n        ValueError, match=\"Qualified name must be in format 'module.attribute'\"\n    ):\n        import_module_from_qualified_name(\"not containing a dot\")\n        import_module_from_qualified_name(\"\")\n\n\ndef test_import_module_from_qualified_name_wrong_package() -> None:\n    with pytest.raises(\n        ImportError,\n        match=\"Failed to import module __doesnt: No module named '__doesnt'\",\n    ):\n        import_module_from_qualified_name(\"__doesnt.exist\")\n\n\ndef test_import_module_from_qualified_name_wrong_module() -> None:\n    with pytest.raises(\n        AttributeError,\n        match=\"Attribute doesntexist not found in module typing: module 'typing' has no attribute 'doesntexist'\",\n    ):\n        import_module_from_qualified_name(\"typing.doesntexist\")\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/conftest.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"Test fixtures and utilities for runtime tests.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport time\nfrom typing import TYPE_CHECKING, Any, AsyncGenerator\n\nimport pytest\nimport time_machine\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.runtime.types.internal_state import BrokerConfig, BrokerState\n\nif TYPE_CHECKING:\n    from workflows.context.serializers import BaseSerializer\n    from workflows.context.state_store import InMemoryStateStore\nfrom workflows.plugins.basic import get_current_run_id\nfrom workflows.runtime.types.plugin import (\n    ExternalRunAdapter,\n    InternalRunAdapter,\n    RegisteredWorkflow,\n    Runtime,\n    SnapshottableAdapter,\n    V2RuntimeCompatibilityShim,\n    WaitResult,\n    WaitResultTick,\n    WaitResultTimeout,\n)\nfrom workflows.runtime.types.step_function import (\n    as_step_worker_functions,\n    create_workflow_run_function,\n)\nfrom workflows.runtime.types.ticks import WorkflowTick\nfrom workflows.workflow import Workflow\n\n\nclass MockRuntime(Runtime):\n    \"\"\"Mock runtime that stores adapters for test access.\"\"\"\n\n    def __init__(self) -> None:\n        self._adapters: dict[str, MockRunAdapter] = {}\n        self._current_run_id: str | None = None\n\n    def register(self, workflow: Workflow) -> RegisteredWorkflow:\n        return RegisteredWorkflow(\n            workflow=workflow,\n            workflow_run_fn=create_workflow_run_function(workflow),\n            steps=as_step_worker_functions(workflow),\n        )\n\n    def get_internal_adapter(self, workflow: Workflow) -> InternalRunAdapter:\n        run_id = get_current_run_id() or self._current_run_id or \"test\"\n        if run_id not in self._adapters:\n            self._adapters[run_id] = MockRunAdapter(run_id)\n        return self._adapters[run_id]\n\n    def get_external_adapter(self, run_id: str) -> ExternalRunAdapter:\n        if run_id not in self._adapters:\n            self._adapters[run_id] = MockRunAdapter(run_id)\n        return self._adapters[run_id]\n\n    def run_workflow(\n        self,\n        run_id: str,\n        workflow: Workflow,\n        init_state: BrokerState,\n        start_event: StartEvent | None = None,\n        serialized_state: dict[str, Any] | None = None,\n        serializer: \"BaseSerializer | None\" = None,\n    ) -> ExternalRunAdapter:\n        self._current_run_id = run_id\n        return self.get_external_adapter(run_id)\n\n    def set_adapter(self, run_id: str, adapter: \"MockRunAdapter\") -> None:\n        \"\"\"Set a specific adapter for a run_id (for test setup).\"\"\"\n        self._adapters[run_id] = adapter\n\n\nclass MockRunAdapter(\n    InternalRunAdapter,\n    ExternalRunAdapter,\n    SnapshottableAdapter,\n    V2RuntimeCompatibilityShim,\n):\n    \"\"\"Mock RunAdapter for testing control loops. Supports snapshot/replay.\"\"\"\n\n    def __init__(\n        self, run_id: str, traveller: time_machine.Coordinates | None = None\n    ) -> None:\n        self._run_id = run_id\n        # Queue for events sent from external sources (e.g., via send_event)\n        self._external_queue: asyncio.Queue[WorkflowTick] = asyncio.Queue()\n        # Queue for events published to the event stream (e.g., for UI/callbacks)\n        self._event_stream: asyncio.Queue[Event] = asyncio.Queue()\n        # Time-machine traveller for deterministic time control\n        self._traveller = traveller\n        # Current time in seconds, can be advanced manually for testing\n        self._current_time: float = time.time()\n        # Recorded ticks for snapshot/replay\n        self._ticks: list[WorkflowTick] = []\n        # State store for context\n        self._state_store: \"InMemoryStateStore[Any] | None\" = None\n        # Result tracking for get_result/cancel\n        self._result: asyncio.Future[StopEvent] = asyncio.Future()\n        self._cancelled: bool = False\n\n    @property\n    def run_id(self) -> str:\n        return self._run_id\n\n    @property\n    def tags(self) -> dict[str, Any]:\n        return {\"llamaindex.run_id\": self._run_id}\n\n    @property\n    def init_state(self) -> BrokerState:\n        # Return a minimal BrokerState for testing\n        return BrokerState(\n            is_running=False,\n            config=BrokerConfig(steps={}, timeout=None),\n            workers={},\n        )\n\n    async def on_tick(self, tick: WorkflowTick) -> None:\n        \"\"\"Record a tick for replay.\"\"\"\n        self._ticks.append(tick)\n\n    def replay(self) -> list[WorkflowTick]:\n        \"\"\"Return recorded ticks for replay.\"\"\"\n        return self._ticks\n\n    async def close(self) -> None:\n        \"\"\"\n        Close the adapter.\n        \"\"\"\n        pass\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        await self._event_stream.put(event)\n\n    async def stream_published_events(self) -> AsyncGenerator[Event, None]:\n        while True:\n            item = await self._event_stream.get()\n            yield item\n            if isinstance(item, StopEvent):\n                break\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        await self._external_queue.put(tick)\n\n    async def get_now(self) -> float:\n        if self._traveller is not None:\n            return time.time()\n        return self._current_time\n\n    async def wait_receive(\n        self,\n        timeout_seconds: float | None = None,\n    ) -> WaitResult:\n        \"\"\"Wait for tick with optional timeout.\n\n        When a timeout occurs, advances mock time by the timeout duration\n        to ensure scheduled ticks become due.\n        \"\"\"\n        try:\n            if timeout_seconds is None:\n                tick = await self._external_queue.get()\n            else:\n                tick = await asyncio.wait_for(\n                    self._external_queue.get(),\n                    timeout=timeout_seconds,\n                )\n            return WaitResultTick(tick=tick)\n        except asyncio.TimeoutError:\n            # Advance mock time when timeout occurs\n            if timeout_seconds is not None:\n                self.advance_time(timeout_seconds)\n            return WaitResultTimeout()\n\n    def advance_time(self, seconds: float) -> None:\n        if self._traveller is not None:\n            self._traveller.shift(seconds)\n        else:\n            self._current_time += seconds\n\n    async def get_stream_event(self, timeout: float = 1.0) -> Event:\n        return await asyncio.wait_for(self._event_stream.get(), timeout=timeout)\n\n    def has_stream_events(self) -> bool:\n        return not self._event_stream.empty()\n\n    def get_state_store(self) -> \"InMemoryStateStore[Any] | None\":\n        return self._state_store\n\n    def set_state_store(self, state_store: \"InMemoryStateStore[Any]\") -> None:\n        self._state_store = state_store\n\n    async def get_result(self) -> StopEvent:\n        \"\"\"Get the result of the workflow run.\"\"\"\n        return await self._result\n\n    def get_result_or_none(self) -> StopEvent | None:\n        \"\"\"Get the result if completed, otherwise None.\"\"\"\n        if self._result.done() and not self._result.cancelled():\n            return self._result.result()\n        return None\n\n    @property\n    def is_running(self) -> bool:\n        \"\"\"Check if the workflow run is still running.\"\"\"\n        return not self._result.done() and not self._cancelled\n\n    def abort(self) -> None:\n        \"\"\"Abort by cancelling the result future.\"\"\"\n        if not self._result.done():\n            self._result.cancel()\n            self._cancelled = True\n\n    def set_result(self, result: StopEvent) -> None:\n        \"\"\"Set the result (for test setup).\"\"\"\n        if not self._result.done():\n            self._result.set_result(result)\n\n\n@pytest.fixture\nasync def test_plugin() -> MockRunAdapter:\n    return MockRunAdapter(run_id=\"test\")\n\n\n@pytest.fixture\nasync def test_plugin_with_time_machine() -> AsyncGenerator[\n    tuple[MockRunAdapter, time_machine.Coordinates], None\n]:\n    \"\"\"Adapter with time-machine at epoch 1000.0, tick=True.\"\"\"\n    with time_machine.travel(\"2026-01-07T12:27:00.000-08:00\", tick=True) as traveller:\n        yield MockRunAdapter(run_id=\"test\", traveller=traveller), traveller\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/test_control_loop.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"\nTests for the control_loop function in the runtime module.\n\nThe control loop is the core event processing engine that:\n- Processes workflow ticks (events, step results, timeouts, cancellations)\n- Manages step worker state and execution\n- Coordinates event routing between steps\n- Handles retries, timeouts, and failures\n\"\"\"\n\nimport asyncio\nimport time\nimport uuid\nfrom typing import Coroutine\n\nimport pytest\nimport time_machine\nfrom workflows.context import Context\nfrom workflows.context.state_store import DictState, InMemoryStateStore\nfrom workflows.decorators import step\nfrom workflows.errors import WorkflowCancelledByUser, WorkflowTimeoutError\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StepStateChanged,\n    StopEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n    WorkflowIdleEvent,\n    WorkflowTimedOutEvent,\n)\nfrom workflows.plugins.basic import setting_run_id\nfrom workflows.retry_policy import (\n    ConstantDelayRetryPolicy,\n    RetryPolicy,\n    retry_policy,\n    stop_before_delay,\n    wait_fixed,\n)\nfrom workflows.runtime.control_loop import control_loop\nfrom workflows.runtime.types.internal_state import BrokerState\nfrom workflows.runtime.types.plugin import RunContext, run_context\nfrom workflows.runtime.types.step_function import as_step_worker_function\nfrom workflows.runtime.types.ticks import TickAddEvent, TickCancelRun\nfrom workflows.workflow import Workflow\n\nfrom .conftest import MockRunAdapter, MockRuntime\n\npytestmark = pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\n\n\nclass IntermediateEvent(Event):\n    \"\"\"Test event passed between workflow steps.\"\"\"\n\n    value: int\n\n\nclass FinalEvent(Event):\n    \"\"\"Test event indicating workflow completion.\"\"\"\n\n    final_value: str\n\n\nclass SimpleWorkflow(Workflow):\n    \"\"\"\n    A simple three-step workflow for testing the happy path.\n\n    Flow:\n        StartEvent -> IntermediateEvent -> FinalEvent -> StopEvent\n    \"\"\"\n\n    @step\n    async def start_step(self, ev: StartEvent) -> IntermediateEvent:\n        \"\"\"First step: receives start event and produces intermediate event.\"\"\"\n        return IntermediateEvent(value=42)\n\n    @step\n    async def middle_step(self, ev: IntermediateEvent) -> FinalEvent:\n        \"\"\"Second step: processes intermediate event and produces final event.\"\"\"\n        return FinalEvent(final_value=f\"processed_{ev.value}\")\n\n    @step\n    async def end_step(self, ev: FinalEvent) -> StopEvent:\n        \"\"\"Final step: receives final event and returns stop event.\"\"\"\n        return StopEvent(result=ev.final_value)\n\n\nclass CollectEv(Event):\n    i: int\n\n\nclass CollectEv2(Event):\n    j: int\n\n\nclass CollectMultipleEventTypesWorkflow(Workflow):\n    @step\n    async def accept_start(self, ev: StartEvent, context: Context) -> CollectEv | None:\n        for i in range(2):\n            context.send_event(CollectEv(i=i + 1))\n        return None\n\n    @step\n    async def accept_collect1(self, ev: CollectEv, context: Context) -> CollectEv2:\n        return CollectEv2(j=ev.i * 10)\n\n    @step\n    async def collector(\n        self, ev: CollectEv | CollectEv2, context: Context\n    ) -> StopEvent | None:\n        events = context.collect_events(ev, [CollectEv, CollectEv2] * 2)\n        if events is None:\n            return None\n        assert [type(x) for x in events] == [\n            CollectEv,\n            CollectEv2,\n        ] * 2  # same order as expected\n        events = sum(\n            [\n                e.i\n                if isinstance(e, CollectEv)\n                else e.j\n                if isinstance(e, CollectEv2)\n                else 0\n                for e in events\n            ]\n        )\n        return StopEvent(result=f\"sum_{events}\")\n\n\nclass CollectWorkflow(Workflow):\n    @step\n    async def accept_start(self, ev: StartEvent, context: Context) -> CollectEv | None:\n        for i in range(4):\n            context.send_event(CollectEv(i=i + 1))\n        return None\n\n    @step\n    async def collector(self, ev: CollectEv, context: Context) -> StopEvent | None:\n        events = context.collect_events(ev, [CollectEv] * 4)\n        if events is None:\n            return None\n        events = sum([e.i for e in events])\n        return StopEvent(result=f\"sum_{events}\")\n\n\ndef run_control_loop(\n    workflow: Workflow,\n    start_event: StartEvent | None,\n    test_runtime: MockRunAdapter,\n) -> Coroutine[None, None, StopEvent]:\n    step_workers = {}\n    for name, step_func in workflow._get_steps().items():\n        unbound = getattr(step_func, \"__func__\", step_func)\n        step_workers[name] = as_step_worker_function(unbound)\n    run_id = str(uuid.uuid4())\n    # Set up mock runtime with the test adapter\n    mock_runtime = MockRuntime()\n    test_runtime.set_state_store(InMemoryStateStore(DictState()))\n    mock_runtime.set_adapter(run_id, test_runtime)\n    # Override workflow's runtime to use mock\n    workflow._runtime = mock_runtime\n    with setting_run_id(run_id):\n        ctx = Context._create_internal(workflow=workflow)\n\n    async def _run() -> StopEvent:\n        with setting_run_id(run_id):\n            run_ctx = RunContext(\n                workflow=workflow,\n                run_adapter=test_runtime,\n                context=ctx,\n                steps=step_workers,\n            )\n            with run_context(run_ctx):\n                return await control_loop(\n                    start_event=start_event,\n                    init_state=BrokerState.from_workflow(workflow),\n                    run_id=run_id,\n                )\n\n    return _run()\n\n\nasync def wait_for_stop_event(\n    plugin: MockRunAdapter, timeout: float = 1.0\n) -> StopEvent | None:\n    \"\"\"\n    Helper to wait for a StopEvent in the event stream.\n\n    Args:\n        plugin: The MockRunAdapter to read events from\n        timeout: Maximum time to wait for StopEvent (default: 1.0 seconds)\n\n    Returns:\n        The StopEvent if found, None if timeout occurs\n    \"\"\"\n    try:\n        while True:\n            try:\n                ev = await asyncio.wait_for(\n                    plugin.get_stream_event(timeout=timeout), timeout=timeout\n                )\n                if isinstance(ev, StopEvent):\n                    return ev\n            except asyncio.TimeoutError:\n                return None\n    except Exception:\n        return None\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_happy_path(test_plugin: MockRunAdapter) -> None:\n    \"\"\"\n    Test the happy path through the control loop.\n\n    This test validates that:\n    1. The control loop properly initializes with workflow state\n    2. Events flow through the workflow steps in order\n    3. Each step executes and produces the correct output event\n    4. The workflow completes with the expected StopEvent result\n    5. Step state changes are published to the event stream\n    \"\"\"\n\n    result = await run_control_loop(\n        workflow=SimpleWorkflow(timeout=1.0),\n        start_event=StartEvent(),\n        test_runtime=test_plugin,\n    )\n\n    # Verify the workflow completed with expected result\n    assert isinstance(result, StopEvent)\n    assert result.result == \"processed_42\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_with_external_event(\n    test_plugin: MockRunAdapter,\n) -> None:\n    \"\"\"\n    Test that external events can be sent to a running workflow.\n\n    This validates that the control loop can receive events from outside\n    during execution, useful for human-in-the-loop or webhook scenarios.\n\n    The workflow starts with no initial event, and we inject a StartEvent\n    externally using the plugin's send_event method.\n    \"\"\"\n\n    class ExternalTriggerWorkflow(Workflow):\n        \"\"\"Workflow that waits for an external event.\"\"\"\n\n        @step\n        async def start_step(self, ev: StartEvent) -> StopEvent:\n            \"\"\"Step that processes the externally sent start event.\"\"\"\n            return StopEvent(result=\"received_external_event\")\n\n    # Setup\n    workflow = ExternalTriggerWorkflow(timeout=1.0)\n\n    result_task = asyncio.create_task(\n        run_control_loop(\n            workflow=workflow,\n            start_event=None,\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Now send an external event to trigger the workflow\n    await test_plugin.send_event(TickAddEvent(event=StartEvent()))\n\n    # Wait for completion\n    result = await asyncio.wait_for(result_task, timeout=5.0)\n\n    # Verify\n    assert isinstance(result, StopEvent)\n    assert result.result == \"received_external_event\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_timeout(\n    test_plugin_with_time_machine: tuple[MockRunAdapter, time_machine.Coordinates],\n) -> None:\n    \"\"\"\n    Test that workflow timeout raises WorkflowTimeoutError and publishes WorkflowTimedOutEvent.\n\n    When a workflow times out, a WorkflowTimedOutEvent should be published to the stream\n    to inform consumers about the timeout before the exception is raised.\n    \"\"\"\n    test_plugin, _ = test_plugin_with_time_machine\n\n    class SlowWorkflow(Workflow):\n        @step\n        async def slow(self, ev: StartEvent) -> StopEvent:\n            await asyncio.sleep(0.5)\n            return StopEvent(result=\"never\")\n\n    wf = SlowWorkflow(timeout=0.01)\n\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Wait for the StopEvent to be published\n    stop_event = await wait_for_stop_event(test_plugin)\n\n    # Verify that the timeout exception is raised\n    with pytest.raises(WorkflowTimeoutError):\n        await asyncio.wait_for(task, timeout=1.0)\n\n    # Verify a WorkflowTimedOutEvent was published to the stream\n    assert stop_event is not None, (\n        \"Timeout should publish WorkflowTimedOutEvent to stream before raising exception\"\n    )\n    assert isinstance(stop_event, WorkflowTimedOutEvent), (\n        f\"Expected WorkflowTimedOutEvent, got {type(stop_event).__name__}\"\n    )\n    assert stop_event.timeout == 0.01, \"Timeout event should contain the timeout value\"\n    assert stop_event.active_steps == [\"slow\"], \"Timeout event should list active steps\"\n\n\n@pytest.mark.asyncio\nasync def test_wait_for_event_timeout(\n    test_plugin_with_time_machine: tuple[MockRunAdapter, time_machine.Coordinates],\n) -> None:\n    \"\"\"wait_for_event raises asyncio.TimeoutError when the timeout elapses.\"\"\"\n    test_plugin, _ = test_plugin_with_time_machine\n\n    class AwaitedEvent(Event):\n        pass\n\n    class WaiterWorkflow(Workflow):\n        @step\n        async def start(self, ev: StartEvent, ctx: Context) -> StopEvent:\n            await ctx.wait_for_event(AwaitedEvent, timeout=0.01)\n            return StopEvent(result=\"should not reach\")\n\n    wf = WaiterWorkflow()\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # No event is sent — the waiter should time out\n    with pytest.raises(asyncio.TimeoutError):\n        await asyncio.wait_for(task, timeout=2.0)\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_retry_policy(test_plugin: MockRunAdapter) -> None:\n    \"\"\"\n    Test that retry policy works correctly when a step fails initially but succeeds on retry.\n    \"\"\"\n\n    class RetryWorkflow(Workflow):\n        attempts = 0\n\n        @step(retry_policy=ConstantDelayRetryPolicy(maximum_attempts=2, delay=0))\n        async def flaky(self, ev: StartEvent) -> StopEvent:\n            self.attempts += 1\n            if self.attempts == 1:\n                raise RuntimeError(\"fail once\")\n            return StopEvent(result=f\"ok_{self.attempts}\")\n\n    wf = RetryWorkflow(timeout=1.0)\n\n    result = await run_control_loop(\n        workflow=wf,\n        start_event=StartEvent(),\n        test_runtime=test_plugin,\n    )\n\n    assert isinstance(result, StopEvent)\n    assert result.result == \"ok_2\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_step_failure_publishes_stop_event(\n    test_plugin: MockRunAdapter,\n) -> None:\n    \"\"\"\n    Test that when a step fails permanently (retries exhausted),\n    a WorkflowFailedEvent is published to the stream before raising the exception.\n\n    This allows external consumers to know why the workflow stream has ended.\n    \"\"\"\n\n    class FailingWorkflow(Workflow):\n        @step(retry_policy=ConstantDelayRetryPolicy(maximum_attempts=1, delay=0))\n        async def always_fails(self, ev: StartEvent) -> StopEvent:\n            raise ValueError(\"intentional failure\")\n\n    wf = FailingWorkflow(timeout=1.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Wait for the StopEvent to be published\n    stop_event = await wait_for_stop_event(test_plugin)\n\n    # Now verify the workflow raised an exception\n    with pytest.raises(ValueError, match=\"intentional failure\"):\n        await asyncio.wait_for(task, timeout=1.0)\n\n    # Verify that a WorkflowFailedEvent was published before the exception\n    assert stop_event is not None, (\n        \"WorkflowFailedEvent should be published to stream when step fails permanently\"\n    )\n    assert isinstance(stop_event, WorkflowFailedEvent), (\n        f\"Expected WorkflowFailedEvent, got {type(stop_event).__name__}\"\n    )\n    assert stop_event.step_name == \"always_fails\", (\n        \"Failed event should contain the step name\"\n    )\n    assert isinstance(stop_event.exception, ValueError), (\n        \"Failed event should carry the live exception\"\n    )\n    assert str(stop_event.exception) == \"intentional failure\", (\n        \"Failed event exception should carry the message\"\n    )\n    assert stop_event.attempts == 1, \"Failed event should contain the attempt count\"\n    assert stop_event.elapsed_seconds >= 0, \"Failed event should contain elapsed time\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_waiter_resolution(test_plugin: MockRunAdapter) -> None:\n    class Awaited(Event):\n        tag: str\n\n    class WaiterWorkflow(Workflow):\n        @step\n        async def start(self, ev: StartEvent, ctx: Context) -> StopEvent:\n            print(\"waiting for event\")\n            awaited = await ctx.wait_for_event(\n                Awaited,\n                waiter_event=InputRequiredEvent(),\n                requirements={\"tag\": \"go\"},\n            )\n            return StopEvent(result=f\"got_{awaited.tag}\")\n\n    wf = WaiterWorkflow(timeout=1.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Let first run add the waiter\n    async def wait_input_required() -> InputRequiredEvent:\n        async for event in test_plugin.stream_published_events():\n            if isinstance(event, InputRequiredEvent):\n                return event\n        raise TimeoutError(\"InputRequiredEvent not found\")\n\n    await asyncio.wait_for(wait_input_required(), timeout=1.0)\n\n    # Send the awaited event that satisfies requirements\n    await test_plugin.send_event(TickAddEvent(event=Awaited(tag=\"go\")))\n\n    result = await asyncio.wait_for(task, timeout=2.0)\n    assert isinstance(result, StopEvent)\n    assert result.result == \"got_go\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_input_required_published_to_stream(\n    test_plugin: MockRunAdapter,\n) -> None:\n    \"\"\"\n    Test that InputRequiredEvent is automatically published to the outward stream.\n\n    When a workflow step calls wait_for_event, an InputRequiredEvent should be\n    automatically published to the event stream so that external consumers\n    (like UIs or monitoring systems) can be notified that the workflow is\n    waiting for input.\n    \"\"\"\n\n    class AwaitedEvent(Event):\n        value: str\n\n    class WaitingWorkflow(Workflow):\n        @step\n        async def waiter(self, ev: StartEvent, ctx: Context) -> StopEvent:\n            # This should cause an InputRequiredEvent to be published\n            awaited = await ctx.wait_for_event(\n                AwaitedEvent,\n                waiter_event=InputRequiredEvent(),\n            )\n            return StopEvent(result=f\"received_{awaited.value}\")\n\n    wf = WaitingWorkflow(timeout=2.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Wait for the InputRequiredEvent to appear in the stream\n    input_required_found = False\n    while True:\n        ev = await test_plugin.get_stream_event(timeout=1.0)\n        if isinstance(ev, InputRequiredEvent):\n            input_required_found = True\n            break\n        # Skip StepStateChanged events\n        if isinstance(ev, StopEvent):\n            break\n\n    assert input_required_found, \"InputRequiredEvent should be published to stream\"\n\n    # Now send the awaited event to complete the workflow\n    await test_plugin.send_event(TickAddEvent(event=AwaitedEvent(value=\"test_data\")))\n\n    # Verify workflow completes successfully\n    result = await asyncio.wait_for(task, timeout=1.0)\n    assert isinstance(result, StopEvent)\n    assert result.result == \"received_test_data\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_collect_events_same_type(\n    test_plugin: MockRunAdapter,\n) -> None:\n    wf = CollectWorkflow(timeout=1.0)\n    result = await asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    assert isinstance(result, StopEvent)\n    assert result.result == \"sum_10\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_collect_events_multiple_types(\n    test_plugin: MockRunAdapter,\n) -> None:\n    wf = CollectMultipleEventTypesWorkflow(timeout=1.0)\n    result = await run_control_loop(\n        workflow=wf,\n        start_event=StartEvent(),\n        test_runtime=test_plugin,\n    )\n    assert isinstance(result, StopEvent)\n    assert result.result == \"sum_33\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_stream_events(test_plugin: MockRunAdapter) -> None:\n    wf = SimpleWorkflow(timeout=5.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Expect at least one StepStateChanged event while running\n    stream_events: list[Event] = []\n    while True:\n        ev = await test_plugin.get_stream_event(timeout=1.0)\n        stream_events.append(ev)\n        if isinstance(ev, StopEvent):\n            break\n\n    result = await asyncio.wait_for(task, timeout=2.0)\n    assert isinstance(result, StopEvent)\n    # Ensure at least one StepStateChanged observed\n    assert len(stream_events) == 7\n    assert [type(x) for x in stream_events] == [StepStateChanged] * 6 + [StopEvent]\n    change_events = [x for x in stream_events if isinstance(x, StepStateChanged)]\n    assert [x.step_state.name + \" - \" + x.name for x in change_events] == [\n        \"RUNNING - start_step\",\n        \"NOT_RUNNING - start_step\",\n        \"RUNNING - middle_step\",\n        \"NOT_RUNNING - middle_step\",\n        \"RUNNING - end_step\",\n        \"NOT_RUNNING - end_step\",\n    ]\n\n\nclass SomeEvent(HumanResponseEvent):\n    pass\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_per_step_routing(test_plugin: MockRunAdapter) -> None:\n    class RouteWorkflow(Workflow):\n        @step\n        async def starter(self, ev: StartEvent) -> StopEvent | None:\n            return None\n\n        @step\n        async def first(self, ev: SomeEvent) -> StopEvent:\n            return StopEvent(result=\"first\")\n\n        @step\n        async def second(self, ev: SomeEvent) -> StopEvent:\n            return StopEvent(result=\"second\")\n\n    wf = RouteWorkflow(timeout=1.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Route explicitly to the 'second' step with an accepted event type\n    await test_plugin.send_event(TickAddEvent(event=SomeEvent(), step_name=\"second\"))\n\n    result = await asyncio.wait_for(task, timeout=2.0)\n    assert isinstance(result, StopEvent)\n    assert result.result == \"second\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_concurrency_queueing(\n    test_plugin: MockRunAdapter,\n) -> None:\n    class LimitedWorkflow(Workflow):\n        @step(num_workers=1)\n        async def only_one(self, ev: StartEvent) -> StopEvent:\n            # Hold to simulate long work\n            await asyncio.sleep(0.01)\n            return StopEvent(result=\"done\")\n\n    wf = LimitedWorkflow(timeout=5.0)\n\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            test_runtime=test_plugin,\n            start_event=None,\n        )\n    )\n\n    await asyncio.sleep(0)\n    # Send two events quickly; with num_workers=1, second should queue (PREPARING)\n    await asyncio.gather(\n        *[test_plugin.send_event(TickAddEvent(event=StartEvent())) for _ in range(10)]\n    )\n\n    # Observe stream for PREPARING signal\n    saw_preparing = False\n    for _ in range(5):\n        ev = await test_plugin.get_stream_event(timeout=1.0)\n        if isinstance(ev, StepStateChanged) and ev.step_state.name == \"PREPARING\":\n            saw_preparing = True\n            break\n\n    # Drain to completion\n    result = await asyncio.wait_for(task, timeout=2.0)\n    assert isinstance(result, StopEvent)\n    assert saw_preparing\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_user_cancellation(test_plugin: MockRunAdapter) -> None:\n    \"\"\"\n    Test that user cancellation raises WorkflowCancelledByUser and publishes WorkflowCancelledEvent.\n\n    When a workflow is cancelled, a WorkflowCancelledEvent should be published to the stream\n    to inform consumers about the cancellation before the exception is raised.\n    \"\"\"\n\n    class CancelWorkflow(Workflow):\n        @step\n        async def slow(self, ev: StartEvent) -> StopEvent:\n            await asyncio.sleep(1.0)\n            return StopEvent(result=\"never\")\n\n    wf = CancelWorkflow(timeout=5.0)\n\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            test_runtime=test_plugin,\n            start_event=StartEvent(),\n        )\n    )\n\n    # Cancel the run externally\n    await asyncio.sleep(0)\n    await test_plugin.send_event(TickCancelRun())\n\n    # Wait for the StopEvent to be published\n    stop_event = await wait_for_stop_event(test_plugin)\n\n    # Verify that the cancellation exception is raised\n    with pytest.raises(WorkflowCancelledByUser):\n        await asyncio.wait_for(task, timeout=1.0)\n\n    # Verify a WorkflowCancelledEvent was published to the stream\n    assert stop_event is not None, (\n        \"Cancellation should publish WorkflowCancelledEvent to stream before raising exception\"\n    )\n    assert isinstance(stop_event, WorkflowCancelledEvent), (\n        f\"Expected WorkflowCancelledEvent, got {type(stop_event).__name__}\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_retry_with_delay(\n    test_plugin_with_time_machine: tuple[MockRunAdapter, time_machine.Coordinates],\n) -> None:\n    \"\"\"Test that retry delay is enforced between attempts.\"\"\"\n    test_plugin, _ = test_plugin_with_time_machine\n    retry_delay = 0.02\n\n    class DelayedRetryWorkflow(Workflow):\n        attempt_times: list[float] = []\n\n        @step(\n            retry_policy=ConstantDelayRetryPolicy(maximum_attempts=3, delay=retry_delay)\n        )\n        async def flaky(self, ev: StartEvent) -> StopEvent:\n            self.attempt_times.append(time.time())\n            if len(self.attempt_times) < 3:\n                raise RuntimeError(f\"fail attempt {len(self.attempt_times)}\")\n            return StopEvent(result=f\"ok_after_{len(self.attempt_times)}_attempts\")\n\n    wf = DelayedRetryWorkflow(timeout=5.0)\n\n    result = await run_control_loop(\n        workflow=wf,\n        start_event=StartEvent(),\n        test_runtime=test_plugin,\n    )\n\n    assert isinstance(result, StopEvent)\n    assert result.result == \"ok_after_3_attempts\"\n\n    assert len(wf.attempt_times) == 3\n    for i in range(1, len(wf.attempt_times)):\n        elapsed = wf.attempt_times[i] - wf.attempt_times[i - 1]\n        assert elapsed >= retry_delay * 0.8, (\n            f\"expected >= {retry_delay * 0.8:.3f}s, got {elapsed:.3f}s\"\n        )\n    # Verify time-machine is active (epoch starts at 1000.0)\n    assert wf.attempt_times[0] >= 1000.0\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_retry_gives_up_after_max_attempts(\n    test_plugin_with_time_machine: tuple[MockRunAdapter, time_machine.Coordinates],\n) -> None:\n    \"\"\"Test that workflow fails after exhausting maximum_attempts.\"\"\"\n    test_plugin, _ = test_plugin_with_time_machine\n    max_attempts = 3\n\n    class AlwaysFailsWorkflow(Workflow):\n        attempt_count = 0\n\n        @step(\n            retry_policy=ConstantDelayRetryPolicy(\n                maximum_attempts=max_attempts, delay=0.01\n            )\n        )\n        async def always_fails(self, ev: StartEvent) -> StopEvent:\n            self.attempt_count += 1\n            raise ValueError(f\"fail #{self.attempt_count}\")\n\n    wf = AlwaysFailsWorkflow(timeout=5.0)\n\n    with pytest.raises(ValueError, match=\"fail #3\"):\n        await run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n\n    assert wf.attempt_count == max_attempts\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_retry_exhaustion_respects_total_time(\n    test_plugin_with_time_machine: tuple[MockRunAdapter, time_machine.Coordinates],\n) -> None:\n    \"\"\"Test that retry policy receives correct elapsed_time across retries.\"\"\"\n    test_plugin, _ = test_plugin_with_time_machine\n    retry_delay = 0.01\n\n    class ElapsedTimeTrackingPolicy(RetryPolicy):\n        def __init__(self, retry_delay: float) -> None:\n            self.retry_delay = retry_delay\n            self.observed_elapsed_times: list[float] = []\n            self.observed_attempts: list[int] = []\n\n        def next(\n            self,\n            elapsed_time: float,\n            attempts: int,\n            error: BaseException,\n            *,\n            seed: int | None = None,\n        ) -> float | None:\n            self.observed_elapsed_times.append(elapsed_time)\n            self.observed_attempts.append(attempts)\n            return self.retry_delay\n\n    policy: ElapsedTimeTrackingPolicy = ElapsedTimeTrackingPolicy(\n        retry_delay=retry_delay\n    )\n\n    class TrackedRetryWorkflow(Workflow):\n        total_calls = 0\n\n        @step(retry_policy=policy)\n        async def always_fail(self, ev: StartEvent) -> StopEvent:\n            self.total_calls += 1\n            if self.total_calls < 5:\n                raise RuntimeError(f\"fail {self.total_calls}\")\n            return StopEvent(result=\"eventually_ok\")\n\n    wf = TrackedRetryWorkflow(timeout=5.0)\n\n    result = await run_control_loop(\n        workflow=wf,\n        start_event=StartEvent(),\n        test_runtime=test_plugin,\n    )\n\n    assert isinstance(result, StopEvent)\n    assert result.result == \"eventually_ok\"\n\n    # Verify we had the expected number of failures (4 failures, 5th succeeds)\n    assert len(policy.observed_elapsed_times) == 4, (\n        f\"Expected 4 retry attempts (failures), got {len(policy.observed_elapsed_times)}\"\n    )\n\n    # Elapsed times should be strictly increasing (not reset to 0)\n    # If first_attempt_at was being reset on each retry, all elapsed times would be ~0\n    for i in range(1, len(policy.observed_elapsed_times)):\n        assert (\n            policy.observed_elapsed_times[i] > policy.observed_elapsed_times[i - 1]\n        ), f\"Elapsed time should increase: {policy.observed_elapsed_times}\"\n\n    # Elapsed times should grow: ~0, ~retry_delay, ~2*retry_delay, ...\n    for i, elapsed in enumerate(policy.observed_elapsed_times):\n        min_expected = retry_delay * i * 0.8\n        assert elapsed >= min_expected, (\n            f\"elapsed[{i}]: expected >= {min_expected:.4f}, got {elapsed:.4f}\"\n        )\n\n    # Attempts should increment: 1, 2, 3, 4\n    expected_attempts = list(range(1, len(policy.observed_attempts) + 1))\n    assert policy.observed_attempts == expected_attempts\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_stop_before_delay_uses_upcoming_sleep(\n    test_plugin_with_time_machine: tuple[MockRunAdapter, time_machine.Coordinates],\n) -> None:\n    \"\"\"Test that stop_before_delay stops before the next sleep crosses the limit.\"\"\"\n    test_plugin, _ = test_plugin_with_time_machine\n    retry_delay = 0.2\n\n    class AlwaysFailsWorkflow(Workflow):\n        attempt_count = 0\n\n        @step(\n            retry_policy=retry_policy(\n                wait=wait_fixed(retry_delay),\n                stop=stop_before_delay(0.6),\n            )\n        )\n        async def always_fails(self, ev: StartEvent) -> StopEvent:\n            self.attempt_count += 1\n            raise ValueError(f\"fail #{self.attempt_count}\")\n\n    wf = AlwaysFailsWorkflow(timeout=5.0)\n\n    with pytest.raises(ValueError, match=\"fail #3\"):\n        await run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n\n    assert wf.attempt_count == 3\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_emits_idle_event_when_waiting(\n    test_plugin: MockRunAdapter,\n) -> None:\n    \"\"\"\n    Test that WorkflowIdleEvent is emitted when workflow becomes idle.\n\n    A workflow is idle when all steps have empty queues and no in-progress\n    workers. This uses a two-step pattern: the first step completes (leaving\n    state idle), and the second step accepts an external event to finish.\n    \"\"\"\n\n    class ExternalEvent(HumanResponseEvent):\n        value: str\n\n    class IdleTrackingWorkflow(Workflow):\n        @step\n        async def start(self, ev: StartEvent) -> None:\n            pass\n\n        @step\n        async def finish(self, ev: ExternalEvent) -> StopEvent:\n            return StopEvent(result=f\"received_{ev.value}\")\n\n    wf = IdleTrackingWorkflow(timeout=2.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Collect events until we see the WorkflowIdleEvent\n    idle_event_found = False\n\n    while True:\n        ev = await test_plugin.get_stream_event(timeout=1.0)\n        if isinstance(ev, WorkflowIdleEvent):\n            idle_event_found = True\n            break\n        if isinstance(ev, StopEvent):\n            break\n\n    assert idle_event_found, (\n        \"WorkflowIdleEvent should be emitted when workflow has no pending work\"\n    )\n\n    # Now send the external event to complete the workflow\n    await test_plugin.send_event(TickAddEvent(event=ExternalEvent(value=\"test\")))\n\n    result = await asyncio.wait_for(task, timeout=1.0)\n    assert isinstance(result, StopEvent)\n    assert result.result == \"received_test\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_emits_idle_event_with_wait_for_event(\n    test_plugin: MockRunAdapter,\n) -> None:\n    \"\"\"WorkflowIdleEvent fires when a step uses ctx.wait_for_event().\n\n    wait_for_event raises an internal exception that registers a waiter and\n    releases the worker. After that, the state has no queued events and no\n    in-progress workers, so the workflow is idle.\n    \"\"\"\n\n    class AwaitedEvent(Event):\n        value: str\n\n    class WaitForEventWorkflow(Workflow):\n        @step\n        async def waiter(self, ev: StartEvent, ctx: Context) -> StopEvent:\n            awaited = await ctx.wait_for_event(\n                AwaitedEvent,\n                waiter_event=InputRequiredEvent(),\n            )\n            return StopEvent(result=f\"received_{awaited.value}\")\n\n    wf = WaitForEventWorkflow(timeout=2.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    idle_event_found = False\n    input_required_found = False\n\n    while True:\n        ev = await test_plugin.get_stream_event(timeout=1.0)\n        if isinstance(ev, WorkflowIdleEvent):\n            idle_event_found = True\n            break\n        if isinstance(ev, InputRequiredEvent):\n            input_required_found = True\n        if isinstance(ev, StopEvent):\n            break\n\n    assert idle_event_found, (\n        \"WorkflowIdleEvent should be emitted when workflow is waiting for external event\"\n    )\n    assert input_required_found, \"InputRequiredEvent should be emitted before idle\"\n\n    # Send the awaited event to complete the workflow\n    await test_plugin.send_event(TickAddEvent(event=AwaitedEvent(value=\"test\")))\n\n    result = await asyncio.wait_for(task, timeout=1.0)\n    assert isinstance(result, StopEvent)\n    assert result.result == \"received_test\"\n\n\n@pytest.mark.asyncio\nasync def test_control_loop_idle_event_not_emitted_on_completion(\n    test_plugin: MockRunAdapter,\n) -> None:\n    \"\"\"\n    Test that WorkflowIdleEvent is NOT emitted when workflow completes normally.\n\n    Even if a workflow has waiters, if it completes (StopEvent), it should not\n    emit an idle event because the workflow is no longer running.\n    \"\"\"\n\n    result = await run_control_loop(\n        workflow=SimpleWorkflow(timeout=1.0),\n        start_event=StartEvent(),\n        test_runtime=test_plugin,\n    )\n\n    # Verify the workflow completed\n    assert isinstance(result, StopEvent)\n    assert result.result == \"processed_42\"\n\n    # Drain and verify no WorkflowIdleEvent was emitted\n    all_events: list[Event] = []\n    while test_plugin.has_stream_events():\n        ev = await test_plugin.get_stream_event(timeout=0.1)\n        all_events.append(ev)\n\n    idle_events = [e for e in all_events if isinstance(e, WorkflowIdleEvent)]\n    assert len(idle_events) == 0, (\n        \"WorkflowIdleEvent should not be emitted when workflow completes normally\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_simultaneous_retries_with_same_delay(\n    test_plugin_with_time_machine: tuple[MockRunAdapter, time_machine.Coordinates],\n) -> None:\n    \"\"\"\n    Test that the control loop handles multiple retries scheduled at the same timestamp.\n\n    When two steps both fail and have the same retry delay, they get scheduled\n    at exactly the same timestamp. Without a sequence counter tiebreaker in the\n    heap, Python's heapq would compare WorkflowTick objects directly, causing\n    TypeError since they don't implement __lt__.\n\n    This test uses a CoarseTimeAdapter that rounds timestamps to 1-second precision,\n    ensuring that retries scheduled within the same second will collide.\n    \"\"\"\n    base_plugin, traveller = test_plugin_with_time_machine\n\n    class CoarseTimeAdapter(MockRunAdapter):\n        \"\"\"Adapter that rounds get_now() to 1-second precision to force collisions.\"\"\"\n\n        async def get_now(self) -> float:\n            # Round to nearest second to force timestamp collisions\n            return float(int(time.time()))\n\n    test_plugin = CoarseTimeAdapter(run_id=\"test\", traveller=traveller)\n    test_plugin.set_state_store(InMemoryStateStore(DictState()))\n\n    # Use a delay that's less than 1 second so both retries land on same rounded second\n    retry_delay = 0.01\n\n    class ResultA(Event):\n        pass\n\n    class ResultB(Event):\n        pass\n\n    class TwoStepsFailOnceWorkflow(Workflow):\n        step_a_attempts = 0\n        step_b_attempts = 0\n\n        @step(\n            retry_policy=ConstantDelayRetryPolicy(maximum_attempts=2, delay=retry_delay)\n        )\n        async def step_a(self, ev: StartEvent) -> ResultA:\n            self.step_a_attempts += 1\n            if self.step_a_attempts == 1:\n                raise RuntimeError(\"step_a fails once\")\n            return ResultA()\n\n        @step(\n            retry_policy=ConstantDelayRetryPolicy(maximum_attempts=2, delay=retry_delay)\n        )\n        async def step_b(self, ev: StartEvent) -> ResultB:\n            self.step_b_attempts += 1\n            if self.step_b_attempts == 1:\n                raise RuntimeError(\"step_b fails once\")\n            return ResultB()\n\n        @step\n        async def collector(\n            self, ev: ResultA | ResultB, ctx: Context\n        ) -> StopEvent | None:\n            events = ctx.collect_events(ev, [ResultA, ResultB])\n            if events is None:\n                return None\n            return StopEvent(result=\"both_succeeded\")\n\n    wf = TwoStepsFailOnceWorkflow(timeout=5.0)\n\n    result = await run_control_loop(\n        workflow=wf,\n        start_event=StartEvent(),\n        test_runtime=test_plugin,\n    )\n\n    assert isinstance(result, StopEvent)\n    assert result.result == \"both_succeeded\"\n    assert wf.step_a_attempts == 2\n    assert wf.step_b_attempts == 2\n\n\n@pytest.mark.asyncio\nasync def test_external_event_not_double_routed_when_waiter_exists(\n    test_plugin: MockRunAdapter,\n) -> None:\n    \"\"\"Regression test: an external event that resolves a wait_for_event waiter\n    should NOT also be routed to another step that accepts the same event type.\n\n    Before the fix, the accepting step would run twice — once from normal\n    routing and once from the waiter waking up and re-emitting the event.\n    \"\"\"\n\n    class ExternalInput(Event):\n        value: str\n\n    step_run_count = 0\n\n    class DoubleRouteWorkflow(Workflow):\n        @step\n        async def kickoff(self, ev: StartEvent) -> ExternalInput:\n            return ExternalInput(value=\"init\")\n\n        @step\n        async def handle_input(self, ev: ExternalInput, ctx: Context) -> StopEvent:\n            # This step accepts ExternalInput AND waits for ExternalInput.\n            # wait_for_event works by raising an exception on first call,\n            # then the control loop re-runs the step after the waiter resolves.\n            # So this step runs twice normally: once to register the waiter,\n            # once after resolution. The bug caused a THIRD run via normal\n            # event routing of the external event to this step.\n            nonlocal step_run_count\n            step_run_count += 1\n            result = await ctx.wait_for_event(\n                ExternalInput,\n                waiter_event=InputRequiredEvent(),\n            )\n            return StopEvent(result=f\"got_{result.value}\")\n\n    wf = DoubleRouteWorkflow(timeout=2.0)\n    task = asyncio.create_task(\n        run_control_loop(\n            workflow=wf,\n            start_event=StartEvent(),\n            test_runtime=test_plugin,\n        )\n    )\n\n    # Wait for the waiter to be registered\n    async for event in test_plugin.stream_published_events():\n        if isinstance(event, InputRequiredEvent):\n            break\n\n    # Send the external event — this resolves the waiter on handle_input.\n    # Without the fix, handle_input would ALSO get ExternalInput via normal\n    # accepted_events routing, causing a second execution.\n    await test_plugin.send_event(TickAddEvent(event=ExternalInput(value=\"hello\")))\n\n    result = await asyncio.wait_for(task, timeout=2.0)\n    assert isinstance(result, StopEvent)\n    assert result.result == \"got_hello\", (\n        f\"Expected waiter resolution result, got '{result.result}'\"\n    )\n    # Step runs twice: once to register the waiter (raises WaitingForEvent),\n    # once after waiter resolution (returns the result). Without the fix,\n    # it would run a third time from the external event being routed directly.\n    assert step_run_count == 2, (\n        f\"handle_input should run exactly twice, but ran {step_run_count} times\"\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/test_control_loop_transformations.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"\nUnit tests for control loop transformation functions.\n\nThese tests focus on the pure transformation functions in the control loop,\ntesting them in isolation without running the full async control loop.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nfrom collections.abc import AsyncIterator\nfrom typing import cast\n\nimport pytest\nfrom workflows.decorators import StepConfig\nfrom workflows.errors import WorkflowTimeoutError\nfrom workflows.events import (\n    Event,\n    IdleReleasedEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StepState,\n    StepStateChanged,\n    StopEvent,\n    UnhandledEvent,\n    WorkflowIdleEvent,\n)\nfrom workflows.retry_policy import (\n    ConstantDelayRetryPolicy,\n    ExponentialBackoffRetryPolicy,\n)\nfrom workflows.runtime.control_loop import (\n    _add_or_enqueue_event,\n    _check_idle_state,\n    _process_add_event_tick,\n    _process_cancel_run_tick,\n    _process_publish_event_tick,\n    _process_step_result_tick,\n    _process_timeout_tick,\n    _reduce_tick,\n    rebuild_state_from_ticks,\n    rebuild_state_from_ticks_stream,\n    replay_ticks_stream,\n    rewind_in_progress,\n)\nfrom workflows.runtime.types.commands import (\n    CommandCompleteRun,\n    CommandFailWorkflow,\n    CommandHalt,\n    CommandPublishEvent,\n    CommandQueueEvent,\n    CommandRunWorker,\n)\nfrom workflows.runtime.types.internal_state import (\n    BrokerConfig,\n    BrokerState,\n    EventAttempt,\n    InProgressState,\n    InternalStepConfig,\n    InternalStepWorkerState,\n)\nfrom workflows.runtime.types.results import (\n    AddCollectedEvent,\n    AddWaiter,\n    DeleteCollectedEvent,\n    DeleteWaiter,\n    StepFunctionResult,\n    StepWorkerFailed,\n    StepWorkerResult,\n    StepWorkerState,\n    StepWorkerWaiter,\n)\nfrom workflows.runtime.types.ticks import (\n    TickAddEvent,\n    TickCancelRun,\n    TickIdleRelease,\n    TickPublishEvent,\n    TickStepResult,\n    TickTimeout,\n    WorkflowTick,\n)\n\npytestmark = pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\n\n\nclass MyTestEvent(Event):\n    value: int\n\n\nclass OtherEvent(Event):\n    data: str\n\n\n@pytest.fixture\ndef base_state() -> BrokerState:\n    \"\"\"Create a minimal BrokerState for testing.\"\"\"\n    step_config = StepConfig(\n        accepted_events=[MyTestEvent, StartEvent],\n        event_name=\"ev\",\n        return_types=[StopEvent, OtherEvent, type(None)],\n        context_parameter=\"ctx\",\n        retry_policy=None,\n        num_workers=1,\n        resources=[],\n    )\n    return BrokerState(\n        is_running=True,\n        config=BrokerConfig(\n            steps={\n                \"test_step\": InternalStepConfig(\n                    accepted_events=[MyTestEvent, StartEvent],\n                    retry_policy=None,\n                    num_workers=1,\n                )\n            },\n            timeout=None,\n        ),\n        workers={\n            \"test_step\": InternalStepWorkerState(\n                queue=[],\n                config=step_config,\n                in_progress=[],\n                collected_events={},\n                collected_waiters=[],\n            )\n        },\n    )\n\n\ndef add_worker(state: BrokerState, event: Event, worker_id: int = 0) -> None:\n    \"\"\"Helper to add an in-progress worker to state.\"\"\"\n    state.workers[\"test_step\"].in_progress.append(\n        InProgressState(\n            event=event,\n            worker_id=worker_id,\n            shared_state=StepWorkerState(\n                step_name=\"test_step\",\n                collected_events={},\n                collected_waiters=[],\n            ),\n            attempts=0,\n            first_attempt_at=100.0,\n        )\n    )\n\n\ndef test_add_event_unhandled_emits_internal_event(base_state: BrokerState) -> None:\n    \"\"\"Unhandled events should emit UnhandledEvent with idle status.\"\"\"\n    tick = TickAddEvent(event=OtherEvent(data=\"unused\"), step_name=None)\n    state, commands = _process_add_event_tick(tick, base_state, now_seconds=0.0)\n\n    publish_events = [c.event for c in commands if isinstance(c, CommandPublishEvent)]\n    unhandled = [e for e in publish_events if isinstance(e, UnhandledEvent)]\n    assert len(unhandled) == 1\n    assert unhandled[0].event_type == \"OtherEvent\"\n    assert unhandled[0].qualified_name.endswith(\".OtherEvent\")\n    assert unhandled[0].step_name is None\n    assert unhandled[0].idle == _check_idle_state(state)\n\n\nclass CustomInputRequired(InputRequiredEvent):\n    \"\"\"Custom InputRequiredEvent subclass for testing.\"\"\"\n\n    prompt: str\n\n\ndef test_add_event_input_required_does_not_emit_unhandled(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"InputRequiredEvent subclasses should NOT emit UnhandledEvent.\n\n    InputRequiredEvent events are designed to be handled externally by human\n    consumers, not by workflow steps. They should not trigger UnhandledEvent.\n    \"\"\"\n    tick = TickAddEvent(event=CustomInputRequired(prompt=\"test\"), step_name=None)\n    _, commands = _process_add_event_tick(tick, base_state, now_seconds=0.0)\n\n    publish_events = [c.event for c in commands if isinstance(c, CommandPublishEvent)]\n    unhandled = [e for e in publish_events if isinstance(e, UnhandledEvent)]\n    assert len(unhandled) == 0\n\n\ndef test_add_event_base_input_required_does_not_emit_unhandled(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"Base InputRequiredEvent should also NOT emit UnhandledEvent.\"\"\"\n    tick = TickAddEvent(event=InputRequiredEvent(), step_name=None)\n    _, commands = _process_add_event_tick(tick, base_state, now_seconds=0.0)\n\n    publish_events = [c.event for c in commands if isinstance(c, CommandPublishEvent)]\n    unhandled = [e for e in publish_events if isinstance(e, UnhandledEvent)]\n    assert len(unhandled) == 0\n\n\ndef test_add_event_matches_waiter_does_not_emit_unhandled(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"Events that satisfy a waiter should not emit UnhandledEvent.\"\"\"\n    base_state.workers[\"test_step\"].collected_waiters.append(\n        StepWorkerWaiter(\n            waiter_id=\"waiter-1\",\n            event=StartEvent(),\n            waiting_for_event=OtherEvent,\n            requirements={},\n            has_requirements=False,\n            resolved_event=None,\n        )\n    )\n    tick = TickAddEvent(event=OtherEvent(data=\"hit\"), step_name=None)\n    _, commands = _process_add_event_tick(tick, base_state, now_seconds=0.0)\n\n    publish_events = [c.event for c in commands if isinstance(c, CommandPublishEvent)]\n    assert not any(isinstance(e, UnhandledEvent) for e in publish_events)\n\n\n@pytest.mark.parametrize(\n    \"result,expected_commands\",\n    [\n        (StopEvent(result=\"done\"), [StepStateChanged, StopEvent, CommandCompleteRun]),\n        (OtherEvent(data=\"next\"), [StepStateChanged, CommandQueueEvent]),\n        (\n            InputRequiredEvent(),\n            [StepStateChanged, InputRequiredEvent, CommandQueueEvent],\n        ),\n        (None, [StepStateChanged]),\n    ],\n)\ndef test_step_worker_results(\n    base_state: BrokerState, result: Event | None, expected_commands: list\n) -> None:\n    \"\"\"Test different step worker result types.\"\"\"\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    tick = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerResult(result=result)],\n    )\n\n    new_state, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n\n    # Check expected command types\n    for i, expected_type in enumerate(expected_commands):\n        if isinstance(expected_type, type) and issubclass(expected_type, Event):\n            command = commands[i]\n            assert isinstance(command, CommandPublishEvent)\n            assert isinstance(command.event, expected_type)\n        else:\n            assert isinstance(commands[i], expected_type)\n\n    # Worker should be removed from in_progress\n    assert len(new_state.workers[\"test_step\"].in_progress) == 0\n\n\ndef test_step_worker_failed_with_retry(base_state: BrokerState) -> None:\n    \"\"\"Test that failures with retry policy queue a retry.\"\"\"\n    base_state.workers[\"test_step\"].config.retry_policy = ConstantDelayRetryPolicy(\n        maximum_attempts=3, delay=1.0\n    )\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerFailed(exception=ValueError(\"test\"), failed_at=110.0)],\n    )\n\n    _, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n\n    # Should queue retry\n    queue_cmds = [c for c in commands if isinstance(c, CommandQueueEvent)]\n    assert len(queue_cmds) == 1\n    assert queue_cmds[0].attempts == 1\n\n    # First command should be NOT_RUNNING state change before re-queue\n    assert isinstance(commands[0], CommandPublishEvent)\n    assert isinstance(commands[0].event, StepStateChanged)\n    assert commands[0].event.step_state == StepState.NOT_RUNNING\n\n\ndef test_step_worker_failed_without_retry(base_state: BrokerState) -> None:\n    \"\"\"Test that failures without retry fail the workflow.\"\"\"\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerFailed(exception=ValueError(\"test\"), failed_at=110.0)],\n    )\n\n    new_state, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n\n    assert new_state.is_running is False\n    assert any(isinstance(c, CommandFailWorkflow) for c in commands)\n\n\ndef test_collected_events(base_state: BrokerState) -> None:\n    \"\"\"Test AddCollectedEvent and DeleteCollectedEvent.\"\"\"\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    # Add event\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[AddCollectedEvent(event_id=\"buf1\", event=OtherEvent(data=\"e1\"))],\n    )\n    new_state, _ = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n    assert \"buf1\" in new_state.workers[\"test_step\"].collected_events\n\n    # Delete event\n    add_worker(new_state, event)\n    tick = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[\n            StepWorkerResult(result=StopEvent()),\n            DeleteCollectedEvent(event_id=\"buf1\"),\n        ],\n    )\n    new_state, _ = _process_step_result_tick(tick, new_state, now_seconds=110.0)\n    assert \"buf1\" not in new_state.workers[\"test_step\"].collected_events\n\n\ndef test_waiters(base_state: BrokerState) -> None:\n    \"\"\"Test AddWaiter and DeleteWaiter.\"\"\"\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    result = AddWaiter(\n        waiter_id=\"w1\",\n        waiter_event=InputRequiredEvent(),\n        requirements={},\n        timeout=None,\n        event_type=OtherEvent,\n    )\n    # Add waiter\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[\n            cast(StepFunctionResult, result),\n        ],\n    )\n    new_state, _ = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n    assert len(new_state.workers[\"test_step\"].collected_waiters) == 1\n\n    # Delete waiter\n    add_worker(new_state, event)\n    tick = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[\n            StepWorkerResult(result=StopEvent()),\n            DeleteWaiter(waiter_id=\"w1\"),\n        ],\n    )\n    new_state, _ = _process_step_result_tick(tick, new_state, now_seconds=110.0)\n    assert len(new_state.workers[\"test_step\"].collected_waiters) == 0\n\n\ndef test_start_event_sets_running(base_state: BrokerState) -> None:\n    \"\"\"Test that StartEvent sets is_running to True.\"\"\"\n    base_state.is_running = False\n    tick = TickAddEvent(event=StartEvent())\n    new_state, _ = _process_add_event_tick(tick, base_state, now_seconds=100.0)\n    assert new_state.is_running is True\n\n\ndef test_event_routing(base_state: BrokerState) -> None:\n    \"\"\"Test that events are routed to accepting steps.\"\"\"\n    tick = TickAddEvent(event=MyTestEvent(value=42))\n    new_state, commands = _process_add_event_tick(tick, base_state, now_seconds=100.0)\n\n    run_cmds = [c for c in commands if isinstance(c, CommandRunWorker)]\n    assert len(run_cmds) == 1\n    assert run_cmds[0].step_name == \"test_step\"\n\n\ndef test_per_step_explicit_routing_accepts_only_matching_types(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"Explicit routing with step_name must still satisfy accepted event types.\"\"\"\n    # base_state only has test_step that accepts MyTestEvent and StartEvent\n    # Explicitly target test_step with MyTestEvent → should run\n    tick_ok = TickAddEvent(event=MyTestEvent(value=1), step_name=\"test_step\")\n    _, cmds_ok = _process_add_event_tick(tick_ok, base_state, now_seconds=100.0)\n    assert any(isinstance(c, CommandRunWorker) for c in cmds_ok)\n\n    # Explicitly target an unknown step → should not run anything\n    tick_bad = TickAddEvent(event=MyTestEvent(value=1), step_name=\"unknown\")\n    _, cmds_bad = _process_add_event_tick(tick_bad, base_state, now_seconds=100.0)\n    assert not any(isinstance(c, CommandRunWorker) for c in cmds_bad)\n\n\ndef test_explicit_routing_requires_acceptance(base_state: BrokerState) -> None:\n    \"\"\"Explicit step routing should still require accepted event types.\"\"\"\n    # Add a second step that does NOT accept MyTestEvent\n    other_step_cfg = StepConfig(\n        accepted_events=[StartEvent],\n        event_name=\"ev\",\n        return_types=[StopEvent, OtherEvent, type(None)],\n        context_parameter=\"ctx\",\n        retry_policy=None,\n        num_workers=1,\n        resources=[],\n    )\n    base_state.config.steps[\"other_step\"] = InternalStepConfig(\n        accepted_events=[StartEvent], retry_policy=None, num_workers=1\n    )\n    base_state.workers[\"other_step\"] = InternalStepWorkerState(\n        queue=[],\n        config=other_step_cfg,\n        in_progress=[],\n        collected_events={},\n        collected_waiters=[],\n    )\n\n    # Try to route MyTestEvent explicitly to non-accepting step → should not start\n    tick = TickAddEvent(event=MyTestEvent(value=1), step_name=\"other_step\")\n    _, commands = _process_add_event_tick(tick, base_state, now_seconds=100.0)\n    assert not any(\n        isinstance(c, CommandRunWorker) and c.step_name == \"other_step\"\n        for c in commands\n    )\n\n    # Explicitly route to accepting step → should start\n    tick_ok = TickAddEvent(event=MyTestEvent(value=2), step_name=\"test_step\")\n    _, commands_ok = _process_add_event_tick(tick_ok, base_state, now_seconds=100.0)\n    assert any(\n        isinstance(c, CommandRunWorker) and c.step_name == \"test_step\"\n        for c in commands_ok\n    )\n\n\ndef test_waiter_resolution(base_state: BrokerState) -> None:\n    \"\"\"Test that events matching waiters trigger step re-execution.\"\"\"\n    original_event = MyTestEvent(value=1)\n    waiter = StepWorkerWaiter(\n        waiter_id=\"w1\",\n        event=original_event,\n        waiting_for_event=OtherEvent,\n        requirements={\"data\": \"expected\"},\n        has_requirements=True,\n        resolved_event=None,\n    )\n    base_state.workers[\"test_step\"].collected_waiters.append(waiter)\n\n    tick = TickAddEvent(event=OtherEvent(data=\"expected\"))\n    new_state, commands = _process_add_event_tick(tick, base_state, now_seconds=100.0)\n\n    assert (\n        new_state.workers[\"test_step\"].collected_waiters[0].resolved_event is not None\n    )\n    run_cmds = [c for c in commands if isinstance(c, CommandRunWorker)]\n    assert any(c.event == original_event for c in run_cmds)\n\n\ndef test_step_state_changed_names(base_state: BrokerState) -> None:\n    \"\"\"Verify input/output event names on StepStateChanged use actual event types.\"\"\"\n    input_ev = MyTestEvent(value=7)\n    add_worker(base_state, input_ev)\n\n    # Return a regular Event → output_event_name should be its type, and input_event_name should be str(type(input))\n    tick = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=input_ev,\n        result=[StepWorkerResult(result=OtherEvent(data=\"x\"))],\n    )\n    _, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n    assert isinstance(commands[0], CommandPublishEvent)\n    assert isinstance(commands[0].event, StepStateChanged)\n    ev = commands[0].event\n    assert ev.input_event_name == str(type(input_ev))\n    assert ev.output_event_name == str(type(OtherEvent(data=\"x\")))\n\n    # Return StopEvent → output_event_name should be None\n    add_worker(base_state, input_ev)\n    tick2 = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=input_ev,\n        result=[StepWorkerResult(result=StopEvent(result=\"done\"))],\n    )\n    _, commands2 = _process_step_result_tick(tick2, base_state, now_seconds=110.0)\n    assert isinstance(commands2[0], CommandPublishEvent)\n    assert isinstance(commands2[0].event, StepStateChanged)\n    ev2 = commands2[0].event\n    assert ev2.input_event_name == str(type(input_ev))\n    assert ev2.output_event_name == \"<class 'workflows.events.StopEvent'>\"\n\n\ndef test_cancel_run(base_state: BrokerState) -> None:\n    \"\"\"Test that cancel sets not running and halts.\"\"\"\n    tick = TickCancelRun()\n    new_state, commands = _process_cancel_run_tick(tick, base_state)\n\n    # This is perhaps unintuitive, but it's important to be able to cancel and resume a workflow\n    # based on this state--Workflow uses this as a signal to determine whether to pass or construct\n    # a start event\n    assert new_state.is_running is True\n    assert len(commands) == 2\n    assert isinstance(commands[0], CommandPublishEvent)\n    assert isinstance(commands[1], CommandHalt)\n\n\ndef test_idle_release(base_state: BrokerState) -> None:\n    \"\"\"Test that idle release returns CommandCompleteRun with IdleReleasedEvent and no published events.\"\"\"\n    tick = TickIdleRelease()\n    new_state, commands = _reduce_tick(tick, base_state, 0.0)\n\n    # State is unchanged (returned early without deepcopy)\n    assert new_state is base_state\n    # Single command: complete run with IdleReleasedEvent\n    assert len(commands) == 1\n    assert isinstance(commands[0], CommandCompleteRun)\n    assert isinstance(commands[0].result, IdleReleasedEvent)\n    # No CommandPublishEvent — nothing written to event stream\n    assert not any(isinstance(c, CommandPublishEvent) for c in commands)\n\n\ndef test_publish_event(base_state: BrokerState) -> None:\n    \"\"\"Test that publish events pass through without state changes.\"\"\"\n    event = MyTestEvent(value=42)\n    tick = TickPublishEvent(event=event)\n    new_state, commands = _process_publish_event_tick(tick, base_state)\n\n    assert new_state is base_state\n    assert len(commands) == 1\n    assert isinstance(commands[0], CommandPublishEvent)\n\n\ndef test_timeout(base_state: BrokerState) -> None:\n    \"\"\"Test that timeout sets not running and halts with error.\"\"\"\n    tick = TickTimeout(timeout=10.0)\n    new_state, commands = _process_timeout_tick(tick, base_state)\n\n    assert new_state.is_running is False\n    assert isinstance(commands[1], CommandHalt)\n    assert isinstance(commands[1].exception, WorkflowTimeoutError)\n\n\ndef test_add_when_capacity_available(base_state: BrokerState) -> None:\n    \"\"\"Test that events start immediately when capacity available.\"\"\"\n    event = MyTestEvent(value=42)\n    commands = _add_or_enqueue_event(\n        EventAttempt(event=event),\n        \"test_step\",\n        base_state.workers[\"test_step\"],\n        now_seconds=100.0,\n    )\n\n    assert len(base_state.workers[\"test_step\"].in_progress) == 1\n    assert any(isinstance(c, CommandRunWorker) for c in commands)\n    assert any(\n        isinstance(c, CommandPublishEvent)\n        and isinstance(c.event, StepStateChanged)\n        and c.event.step_state == StepState.RUNNING\n        for c in commands\n    )\n\n\ndef test_enqueue_when_no_capacity(base_state: BrokerState) -> None:\n    \"\"\"Test that events queue when no capacity available.\"\"\"\n    # Fill capacity\n    add_worker(base_state, MyTestEvent(value=1))\n\n    # Try to add another\n    event = MyTestEvent(value=42)\n    commands = _add_or_enqueue_event(\n        EventAttempt(event=event),\n        \"test_step\",\n        base_state.workers[\"test_step\"],\n        now_seconds=100.0,\n    )\n\n    assert len(base_state.workers[\"test_step\"].queue) == 1\n    # PREPARING should be published when we enqueue\n    assert isinstance(commands[0], CommandPublishEvent)\n    assert isinstance(commands[0].event, StepStateChanged)\n    assert commands[0].event.step_state == StepState.PREPARING\n\n\ndef test_rewind_restarts_workers(base_state: BrokerState) -> None:\n    \"\"\"Test that in_progress workers are restarted.\"\"\"\n    base_state.workers[\"test_step\"].config.num_workers = 2\n    base_state.config.steps[\"test_step\"].num_workers = 2\n\n    add_worker(base_state, MyTestEvent(value=1), worker_id=0)\n    add_worker(base_state, MyTestEvent(value=2), worker_id=1)\n\n    new_state, commands = rewind_in_progress(base_state, now_seconds=120.0)\n\n    # Both should be restarted\n    run_cmds = [c for c in commands if isinstance(c, CommandRunWorker)]\n    assert len(run_cmds) == 2\n    assert len(new_state.workers[\"test_step\"].in_progress) == 2\n\n\ndef test_add_event_tick_preserves_retry_metadata(base_state: BrokerState) -> None:\n    \"\"\"Test that attempts and first_attempt_at are preserved from TickAddEvent.\"\"\"\n    now = 200.0\n    first_attempt_time = 100.0\n    attempts = 3\n\n    tick = TickAddEvent(\n        event=MyTestEvent(value=42),\n        attempts=attempts,\n        first_attempt_at=first_attempt_time,\n    )\n\n    new_state, commands = _process_add_event_tick(tick, base_state, now_seconds=now)\n\n    # Verify the worker was started\n    run_cmds = [c for c in commands if isinstance(c, CommandRunWorker)]\n    assert len(run_cmds) == 1\n\n    # Verify retry metadata was preserved in the InProgressState\n    in_progress = new_state.workers[\"test_step\"].in_progress\n    assert len(in_progress) == 1\n    assert in_progress[0].attempts == attempts\n    assert in_progress[0].first_attempt_at == first_attempt_time\n\n\ndef test_add_event_tick_uses_now_when_no_retry_metadata(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"Test that fresh events get attempts=0 and first_attempt_at=now.\"\"\"\n    now = 200.0\n\n    tick = TickAddEvent(event=MyTestEvent(value=42))  # No retry metadata\n\n    new_state, _ = _process_add_event_tick(tick, base_state, now_seconds=now)\n\n    in_progress = new_state.workers[\"test_step\"].in_progress\n    assert len(in_progress) == 1\n    assert in_progress[0].attempts == 0\n    assert in_progress[0].first_attempt_at == now\n\n\ndef test_step_worker_failed_retry_preserves_delay(base_state: BrokerState) -> None:\n    \"\"\"Test that CommandQueueEvent includes delay from retry policy.\"\"\"\n    retry_delay = 5.0\n    base_state.workers[\"test_step\"].config.retry_policy = ConstantDelayRetryPolicy(\n        maximum_attempts=3, delay=retry_delay\n    )\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerFailed(exception=ValueError(\"test\"), failed_at=110.0)],\n    )\n\n    _, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n\n    queue_cmds = [c for c in commands if isinstance(c, CommandQueueEvent)]\n    assert len(queue_cmds) == 1\n    assert queue_cmds[0].delay == retry_delay\n    assert queue_cmds[0].attempts == 1\n    assert queue_cmds[0].first_attempt_at == 100.0  # from add_worker fixture\n    assert queue_cmds[0].step_name == \"test_step\"\n\n\ndef test_step_worker_failed_retry_preserves_first_attempt_at(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"Test that first_attempt_at stays constant across retries.\"\"\"\n    base_state.workers[\"test_step\"].config.retry_policy = ConstantDelayRetryPolicy(\n        maximum_attempts=5, delay=1.0\n    )\n    event = MyTestEvent(value=42)\n\n    original_first_attempt_at = 50.0\n    # Simulate a worker that's already been retried twice\n    base_state.workers[\"test_step\"].in_progress.append(\n        InProgressState(\n            event=event,\n            worker_id=0,\n            shared_state=StepWorkerState(\n                step_name=\"test_step\",\n                collected_events={},\n                collected_waiters=[],\n            ),\n            attempts=2,  # Already retried twice\n            first_attempt_at=original_first_attempt_at,\n        )\n    )\n\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerFailed(exception=ValueError(\"test\"), failed_at=200.0)],\n    )\n\n    _, commands = _process_step_result_tick(tick, base_state, now_seconds=200.0)\n\n    queue_cmds = [c for c in commands if isinstance(c, CommandQueueEvent)]\n    assert len(queue_cmds) == 1\n    assert queue_cmds[0].attempts == 3  # incremented from 2\n    assert queue_cmds[0].first_attempt_at == original_first_attempt_at  # preserved!\n\n\ndef test_step_worker_failed_exponential_jitter_deterministic(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"Retry delay must be identical on two calls with the same run_id (DBOS replay determinism).\"\"\"\n    policy = ExponentialBackoffRetryPolicy(\n        initial_delay=1.0,\n        multiplier=2.0,\n        max_delay=60.0,\n        maximum_attempts=5,\n        jitter=True,\n    )\n    base_state.workers[\"test_step\"].config.retry_policy = policy\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerFailed(exception=ValueError(\"test\"), failed_at=110.0)],\n    )\n\n    run_id = \"run-determinism-test\"\n    failures = 1\n    jitter_seed = (\n        int(\n            hashlib.sha256(f\"{run_id}:test_step:{failures}\".encode()).hexdigest(),\n            16,\n        )\n        & 0xFFFF_FFFF\n    )\n    expected_delay = policy.next(\n        10.0,\n        failures,\n        ValueError(\"test\"),\n        seed=jitter_seed,\n    )\n\n    _, commands_first = _process_step_result_tick(\n        tick, base_state, now_seconds=110.0, run_id=run_id\n    )\n    _, commands_second = _process_step_result_tick(\n        tick, base_state, now_seconds=110.0, run_id=run_id\n    )\n\n    first_delay = next(\n        c for c in commands_first if isinstance(c, CommandQueueEvent)\n    ).delay\n    second_delay = next(\n        c for c in commands_second if isinstance(c, CommandQueueEvent)\n    ).delay\n    assert first_delay == second_delay == expected_delay\n\n\n# =============================================================================\n# Idle Workflow Tracking Tests\n# =============================================================================\n\n\ndef test_check_idle_state_not_running(base_state: BrokerState) -> None:\n    \"\"\"A workflow that is not running is not idle.\"\"\"\n    base_state.is_running = False\n    assert _check_idle_state(base_state) is False\n\n\ndef test_check_idle_state_has_queued_events(base_state: BrokerState) -> None:\n    \"\"\"A workflow with queued events is not idle.\"\"\"\n    base_state.workers[\"test_step\"].queue.append(\n        EventAttempt(event=MyTestEvent(value=1))\n    )\n    assert _check_idle_state(base_state) is False\n\n\ndef test_check_idle_state_has_in_progress(base_state: BrokerState) -> None:\n    \"\"\"A workflow with in-progress workers is not idle.\"\"\"\n    add_worker(base_state, MyTestEvent(value=1))\n    assert _check_idle_state(base_state) is False\n\n\ndef test_check_idle_state_no_work_is_idle(base_state: BrokerState) -> None:\n    \"\"\"A running workflow with empty queues and no in-progress work is idle.\"\"\"\n    assert _check_idle_state(base_state) is True\n\n\ndef test_check_idle_state_is_idle_with_waiter(base_state: BrokerState) -> None:\n    \"\"\"A running workflow with only waiters and no work is idle.\"\"\"\n    waiter = StepWorkerWaiter(\n        waiter_id=\"w1\",\n        event=MyTestEvent(value=1),\n        waiting_for_event=OtherEvent,\n        requirements={},\n        has_requirements=False,\n        resolved_event=None,\n    )\n    base_state.workers[\"test_step\"].collected_waiters.append(waiter)\n    assert _check_idle_state(base_state) is True\n\n\ndef test_step_result_does_not_emit_idle(base_state: BrokerState) -> None:\n    \"\"\"Step result tick never emits WorkflowIdleEvent directly.\n\n    Idle detection is handled at the runner level via TickIdleCheck, not in\n    the pure reducer. This test confirms the reducer doesn't emit idle.\n    \"\"\"\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerResult(result=None)],\n    )\n\n    new_state, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n\n    idle_commands = [\n        c\n        for c in commands\n        if isinstance(c, CommandPublishEvent) and isinstance(c.event, WorkflowIdleEvent)\n    ]\n    assert len(idle_commands) == 0\n    # State IS idle (no queued work, no in-progress), but emission is the runner's job\n    assert _check_idle_state(new_state) is True\n\n\ndef test_check_idle_state_multi_step_not_idle_if_one_has_work(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"With multiple steps, not idle if any step has work.\"\"\"\n    # Add a second step\n    other_step_cfg = StepConfig(\n        accepted_events=[OtherEvent],\n        event_name=\"ev\",\n        return_types=[StopEvent, type(None)],\n        context_parameter=\"ctx\",\n        retry_policy=None,\n        num_workers=1,\n        resources=[],\n    )\n    base_state.config.steps[\"other_step\"] = InternalStepConfig(\n        accepted_events=[OtherEvent], retry_policy=None, num_workers=1\n    )\n    base_state.workers[\"other_step\"] = InternalStepWorkerState(\n        queue=[],\n        config=other_step_cfg,\n        in_progress=[],\n        collected_events={},\n        collected_waiters=[],\n    )\n\n    # Add waiter to test_step (which alone would make it idle)\n    waiter = StepWorkerWaiter(\n        waiter_id=\"w1\",\n        event=MyTestEvent(value=1),\n        waiting_for_event=OtherEvent,\n        requirements={},\n        has_requirements=False,\n        resolved_event=None,\n    )\n    base_state.workers[\"test_step\"].collected_waiters.append(waiter)\n\n    # Without work in other_step, workflow is idle\n    assert _check_idle_state(base_state) is True\n\n    # Add in_progress work to other_step - now not idle\n    base_state.workers[\"other_step\"].in_progress.append(\n        InProgressState(\n            event=OtherEvent(data=\"test\"),\n            worker_id=0,\n            shared_state=StepWorkerState(\n                step_name=\"other_step\",\n                collected_events={},\n                collected_waiters=[],\n            ),\n            attempts=0,\n            first_attempt_at=100.0,\n        )\n    )\n    assert _check_idle_state(base_state) is False\n\n    # Or with queued work\n    base_state.workers[\"other_step\"].in_progress = []\n    base_state.workers[\"other_step\"].queue.append(\n        EventAttempt(event=OtherEvent(data=\"queued\"))\n    )\n    assert _check_idle_state(base_state) is False\n\n\ndef test_no_idle_event_when_work_remains(base_state: BrokerState) -> None:\n    \"\"\"WorkflowIdleEvent is not emitted if there's still work to do.\"\"\"\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    # Queue another event so work remains after processing\n    base_state.workers[\"test_step\"].queue.append(\n        EventAttempt(event=MyTestEvent(value=99))\n    )\n\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerResult(result=None)],  # Completes but queue has more\n    )\n\n    _, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n\n    idle_commands = [\n        c\n        for c in commands\n        if isinstance(c, CommandPublishEvent) and isinstance(c.event, WorkflowIdleEvent)\n    ]\n    assert len(idle_commands) == 0\n\n\ndef test_no_idle_event_when_workflow_completes(base_state: BrokerState) -> None:\n    \"\"\"WorkflowIdleEvent is not emitted when workflow completes (StopEvent).\"\"\"\n    event = MyTestEvent(value=42)\n    add_worker(base_state, event)\n\n    # Add a waiter\n    waiter = StepWorkerWaiter(\n        waiter_id=\"w1\",\n        event=event,\n        waiting_for_event=OtherEvent,\n        requirements={},\n        has_requirements=False,\n        resolved_event=None,\n    )\n    base_state.workers[\"test_step\"].collected_waiters.append(waiter)\n\n    # Complete the workflow with StopEvent\n    tick: TickStepResult = TickStepResult(\n        step_name=\"test_step\",\n        worker_id=0,\n        event=event,\n        result=[StepWorkerResult(result=StopEvent(result=\"done\"))],\n    )\n\n    new_state, commands = _process_step_result_tick(tick, base_state, now_seconds=110.0)\n\n    # Workflow is no longer running\n    assert new_state.is_running is False\n\n    # No idle event should be emitted\n    idle_commands = [\n        c\n        for c in commands\n        if isinstance(c, CommandPublishEvent) and isinstance(c.event, WorkflowIdleEvent)\n    ]\n    assert len(idle_commands) == 0\n\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Tests for rebuild_state_from_ticks\n# ─────────────────────────────────────────────────────────────────────────────\n\n\ndef test_rebuild_state_from_ticks_clears_in_progress(base_state: BrokerState) -> None:\n    \"\"\"\n    Test that rebuild_state_from_ticks clears in_progress before replaying ticks.\n\n    This is critical for checkpointing resumed workflows. When a workflow is resumed:\n    1. The checkpoint has in_progress workers with IDs like [1, 2, 3]\n    2. rewind_in_progress() clears in_progress and assigns new IDs [0, 1, 2]\n    3. New ticks reference the new worker IDs [0, 1, 2]\n    4. When checkpointing again, rebuild_state_from_ticks must also clear in_progress\n       before replaying ticks, otherwise worker IDs won't match.\n\n    Without the fix, this would raise: \"Worker 0 not found in in_progress\"\n    \"\"\"\n    event1 = MyTestEvent(value=1)\n    event2 = MyTestEvent(value=2)\n\n    # Simulate checkpoint state with in_progress workers using IDs 1, 2\n    # (as if they were mid-execution when checkpoint was taken)\n    shared_state = StepWorkerState(\n        step_name=\"test_step\",\n        collected_events={},\n        collected_waiters=[],\n    )\n    base_state.workers[\"test_step\"].in_progress = [\n        InProgressState(\n            event=event1,\n            worker_id=1,  # Original worker ID from checkpoint\n            shared_state=shared_state,\n            attempts=0,\n            first_attempt_at=100.0,\n        ),\n        InProgressState(\n            event=event2,\n            worker_id=2,  # Original worker ID from checkpoint\n            shared_state=shared_state,\n            attempts=0,\n            first_attempt_at=100.0,\n        ),\n    ]\n\n    # Simulate ticks from a resumed run where rewind_in_progress assigned new IDs\n    # These ticks reference worker IDs 0 and 1 (not 1 and 2 from checkpoint)\n    ticks: list[WorkflowTick] = [\n        # Worker 0 starts (after rewind assigned new ID)\n        TickAddEvent(event=event1),\n        # Worker 0 completes\n        TickStepResult(\n            step_name=\"test_step\",\n            worker_id=0,  # New ID assigned after rewind\n            event=event1,\n            result=[StepWorkerResult(result=OtherEvent(data=\"done1\"))],\n        ),\n        # Worker 1 starts (after rewind assigned new ID)\n        TickAddEvent(event=event2),\n        # Worker 1 completes\n        TickStepResult(\n            step_name=\"test_step\",\n            worker_id=0,  # Reuses ID 0 since previous worker completed\n            event=event2,\n            result=[StepWorkerResult(result=StopEvent(result=\"done2\"))],\n        ),\n    ]\n\n    # This should NOT raise \"Worker 0 not found in in_progress\"\n    # because rebuild_state_from_ticks now clears in_progress before replaying\n    final_state = rebuild_state_from_ticks(base_state, ticks)\n\n    # Verify the workflow completed\n    assert final_state.is_running is False\n    assert len(final_state.workers[\"test_step\"].in_progress) == 0\n\n\ndef test_rebuild_state_from_ticks_preserves_queue_order(\n    base_state: BrokerState,\n) -> None:\n    \"\"\"\n    Test that rebuild_state_from_ticks applies rewind_in_progress which moves\n    in_progress events to the front of the queue and then re-starts them.\n\n    Since the base fixture has num_workers=1, only one event can be in_progress\n    at a time. The originally in_progress event (event1) should be re-started\n    with worker_id=0, and event2 should remain in the queue.\n    \"\"\"\n    event1 = MyTestEvent(value=1)\n    event2 = MyTestEvent(value=2)\n\n    # State with in_progress worker\n    shared_state = StepWorkerState(\n        step_name=\"test_step\",\n        collected_events={},\n        collected_waiters=[],\n    )\n    base_state.workers[\"test_step\"].in_progress = [\n        InProgressState(\n            event=event1,\n            worker_id=0,\n            shared_state=shared_state,\n            attempts=2,  # Already retried twice\n            first_attempt_at=100.0,\n        ),\n    ]\n    # Also has queued event\n    base_state.workers[\"test_step\"].queue = [\n        EventAttempt(event=event2, attempts=0, first_attempt_at=None)\n    ]\n\n    # No ticks - test that rebuild applies rewind_in_progress\n    result = rebuild_state_from_ticks(base_state, [])\n\n    # rewind_in_progress re-starts workers, so event1 should be back in in_progress\n    # with worker_id=0 (reassigned) and retry info preserved\n    assert len(result.workers[\"test_step\"].in_progress) == 1\n    assert result.workers[\"test_step\"].in_progress[0].event == event1\n    assert result.workers[\"test_step\"].in_progress[0].worker_id == 0\n    assert result.workers[\"test_step\"].in_progress[0].attempts == 2\n    # Queue should have event2 (since num_workers=1, only 1 can be in_progress)\n    assert len(result.workers[\"test_step\"].queue) == 1\n    assert result.workers[\"test_step\"].queue[0].event == event2\n\n\nasync def _aiter(ticks: list[WorkflowTick]) -> AsyncIterator[WorkflowTick]:\n    for t in ticks:\n        yield t\n\n\ndef _simple_step_tick_sequence() -> list[WorkflowTick]:\n    event1 = MyTestEvent(value=1)\n    event2 = MyTestEvent(value=2)\n    return [\n        TickAddEvent(event=event1),\n        TickStepResult(\n            step_name=\"test_step\",\n            worker_id=0,\n            event=event1,\n            result=[StepWorkerResult(result=OtherEvent(data=\"done1\"))],\n        ),\n        TickAddEvent(event=event2),\n        TickStepResult(\n            step_name=\"test_step\",\n            worker_id=0,\n            event=event2,\n            result=[StepWorkerResult(result=StopEvent(result=\"done2\"))],\n        ),\n    ]\n\n\nasync def test_rebuild_state_from_ticks_stream_empty(base_state: BrokerState) -> None:\n    shared_state = StepWorkerState(\n        step_name=\"test_step\", collected_events={}, collected_waiters=[]\n    )\n    event1 = MyTestEvent(value=1)\n    base_state.workers[\"test_step\"].in_progress = [\n        InProgressState(\n            event=event1,\n            worker_id=0,\n            shared_state=shared_state,\n            attempts=1,\n            first_attempt_at=100.0,\n        ),\n    ]\n\n    streamed = await rebuild_state_from_ticks_stream(base_state, _aiter([]))\n\n    # rewind_in_progress re-assigns worker_id=0 starting fresh; in_progress preserved.\n    assert len(streamed.workers[\"test_step\"].in_progress) == 1\n    assert streamed.workers[\"test_step\"].in_progress[0].worker_id == 0\n    assert streamed.workers[\"test_step\"].in_progress[0].event == event1\n\n\nasync def test_rebuild_state_from_ticks_stream_single_tick(\n    base_state: BrokerState, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    # Freeze time so timestamp kludges don't diverge between paths.\n    monkeypatch.setattr(\"workflows.runtime.control_loop.time.time\", lambda: 12345.0)\n    ticks: list[WorkflowTick] = [TickAddEvent(event=MyTestEvent(value=42))]\n    streamed = await rebuild_state_from_ticks_stream(\n        base_state.deepcopy(), _aiter(ticks)\n    )\n    listed = rebuild_state_from_ticks(base_state.deepcopy(), ticks)\n    assert streamed == listed\n\n\nasync def test_rebuild_state_from_ticks_stream_multi_tick_equivalence(\n    base_state: BrokerState, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.setattr(\"workflows.runtime.control_loop.time.time\", lambda: 12345.0)\n    ticks = _simple_step_tick_sequence()\n    streamed = await rebuild_state_from_ticks_stream(\n        base_state.deepcopy(), _aiter(list(ticks))\n    )\n    listed = rebuild_state_from_ticks(base_state.deepcopy(), list(ticks))\n    assert streamed == listed\n    assert streamed.is_running is False\n\n\nasync def test_rebuild_state_from_ticks_stream_large_history_equivalence(\n    base_state: BrokerState, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.setattr(\"workflows.runtime.control_loop.time.time\", lambda: 12345.0)\n    ticks: list[WorkflowTick] = [\n        TickAddEvent(event=MyTestEvent(value=i)) for i in range(500)\n    ]\n    streamed = await rebuild_state_from_ticks_stream(\n        base_state.deepcopy(), _aiter(list(ticks))\n    )\n    listed = rebuild_state_from_ticks(base_state.deepcopy(), list(ticks))\n    assert streamed == listed\n\n\nasync def test_rebuild_state_from_ticks_stream_clears_in_progress(\n    base_state: BrokerState,\n) -> None:\n    event1 = MyTestEvent(value=1)\n    event2 = MyTestEvent(value=2)\n    shared_state = StepWorkerState(\n        step_name=\"test_step\", collected_events={}, collected_waiters=[]\n    )\n    base_state.workers[\"test_step\"].in_progress = [\n        InProgressState(\n            event=event1,\n            worker_id=1,\n            shared_state=shared_state,\n            attempts=0,\n            first_attempt_at=100.0,\n        ),\n        InProgressState(\n            event=event2,\n            worker_id=2,\n            shared_state=shared_state,\n            attempts=0,\n            first_attempt_at=100.0,\n        ),\n    ]\n    ticks: list[WorkflowTick] = [\n        TickAddEvent(event=event1),\n        TickStepResult(\n            step_name=\"test_step\",\n            worker_id=0,\n            event=event1,\n            result=[StepWorkerResult(result=OtherEvent(data=\"done1\"))],\n        ),\n        TickAddEvent(event=event2),\n        TickStepResult(\n            step_name=\"test_step\",\n            worker_id=0,\n            event=event2,\n            result=[StepWorkerResult(result=StopEvent(result=\"done2\"))],\n        ),\n    ]\n\n    final_state = await rebuild_state_from_ticks_stream(base_state, _aiter(ticks))\n\n    assert final_state.is_running is False\n    assert len(final_state.workers[\"test_step\"].in_progress) == 0\n\n\nasync def test_replay_ticks_stream_surfaces_stop_event(base_state: BrokerState) -> None:\n    ticks = _simple_step_tick_sequence()\n    replay = await replay_ticks_stream(base_state, _aiter(list(ticks)))\n    assert replay.state.is_running is False\n    assert isinstance(replay.exit_command, CommandCompleteRun)\n    assert isinstance(replay.exit_command.result, StopEvent)\n    assert replay.exit_command.result.result == \"done2\"\n\n\nasync def test_replay_ticks_stream_no_exit_command_when_running(\n    base_state: BrokerState,\n) -> None:\n    ticks: list[WorkflowTick] = [TickAddEvent(event=MyTestEvent(value=1))]\n    replay = await replay_ticks_stream(base_state, _aiter(ticks))\n    assert replay.exit_command is None\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/test_named_task.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"Tests for NamedTask (WorkerTask | PullTask).\n\nNamedTask associates asyncio tasks with stable string keys, providing:\n- Task identification for DBOS journaling\n- Task lookup by key for replay scenarios\n- Priority-based task selection (by list order)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import Any\n\nimport pytest\nfrom workflows.runtime.types.named_task import (\n    PULL_PREFIX,\n    PullTask,\n    WorkerTask,\n    all_tasks,\n    find_by_key,\n    get_key,\n    pick_highest_priority,\n)\n\n\nasync def _never_completes() -> None:\n    \"\"\"Coroutine that never completes, for creating pending tasks.\"\"\"\n    await asyncio.Future()\n\n\ndef create_pending_task() -> asyncio.Task[Any]:\n    \"\"\"Create a pending task that never completes.\"\"\"\n    return asyncio.create_task(_never_completes())\n\n\n# --- NamedTask creation ---\n\n\nasync def test_worker_task_creates_correct_key() -> None:\n    \"\"\"WorkerTask should create key as 'step_name:worker_id'.\"\"\"\n    task = create_pending_task()\n    try:\n        nt = WorkerTask(\"my_step\", 42, task)\n        assert nt.key == \"my_step:42\"\n        assert nt.task is task\n        assert isinstance(nt, WorkerTask)\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_pull_task_creates_correct_key() -> None:\n    \"\"\"PullTask should create key as '__pull__:sequence'.\"\"\"\n    task = create_pending_task()\n    try:\n        nt = PullTask(7, task)\n        assert nt.key == f\"{PULL_PREFIX}:7\"\n        assert nt.task is task\n        assert isinstance(nt, PullTask)\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\n# --- all_tasks ---\n\n\nasync def test_all_tasks_returns_set_of_tasks() -> None:\n    \"\"\"all_tasks should return a set of all tasks.\"\"\"\n    w1 = create_pending_task()\n    w2 = create_pending_task()\n    pull = create_pending_task()\n    try:\n        named_tasks = [\n            WorkerTask(\"step_a\", 0, w1),\n            WorkerTask(\"step_b\", 0, w2),\n            PullTask(0, pull),\n        ]\n        result = all_tasks(named_tasks)\n        assert len(result) == 3\n        assert w1 in result\n        assert w2 in result\n        assert pull in result\n    finally:\n        for t in [w1, w2, pull]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n\n\nasync def test_all_tasks_empty_list() -> None:\n    \"\"\"all_tasks should return empty set for empty list.\"\"\"\n    assert all_tasks([]) == set()\n\n\nasync def test_all_tasks_works_with_asyncio_wait() -> None:\n    \"\"\"all_tasks result should work with asyncio.wait.\"\"\"\n    task = create_pending_task()\n    try:\n        named_tasks = [WorkerTask(\"step\", 0, task)]\n        result = all_tasks(named_tasks)\n        done, pending = await asyncio.wait(result, timeout=0.001)\n        assert task in pending\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\n# --- find_by_key ---\n\n\nasync def test_find_by_key_returns_worker_task() -> None:\n    \"\"\"find_by_key should return the correct worker task.\"\"\"\n    w1 = create_pending_task()\n    w2 = create_pending_task()\n    try:\n        named_tasks = [\n            WorkerTask(\"step_a\", 0, w1),\n            WorkerTask(\"step_b\", 1, w2),\n        ]\n        assert find_by_key(named_tasks, \"step_a:0\") is w1\n        assert find_by_key(named_tasks, \"step_b:1\") is w2\n    finally:\n        for t in [w1, w2]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n\n\nasync def test_find_by_key_returns_pull_task() -> None:\n    \"\"\"find_by_key should return the pull task.\"\"\"\n    pull = create_pending_task()\n    try:\n        named_tasks = [PullTask(5, pull)]\n        assert find_by_key(named_tasks, f\"{PULL_PREFIX}:5\") is pull\n    finally:\n        pull.cancel()\n        try:\n            await pull\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_find_by_key_returns_none_for_unknown() -> None:\n    \"\"\"find_by_key should return None for unknown key.\"\"\"\n    task = create_pending_task()\n    try:\n        named_tasks = [WorkerTask(\"step\", 0, task)]\n        assert find_by_key(named_tasks, \"unknown:99\") is None\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_find_by_key_empty_list() -> None:\n    \"\"\"find_by_key should return None for empty list.\"\"\"\n    assert find_by_key([], \"any:key\") is None\n\n\n# --- get_key ---\n\n\nasync def test_get_key_returns_worker_key() -> None:\n    \"\"\"get_key should return the key for a worker task.\"\"\"\n    task = create_pending_task()\n    try:\n        named_tasks = [WorkerTask(\"my_step\", 3, task)]\n        assert get_key(named_tasks, task) == \"my_step:3\"\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_get_key_returns_pull_key() -> None:\n    \"\"\"get_key should return the key for a pull task.\"\"\"\n    task = create_pending_task()\n    try:\n        named_tasks = [PullTask(2, task)]\n        assert get_key(named_tasks, task) == f\"{PULL_PREFIX}:2\"\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_get_key_raises_for_unknown_task() -> None:\n    \"\"\"get_key should raise KeyError for unknown task.\"\"\"\n    known = create_pending_task()\n    unknown = create_pending_task()\n    try:\n        named_tasks = [WorkerTask(\"step\", 0, known)]\n        with pytest.raises(KeyError):\n            get_key(named_tasks, unknown)\n    finally:\n        for t in [known, unknown]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n\n\n# --- get_key / find_by_key round trip ---\n\n\nasync def test_round_trip_worker() -> None:\n    \"\"\"get_key and find_by_key should be inverses for workers.\"\"\"\n    task = create_pending_task()\n    try:\n        named_tasks = [WorkerTask(\"step\", 5, task)]\n        key = get_key(named_tasks, task)\n        found = find_by_key(named_tasks, key)\n        assert found is task\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_round_trip_pull() -> None:\n    \"\"\"get_key and find_by_key should be inverses for pull.\"\"\"\n    task = create_pending_task()\n    try:\n        named_tasks = [PullTask(9, task)]\n        key = get_key(named_tasks, task)\n        found = find_by_key(named_tasks, key)\n        assert found is task\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\n# --- pick_highest_priority ---\n\n\nasync def test_pick_highest_priority_respects_list_order() -> None:\n    \"\"\"pick_highest_priority should return first completed task in list order.\"\"\"\n    t1 = create_pending_task()\n    t2 = create_pending_task()\n    t3 = create_pending_task()\n    try:\n        named_tasks = [\n            WorkerTask(\"step_b\", 0, t2),\n            WorkerTask(\"step_a\", 0, t1),\n            PullTask(0, t3),\n        ]\n        done = {t2, t3}\n        result = pick_highest_priority(named_tasks, done)\n        assert result is t2\n    finally:\n        for t in [t1, t2, t3]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n\n\nasync def test_pick_highest_priority_workers_before_pull() -> None:\n    \"\"\"Workers listed first should have priority over pull.\"\"\"\n    worker = create_pending_task()\n    pull = create_pending_task()\n    try:\n        named_tasks = [\n            WorkerTask(\"step\", 0, worker),\n            PullTask(0, pull),\n        ]\n        done = {worker, pull}\n        result = pick_highest_priority(named_tasks, done)\n        assert result is worker\n    finally:\n        for t in [worker, pull]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n\n\nasync def test_pick_highest_priority_returns_pull_when_only_pull_done() -> None:\n    \"\"\"Should return pull if it's the only completed task.\"\"\"\n    worker = create_pending_task()\n    pull = create_pending_task()\n    try:\n        named_tasks = [\n            WorkerTask(\"step\", 0, worker),\n            PullTask(0, pull),\n        ]\n        done = {pull}\n        result = pick_highest_priority(named_tasks, done)\n        assert result is pull\n    finally:\n        for t in [worker, pull]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n\n\nasync def test_pick_highest_priority_empty_done() -> None:\n    \"\"\"Should return None when done set is empty.\"\"\"\n    task = create_pending_task()\n    try:\n        named_tasks = [WorkerTask(\"step\", 0, task)]\n        result = pick_highest_priority(named_tasks, set())\n        assert result is None\n    finally:\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_pick_highest_priority_no_match_raises() -> None:\n    \"\"\"Should raise ValueError when done is non-empty but no tasks match.\"\"\"\n    task1 = create_pending_task()\n    task2 = create_pending_task()\n    try:\n        named_tasks = [WorkerTask(\"step\", 0, task1)]\n        done = {task2}\n        with pytest.raises(ValueError, match=\"No tasks in done set match\"):\n            pick_highest_priority(named_tasks, done)\n    finally:\n        for t in [task1, task2]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n\n\n# --- Integration ---\n\n\nasync def test_integration_with_asyncio_wait() -> None:\n    \"\"\"Full integration: create tasks, wait, pick priority, get key.\"\"\"\n\n    async def quick() -> str:\n        return \"done\"\n\n    worker = asyncio.create_task(quick())\n    pull = create_pending_task()\n    try:\n        named_tasks = [\n            WorkerTask(\"fast_step\", 0, worker),\n            PullTask(0, pull),\n        ]\n\n        tasks = all_tasks(named_tasks)\n        done, _ = await asyncio.wait(tasks, timeout=1.0)\n\n        assert worker in done\n        completed = pick_highest_priority(named_tasks, done)\n        assert completed is not None\n        assert completed is worker\n\n        key = get_key(named_tasks, completed)\n        assert key == \"fast_step:0\"\n    finally:\n        pull.cancel()\n        try:\n            await pull\n        except asyncio.CancelledError:\n            pass\n\n\nasync def test_multiple_workers_same_step() -> None:\n    \"\"\"Should handle multiple workers for the same step (num_workers > 1).\"\"\"\n    w0 = create_pending_task()\n    w1 = create_pending_task()\n    try:\n        named_tasks = [\n            WorkerTask(\"parallel_step\", 0, w0),\n            WorkerTask(\"parallel_step\", 1, w1),\n        ]\n\n        assert find_by_key(named_tasks, \"parallel_step:0\") is w0\n        assert find_by_key(named_tasks, \"parallel_step:1\") is w1\n        assert get_key(named_tasks, w0) == \"parallel_step:0\"\n        assert get_key(named_tasks, w1) == \"parallel_step:1\"\n    finally:\n        for t in [w0, w1]:\n            t.cancel()\n            try:\n                await t\n            except asyncio.CancelledError:\n                pass\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/test_runtime_lifecycle.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for Runtime lifecycle (registering context manager, context variable).\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.plugins import BasicRuntime, basic_runtime, get_current_runtime\nfrom workflows.runtime.types.plugin import (\n    Runtime,\n    _current_runtime,\n)\n\n\nclass SimpleWorkflow(Workflow):\n    @step\n    async def start(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\ndef test_basic_runtime_is_runtime_instance() -> None:\n    \"\"\"BasicRuntime extends Runtime ABC.\"\"\"\n    assert isinstance(basic_runtime, Runtime)\n\n\ndef test_basic_runtime_has_lifecycle_methods() -> None:\n    \"\"\"BasicRuntime has launch() and destroy() methods.\"\"\"\n    runtime = BasicRuntime()\n    # Should not raise\n    runtime.launch_sync()\n    runtime.destroy_sync()\n\n\ndef test_get_current_runtime_returns_basic_runtime_by_default() -> None:\n    \"\"\"When no context-scoped runtime, get_current_runtime returns basic_runtime.\"\"\"\n    runtime = get_current_runtime()\n    assert runtime is basic_runtime\n\n\ndef test_registering_sets_current_runtime() -> None:\n    \"\"\"registering() context manager sets the context-scoped runtime.\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with custom_runtime.registering():\n        current = _current_runtime.get()\n        assert current is custom_runtime\n\n\ndef test_registering_resets_on_exit() -> None:\n    \"\"\"registering() context manager resets the context variable on exit.\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with custom_runtime.registering():\n        pass\n\n    current = _current_runtime.get()\n    assert current is None\n\n\ndef test_registering_returns_runtime() -> None:\n    \"\"\"registering() context manager returns the runtime when entering.\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with custom_runtime.registering() as r:\n        assert r is custom_runtime\n\n\ndef test_registering_does_not_call_launch_on_exit() -> None:\n    \"\"\"registering() context manager does NOT call launch() on exit.\"\"\"\n    launched = False\n\n    class TrackingRuntime(BasicRuntime):\n        async def launch(self) -> None:\n            nonlocal launched\n            launched = True\n\n    with TrackingRuntime().registering():\n        pass\n\n    # Key change: launch() should NOT be called\n    assert not launched\n\n\ndef test_registering_does_not_call_launch_on_exception() -> None:\n    \"\"\"registering() context manager does NOT call launch() when exception is raised.\"\"\"\n    launched = False\n\n    class TrackingRuntime(BasicRuntime):\n        async def launch(self) -> None:\n            nonlocal launched\n            launched = True\n\n    with pytest.raises(ValueError):\n        with TrackingRuntime().registering():\n            raise ValueError(\"test error\")\n\n    assert not launched\n\n\ndef test_get_current_runtime_returns_context_runtime_when_set() -> None:\n    \"\"\"get_current_runtime returns context-scoped runtime when set.\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with custom_runtime.registering():\n        assert get_current_runtime() is custom_runtime\n\n\ndef test_nested_registering_contexts() -> None:\n    \"\"\"Nested registering() context managers restore correctly.\"\"\"\n    outer_runtime = BasicRuntime()\n    inner_runtime = BasicRuntime()\n\n    with outer_runtime.registering():\n        assert get_current_runtime() is outer_runtime\n\n        with inner_runtime.registering():\n            assert get_current_runtime() is inner_runtime\n\n        # After inner exits, should restore to outer\n        assert get_current_runtime() is outer_runtime\n\n    # After outer exits, should restore to basic_runtime\n    assert get_current_runtime() is basic_runtime\n\n\ndef test_explicit_runtime_parameter() -> None:\n    \"\"\"Workflow with runtime= uses that runtime.\"\"\"\n    custom_runtime = BasicRuntime()\n    wf = SimpleWorkflow(runtime=custom_runtime)\n    assert wf.runtime is custom_runtime\n\n\ndef test_registering_context_manager() -> None:\n    \"\"\"Workflows inside registering() use context-scoped runtime.\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with custom_runtime.registering():\n        wf = SimpleWorkflow()\n        assert wf.runtime is custom_runtime\n\n\ndef test_explicit_overrides_registering() -> None:\n    \"\"\"Explicit runtime= takes precedence over registering() context.\"\"\"\n    context_runtime = BasicRuntime()\n    explicit_runtime = BasicRuntime()\n\n    with context_runtime.registering():\n        wf = SimpleWorkflow(runtime=explicit_runtime)\n        assert wf.runtime is explicit_runtime\n\n\ndef test_fallback_to_basic_runtime() -> None:\n    \"\"\"Workflow without runtime uses basic_runtime.\"\"\"\n    wf = SimpleWorkflow()\n    assert wf.runtime is basic_runtime\n\n\ndef test_workflow_runs_after_registering_exit() -> None:\n    \"\"\"Workflow can run after registering() exits (requires explicit launch()).\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with custom_runtime.registering():\n        SimpleWorkflow()\n\n    # Explicit launch() should be called before running\n    custom_runtime.launch_sync()\n    # BasicRuntime doesn't actually need launch(), but the pattern should work\n\n\ndef test_registering_yields_runtime() -> None:\n    \"\"\"registering() context manager yields the runtime.\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with custom_runtime.registering() as r:\n        assert r is custom_runtime\n        assert isinstance(r, Runtime)\n\n\ndef test_empty_registering_block() -> None:\n    \"\"\"Empty registering() block is valid no-op.\"\"\"\n    custom_runtime = BasicRuntime()\n\n    # Should not raise\n    with custom_runtime.registering():\n        pass\n\n    # Context should be reset\n    assert get_current_runtime() is basic_runtime\n\n\ndef test_registering_with_exception_still_resets_context() -> None:\n    \"\"\"Context reset even on exception inside registering().\"\"\"\n    custom_runtime = BasicRuntime()\n\n    with pytest.raises(RuntimeError):\n        with custom_runtime.registering():\n            assert get_current_runtime() is custom_runtime\n            raise RuntimeError(\"test\")\n\n    # Context should still be reset after exception\n    assert get_current_runtime() is basic_runtime\n\n\ndef test_basic_runtime_is_always_launched() -> None:\n    \"\"\"BasicRuntime is always launched — no explicit launch() needed.\"\"\"\n    runtime = BasicRuntime()\n    assert runtime.is_launched is True\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/test_state.py",
    "content": "from json import JSONDecodeError\n\nimport pytest\nfrom workflows.context.context_types import SerializedContext, SerializedContextV0\nfrom workflows.workflow import Workflow\n\n\ndef test_deserialize_broken_state_raises_validation_error(workflow: Workflow) -> None:\n    \"\"\"Test that broken V0 state raises an error when deserializing.\"\"\"\n    broken_state = {\n        \"state\": {},\n        \"streaming_queue\": \"[]\",\n        \"queues\": {\"middle_step\": \"not-deserializable-as-a-queue\"},\n        \"event_buffers\": {},\n        \"in_progress\": {},\n        \"accepted_events\": [],\n        \"broker_log\": [],\n        \"is_running\": True,\n        \"waiting_ids\": [],\n    }\n\n    # This is V0 format (no version field)\n    serialized_v0 = SerializedContextV0.model_validate(broken_state)\n\n    # The broken queue string should cause an error during V0->V1 conversion\n    # because the queue value is not valid JSON\n    with pytest.raises(JSONDecodeError):\n        SerializedContext.from_v0(serialized_v0)\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/test_tick_serialization.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport json\nimport time\n\nimport pytest\nfrom pydantic import TypeAdapter\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StopEvent,\n    _deserialize_event,\n    _deserialize_event_type,\n    _deserialize_exception,\n    _serialize_event,\n    _serialize_event_type,\n    _serialize_exception,\n)\nfrom workflows.runtime.types.results import (\n    AddCollectedEvent,\n    AddWaiter,\n    DeleteCollectedEvent,\n    DeleteWaiter,\n    StepWorkerFailed,\n    StepWorkerResult,\n)\nfrom workflows.runtime.types.ticks import (\n    TickAddEvent,\n    TickCancelRun,\n    TickPublishEvent,\n    TickStepResult,\n    TickTimeout,\n    WorkflowTick,\n)\n\n\nclass MyEvent(Event):\n    value: str = \"hello\"\n\n\n# -- Serialization helper roundtrip tests --\n\n\ndef test_event_roundtrip() -> None:\n    event = MyEvent(value=\"world\")\n    serialized = _serialize_event(event)\n    result = _deserialize_event(serialized)\n    assert isinstance(result, MyEvent)\n    assert result.value == \"world\"\n\n\ndef test_exception_roundtrip() -> None:\n    exc = ValueError(\"something went wrong\")\n    serialized = _serialize_exception(exc)\n    result = _deserialize_exception(serialized)\n    assert isinstance(result, ValueError)\n    assert str(result) == \"something went wrong\"\n\n\ndef test_exception_roundtrip_unimportable() -> None:\n    CustomError = type(\"CustomError\", (Exception,), {})\n    exc = CustomError(\"oops\")\n    serialized = _serialize_exception(exc)\n    result = _deserialize_exception(serialized)\n    assert type(result) is Exception\n    assert str(result) == \"oops\"\n\n\ndef test_event_type_roundtrip() -> None:\n    serialized = _serialize_event_type(MyEvent)\n    result = _deserialize_event_type(serialized)\n    assert result is MyEvent\n\n\n# -- Tick roundtrip tests --\n\n\n@pytest.mark.parametrize(\n    \"tick\",\n    [\n        pytest.param(\n            TickAddEvent(\n                event=StartEvent(),\n                step_name=\"my_step\",\n                attempts=3,\n                first_attempt_at=1234567890.0,\n            ),\n            id=\"add_event\",\n        ),\n        pytest.param(\n            TickPublishEvent(event=MyEvent(value=\"world\")),\n            id=\"publish_event\",\n        ),\n        pytest.param(\n            TickCancelRun(),\n            id=\"cancel_run\",\n        ),\n        pytest.param(\n            TickTimeout(timeout=30.5),\n            id=\"timeout\",\n        ),\n        pytest.param(\n            TickStepResult(\n                step_name=\"process\",\n                worker_id=42,\n                event=MyEvent(value=\"trigger\"),\n                result=[StepWorkerResult(result=StopEvent(result=\"done\"))],\n            ),\n            id=\"step_result_with_event\",\n        ),\n        pytest.param(\n            TickStepResult(\n                step_name=\"process\",\n                worker_id=1,\n                event=StartEvent(),\n                result=[StepWorkerResult(result=None)],\n            ),\n            id=\"step_result_with_none\",\n        ),\n        pytest.param(\n            TickStepResult(\n                step_name=\"collector\",\n                worker_id=2,\n                event=StartEvent(),\n                result=[\n                    AddCollectedEvent(\n                        event_id=\"evt-1\", event=MyEvent(value=\"collected\")\n                    )\n                ],\n            ),\n            id=\"step_result_add_collected_event\",\n        ),\n        pytest.param(\n            TickStepResult(\n                step_name=\"collector\",\n                worker_id=3,\n                event=StartEvent(),\n                result=[DeleteCollectedEvent(event_id=\"evt-2\")],\n            ),\n            id=\"step_result_delete_collected_event\",\n        ),\n        pytest.param(\n            TickStepResult(\n                step_name=\"cleanup\",\n                worker_id=5,\n                event=StartEvent(),\n                result=[DeleteWaiter(waiter_id=\"w-2\")],\n            ),\n            id=\"step_result_delete_waiter\",\n        ),\n    ],\n)\ndef test_tick_roundtrip(tick: WorkflowTick) -> None:\n    serialized = tick.model_dump(mode=\"json\")\n    roundtripped = json.loads(json.dumps(serialized))\n    result = type(tick).model_validate(roundtripped)\n    assert result == tick\n\n\n# -- Tick roundtrip tests with lossy serialization --\n\n\ndef test_tick_step_result_with_failed_value_error() -> None:\n    failed_at = time.time()\n    tick = TickStepResult(\n        step_name=\"broken_step\",\n        worker_id=7,\n        event=StartEvent(),\n        result=[\n            StepWorkerFailed(\n                exception=ValueError(\"something went wrong\"), failed_at=failed_at\n            )\n        ],\n    )\n    serialized = tick.model_dump(mode=\"json\")\n    roundtripped = json.loads(json.dumps(serialized))\n    result = TickStepResult.model_validate(roundtripped)\n\n    assert isinstance(result, TickStepResult)\n    r = result.result[0]\n    assert isinstance(r, StepWorkerFailed)\n    assert isinstance(r.exception, ValueError)\n    assert str(r.exception) == \"something went wrong\"\n    assert r.failed_at == failed_at\n\n\ndef test_tick_step_result_with_failed_unimportable_exception() -> None:\n    CustomError = type(\"CustomError\", (Exception,), {})\n    failed_at = time.time()\n    tick = TickStepResult(\n        step_name=\"broken_step\",\n        worker_id=8,\n        event=StartEvent(),\n        result=[StepWorkerFailed(exception=CustomError(\"oops\"), failed_at=failed_at)],\n    )\n    serialized = tick.model_dump(mode=\"json\")\n    roundtripped = json.loads(json.dumps(serialized))\n    result = TickStepResult.model_validate(roundtripped)\n\n    assert isinstance(result, TickStepResult)\n    r = result.result[0]\n    assert isinstance(r, StepWorkerFailed)\n    assert type(r.exception) is Exception\n    assert str(r.exception) == \"oops\"\n    assert r.failed_at == failed_at\n\n\ndef test_tick_step_result_with_add_waiter() -> None:\n    tick = TickStepResult(\n        step_name=\"waiter_step\",\n        worker_id=4,\n        event=StartEvent(),\n        result=[\n            AddWaiter(\n                waiter_id=\"w-1\",\n                waiter_event=MyEvent(value=\"waiting\"),\n                requirements={\"key\": \"value\"},\n                timeout=60.0,\n                event_type=MyEvent,\n            )\n        ],\n    )\n    serialized = tick.model_dump(mode=\"json\")\n\n    # Verify the serialized form captures has_requirements correctly\n    waiter_data = serialized[\"result\"][0]\n    assert waiter_data[\"has_requirements\"] is True\n    assert waiter_data[\"requirements\"] == {}\n\n    roundtripped = json.loads(json.dumps(serialized))\n    result = TickStepResult.model_validate(roundtripped)\n\n    assert isinstance(result, TickStepResult)\n    r = result.result[0]\n    assert isinstance(r, AddWaiter)\n    assert r.waiter_id == \"w-1\"\n    assert isinstance(r.waiter_event, MyEvent)\n    assert r.waiter_event.value == \"waiting\"\n    # Requirements are always {} after deserialization\n    assert r.requirements == {}\n    assert r.timeout == 60.0\n    assert r.event_type is MyEvent\n\n\n# -- WorkflowTick discriminated union tests --\n\n\ndef test_workflow_tick_discriminated_union_roundtrip() -> None:\n    \"\"\"Verify that WorkflowTick TypeAdapter can roundtrip all tick types.\"\"\"\n    adapter = TypeAdapter(WorkflowTick)\n\n    ticks = [\n        TickAddEvent(event=StartEvent(), step_name=\"s\"),\n        TickPublishEvent(event=MyEvent(value=\"x\")),\n        TickCancelRun(),\n        TickTimeout(timeout=10.0),\n        TickStepResult(\n            step_name=\"s\",\n            worker_id=0,\n            event=StartEvent(),\n            result=[StepWorkerResult(result=None)],\n        ),\n    ]\n    for tick in ticks:\n        dumped = adapter.dump_python(tick, mode=\"json\")\n        roundtripped = json.loads(json.dumps(dumped))\n        restored = adapter.validate_python(roundtripped)\n        assert type(restored) is type(tick)\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/runtime/test_workflow_set_and_tracking.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for WorkflowSet, runtime tracking, and workflow mutation.\"\"\"\n\nfrom __future__ import annotations\n\nimport gc\nfrom typing import Any\n\nimport pytest\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.plugins import BasicRuntime\nfrom workflows.runtime.types.plugin import WorkflowSet\n\n\nclass SimpleWorkflow(Workflow):\n    @step\n    async def start(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\nclass UnhashableWorkflow(Workflow):\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self.data = [1, 2, 3]  # Makes it unhashable\n\n    @step\n    async def start(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\n@pytest.fixture\ndef basic_runtime() -> BasicRuntime:\n    return BasicRuntime()\n\n\n@pytest.fixture\ndef workflow_set() -> WorkflowSet:\n    return WorkflowSet()\n\n\n# ---------------------------------------------------------------------------\n# WorkflowSet tests\n# ---------------------------------------------------------------------------\n\n\ndef test_workflow_set_add_and_contains(\n    workflow_set: WorkflowSet, basic_runtime: BasicRuntime\n) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    workflow_set.add(wf)\n    assert wf in workflow_set\n\n\ndef test_workflow_set_add_unhashable_workflow(\n    workflow_set: WorkflowSet, basic_runtime: BasicRuntime\n) -> None:\n    wf = UnhashableWorkflow(runtime=basic_runtime)\n    workflow_set.add(wf)\n    assert wf in workflow_set\n    items = list(workflow_set)\n    assert wf in items\n\n\ndef test_workflow_set_discard(\n    workflow_set: WorkflowSet, basic_runtime: BasicRuntime\n) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    workflow_set.add(wf)\n    assert wf in workflow_set\n    workflow_set.discard(wf)\n    assert wf not in workflow_set\n\n\ndef test_workflow_set_len_and_bool(\n    workflow_set: WorkflowSet, basic_runtime: BasicRuntime\n) -> None:\n    assert not workflow_set\n    assert len(workflow_set) == 0\n\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    workflow_set.add(wf)\n    assert workflow_set\n    assert len(workflow_set) == 1\n\n\ndef test_workflow_set_iter(\n    workflow_set: WorkflowSet, basic_runtime: BasicRuntime\n) -> None:\n    wf1 = SimpleWorkflow(runtime=basic_runtime)\n    wf2 = SimpleWorkflow(runtime=basic_runtime)\n    wf3 = SimpleWorkflow(runtime=basic_runtime)\n    workflow_set.add(wf1)\n    workflow_set.add(wf2)\n    workflow_set.add(wf3)\n    items = set(id(w) for w in workflow_set)\n    assert items == {id(wf1), id(wf2), id(wf3)}\n\n\ndef test_workflow_set_gc_cleanup(\n    workflow_set: WorkflowSet, basic_runtime: BasicRuntime\n) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    workflow_set.add(wf)\n    assert len(workflow_set) == 1\n    del wf\n    gc.collect()\n    assert len(workflow_set) == 0\n\n\ndef test_workflow_set_add_idempotent(\n    workflow_set: WorkflowSet, basic_runtime: BasicRuntime\n) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    workflow_set.add(wf)\n    workflow_set.add(wf)\n    assert len(workflow_set) == 1\n\n\n# ---------------------------------------------------------------------------\n# Runtime tracking tests\n# ---------------------------------------------------------------------------\n\n\ndef test_track_workflow_adds_to_set(basic_runtime: BasicRuntime) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    assert wf in basic_runtime._pending\n\n\ndef test_untrack_workflow_removes_from_set(basic_runtime: BasicRuntime) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    assert wf in basic_runtime._pending\n    basic_runtime.untrack_workflow(wf)\n    assert wf not in basic_runtime._pending\n\n\ndef test_launch_locks_tracked_workflows(basic_runtime: BasicRuntime) -> None:\n    wf1 = SimpleWorkflow(runtime=basic_runtime)\n    wf2 = SimpleWorkflow(runtime=basic_runtime)\n    basic_runtime.launch_sync()\n    assert wf1._runtime_locked is True\n    assert wf2._runtime_locked is True\n\n\ndef test_relaunch_locks_new_workflows(basic_runtime: BasicRuntime) -> None:\n    wf1 = SimpleWorkflow(runtime=basic_runtime)\n    basic_runtime.launch_sync()\n    assert wf1._runtime_locked is True\n\n    wf2 = SimpleWorkflow(runtime=basic_runtime)\n    assert wf2._runtime_locked is False\n    basic_runtime.launch_sync()\n    assert wf1._runtime_locked is True\n    assert wf2._runtime_locked is True\n\n\ndef test_weak_reference_cleanup(basic_runtime: BasicRuntime) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    assert len(basic_runtime._pending) == 1\n    del wf\n    gc.collect()\n    assert len(basic_runtime._pending) == 0\n\n\ndef test_basic_runtime_launch_sets_launched_flag(basic_runtime: BasicRuntime) -> None:\n    assert basic_runtime._launched is False\n    basic_runtime.launch_sync()\n    assert basic_runtime._launched is True\n\n\n# ---------------------------------------------------------------------------\n# Workflow mutation tests\n# ---------------------------------------------------------------------------\n\n\ndef test_workflow_name_setter(basic_runtime: BasicRuntime) -> None:\n    wf = SimpleWorkflow(runtime=basic_runtime)\n    wf._switch_workflow_name(\"custom-name\")\n    assert wf.workflow_name == \"custom-name\"\n\n\ndef test_workflow_name_setter_raises_after_launch() -> None:\n    rt = BasicRuntime()\n    wf = SimpleWorkflow(runtime=rt)\n    wf._switch_workflow_name(\"before-launch\")\n    assert wf.workflow_name == \"before-launch\"\n\n    rt.launch_sync()\n    with pytest.raises(RuntimeError, match=\"Cannot change workflow_name\"):\n        wf._switch_workflow_name(\"after-launch\")\n\n\ndef test_runtime_setter_swaps_tracking() -> None:\n    rt1 = BasicRuntime()\n    rt2 = BasicRuntime()\n    wf = SimpleWorkflow(runtime=rt1)\n    assert wf in rt1._pending\n    assert wf not in rt2._pending\n\n    wf._switch_runtime(rt2)\n    assert wf not in rt1._pending\n    assert wf in rt2._pending\n\n\ndef test_runtime_setter_post_launch_raises() -> None:\n    rt1 = BasicRuntime()\n    rt2 = BasicRuntime()\n    wf = SimpleWorkflow(runtime=rt1)\n    rt1.launch_sync()\n\n    with pytest.raises(RuntimeError, match=\"Cannot reassign runtime\"):\n        wf._switch_runtime(rt2)\n\n\ndef test_runtime_setter_same_runtime_after_launch_is_noop() -> None:\n    rt = BasicRuntime()\n    wf = SimpleWorkflow(runtime=rt)\n    rt.launch_sync()\n    # Assigning the same runtime should not raise\n    wf._switch_runtime(rt)\n    assert wf.runtime is rt\n\n\n@pytest.mark.asyncio\nasync def test_run_locks_runtime() -> None:\n    rt1 = BasicRuntime()\n    rt2 = BasicRuntime()\n    wf = SimpleWorkflow(runtime=rt1)\n    assert wf._runtime_locked is False\n\n    handler = wf.run()\n    assert wf._runtime_locked is True\n\n    with pytest.raises(RuntimeError, match=\"Cannot reassign runtime\"):\n        wf._switch_runtime(rt2)\n\n    await handler\n\n\ndef test_runtime_setter_before_launch_then_launch_locks() -> None:\n    rt1 = BasicRuntime()\n    rt2 = BasicRuntime()\n    wf = SimpleWorkflow(runtime=rt1)\n    wf._switch_runtime(rt2)\n    assert wf._runtime_locked is False\n    rt2.launch_sync()\n    assert wf._runtime_locked is True\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_annotation_resolution.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport json\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Annotated, cast\n\nimport pytest\nfrom pydantic import BaseModel\nfrom workflows.decorators import step\nfrom workflows.errors import WorkflowValidationError\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.representation import get_workflow_representation\nfrom workflows.resource import Resource, ResourceConfig\nfrom workflows.workflow import Workflow\n\nif TYPE_CHECKING:\n\n    class MissingReturn:  # pragma: no cover\n        pass\n\n\ndef test_step_decorator_resolves_local_resource_factory_with_future_annotations() -> (\n    None\n):\n    class Repo:\n        pass\n\n    def get_repo() -> Repo:\n        return Repo()\n\n    class LocalWorkflow(Workflow):\n        @step\n        async def start(\n            self,\n            ev: StartEvent,\n            repo: Annotated[Repo, Resource(get_repo)],\n        ) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n    resources = LocalWorkflow.start._step_config.resources\n    assert len(resources) == 1\n    assert resources[0].name == \"repo\"\n    assert resources[0].type_annotation is Repo\n\n\ndef test_step_decorator_resolves_local_return_type_with_future_annotations() -> None:\n    class ResultEvent(StopEvent):\n        pass\n\n    class LocalWorkflow(Workflow):\n        @step\n        async def start(self, ev: StartEvent) -> ResultEvent:\n            return ResultEvent()\n\n    return_types = LocalWorkflow.start._step_config.return_types\n    assert return_types == [ResultEvent]\n\n\ndef test_step_decorator_error_message_for_unresolved_string_annotations() -> None:\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Failed to resolve type annotations\",\n    ):\n\n        class BadWorkflow(Workflow):\n            @step\n            async def start(self, ev: StartEvent) -> \"MissingReturn\":\n                return cast(\"MissingReturn\", StopEvent(result=\"ok\"))\n\n\n@pytest.mark.asyncio\nasync def test_resource_config_in_factory_with_future_annotations(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"ResourceConfig in resource factories should resolve under future annotations.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    class SimpleConfig(BaseModel):\n        name: str\n\n    class SimpleClient:\n        def __init__(self, config: SimpleConfig) -> None:\n            self.config = config\n\n    config_path = tmp_path / \"config.json\"\n    config_path.write_text(json.dumps({\"name\": \"demo\"}))\n\n    def get_client(\n        config: Annotated[SimpleConfig, ResourceConfig(config_file=str(config_path))],\n    ) -> SimpleClient:\n        return SimpleClient(config=config)\n\n    class WorkflowWithConfig(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            client: Annotated[SimpleClient, Resource(get_client)],\n        ) -> StopEvent:\n            assert client.config.name == \"demo\"\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithConfig(disable_validation=True)\n    await wf.run()\n\n\n@pytest.mark.asyncio\nasync def test_nested_resource_in_resource_with_future_annotations() -> None:\n    \"\"\"Nested Resource dependencies should resolve under future annotations.\"\"\"\n\n    class DBConnection:\n        def __init__(self) -> None:\n            self.connected = True\n\n    class Repository:\n        def __init__(self, db: DBConnection) -> None:\n            self.db = db\n\n    def get_db() -> DBConnection:\n        return DBConnection()\n\n    def get_repo(\n        db: Annotated[DBConnection, Resource(get_db)],\n    ) -> Repository:\n        return Repository(db=db)\n\n    class WorkflowWithNestedResources(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            repo: Annotated[Repository, Resource(get_repo)],\n        ) -> StopEvent:\n            assert repo.db.connected\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithNestedResources(disable_validation=True)\n    result = await wf.run()\n    assert result == \"done\"\n\n\ndef test_resource_config_representation_with_future_annotations(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Representation should include ResourceConfig under future annotations.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    class SimpleConfig(BaseModel):\n        name: str\n\n    class SimpleClient:\n        def __init__(self, config: SimpleConfig) -> None:\n            self.config = config\n\n    config_path = tmp_path / \"config.json\"\n    config_path.write_text(json.dumps({\"name\": \"demo\"}))\n\n    def get_client(\n        config: Annotated[SimpleConfig, ResourceConfig(config_file=str(config_path))],\n    ) -> SimpleClient:\n        return SimpleClient(config=config)\n\n    class WorkflowWithConfig(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            client: Annotated[SimpleClient, Resource(get_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    graph = get_workflow_representation(WorkflowWithConfig())\n    resource_config_nodes = [\n        node for node in graph.nodes if node.node_type == \"resource_config\"\n    ]\n    assert len(resource_config_nodes) == 1\n\n\n@pytest.mark.asyncio\nasync def test_localns_does_not_shadow_factory_module_types(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Factory annotations should resolve from factory's module, not step's scope.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    # Import the factory helper from test_resources which has its own _FactoryConfig class\n    from tests.test_resources import _get_factory_with_config_path\n\n    # Define a LOCAL class with the same name as test_resources._FactoryConfig\n    # This should NOT shadow the factory's type when resolving annotations\n    class _FactoryConfig:\n        \"\"\"Local shadow - factory should NOT use this.\"\"\"\n\n        wrong_type = True\n\n    config_path = tmp_path / \"config.json\"\n    config_path.write_text('{\"name\": \"test-value\"}')\n\n    # Get a resource that uses the module-scoped _FactoryConfig from test_resources\n    # The factory is defined in test_resources module, so its annotations should\n    # resolve using test_resources' namespace, NOT this local namespace\n    resource = _get_factory_with_config_path(str(config_path))\n\n    class WorkflowTestingShadow(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            result: Annotated[dict, resource],\n        ) -> StopEvent:\n            return StopEvent(result=result)\n\n    wf = WorkflowTestingShadow(disable_validation=True)\n    result = await wf.run()\n    # If the factory used the local _FactoryConfig (wrong), this would fail\n    assert result == {\"name\": \"test-value\"}\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_catch_error.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime\nfrom typing import Any\n\nimport pytest\nfrom workflows import (\n    Context,\n    Workflow,\n    catch_error,\n    step,\n)\nfrom workflows.context.serializers import JsonSerializer\nfrom workflows.errors import (\n    ContextStateError,\n    WorkflowRuntimeError,\n    WorkflowTimeoutError,\n    WorkflowValidationError,\n)\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StepFailedEvent,\n    StopEvent,\n    WorkflowFailedEvent,\n)\nfrom workflows.retry_policy import (\n    RetryInfo,\n    retry_always,\n    retry_policy,\n    stop_after_attempt,\n    wait_fixed,\n)\nfrom workflows.runtime.types.internal_state import BrokerState, EventAttempt\n\n\ndef _retry(attempts: int) -> Any:\n    return retry_policy(\n        retry=retry_always(),\n        wait=wait_fixed(0),\n        stop=stop_after_attempt(attempts),\n    )\n\n\nclass _InputStart(StartEvent):\n    query: str = \"hello\"\n\n\n# ---------------------------------------------------------------------------\n# retry_info()\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_retry_info_defaults_on_first_attempt() -> None:\n    captured: dict[str, RetryInfo] = {}\n\n    class Flow(Workflow):\n        @step\n        async def first(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            captured[\"info\"] = ctx.retry_info()\n            return StopEvent(result=\"ok\")\n\n    await Flow(timeout=5).run()\n    info = captured[\"info\"]\n    assert info.retry_number == 0\n    assert info.elapsed_seconds == 0.0\n    assert info.last_exception is None\n    assert info.last_failed_at is None\n\n\n@pytest.mark.asyncio\nasync def test_retry_info_after_failure_populated() -> None:\n    observed: list[RetryInfo] = []\n\n    class Flow(Workflow):\n        @step(retry_policy=_retry(3))\n        async def flaky(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            info = ctx.retry_info()\n            observed.append(info)\n            if info.retry_number < 1:\n                raise ValueError(\"boom\")\n            return StopEvent(result=\"ok\")\n\n    result = await Flow(timeout=5).run()\n    assert result == \"ok\"\n    assert observed[0].retry_number == 0\n    assert observed[0].last_exception is None\n    assert observed[0].last_failed_at is None\n    assert observed[1].retry_number == 1\n    assert observed[1].elapsed_seconds >= 0.0\n    assert isinstance(observed[1].last_exception, ValueError)\n    assert str(observed[1].last_exception) == \"boom\"\n    assert isinstance(observed[1].last_failed_at, datetime)\n    assert observed[1].last_failed_at.tzinfo is not None\n\n\ndef test_retry_info_outside_step_raises() -> None:\n    class Flow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n    ctx: Context = Context(Flow())\n    with pytest.raises((WorkflowRuntimeError, ContextStateError)):\n        ctx.retry_info()\n\n\ndef test_last_exception_serialization_roundtrip() -> None:\n    class Flow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n    wf = Flow()\n    state = BrokerState.from_workflow(wf)\n    state.workers[\"a\"].queue.append(\n        EventAttempt(\n            event=StartEvent(),\n            attempts=1,\n            first_attempt_at=100.0,\n            last_exception=ValueError(\"boom\"),\n            last_failed_at=123.456,\n        )\n    )\n    serialized = state.to_serialized(JsonSerializer())\n    restored = BrokerState.from_serialized(serialized, wf, JsonSerializer())\n    restored_attempt = restored.workers[\"a\"].queue[0]\n    assert isinstance(restored_attempt.last_exception, ValueError)\n    assert str(restored_attempt.last_exception) == \"boom\"\n    assert restored_attempt.last_failed_at == 123.456\n\n\n# ---------------------------------------------------------------------------\n# Handler-decorator validation\n# ---------------------------------------------------------------------------\n\n\ndef test_catch_error_wrong_event_type_invalid() -> None:\n    with pytest.raises(WorkflowValidationError, match=\"must accept StepFailedEvent\"):\n\n        @catch_error\n        async def bad_handler(self: Any, ctx: Context, ev: StartEvent) -> StopEvent:\n            return StopEvent()\n\n\n# ---------------------------------------------------------------------------\n# Runtime routing\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_catch_error_returning_stop_completes_workflow() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def flaky(self, ev: StartEvent) -> StopEvent:\n            raise RuntimeError(\"transient\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result={\"recovered_from\": ev.step_name})\n\n    handler = Flow(timeout=5).run()\n    events: list[Event] = []\n    async for ev in handler.stream_events():\n        events.append(ev)\n    result = await handler\n    assert result == {\"recovered_from\": \"flaky\"}\n    assert not any(isinstance(ev, WorkflowFailedEvent) for ev in events)\n\n\n@pytest.mark.asyncio\nasync def test_catch_error_raising_fails_workflow() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def flaky(self, ev: StartEvent) -> StopEvent:\n            raise RuntimeError(\"primary\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            raise ValueError(\"handler-failed\")\n\n    handler_run = Flow(timeout=5).run()\n    events: list[Event] = []\n    async for ev in handler_run.stream_events():\n        events.append(ev)\n    with pytest.raises(ValueError, match=\"handler-failed\"):\n        await handler_run\n    failed_events = [ev for ev in events if isinstance(ev, WorkflowFailedEvent)]\n    assert len(failed_events) == 1\n    assert failed_events[0].step_name == \"handler\"\n    assert str(failed_events[0].exception) == \"handler-failed\"\n\n\n@pytest.mark.asyncio\nasync def test_catch_error_not_invoked_on_recoverable_retry() -> None:\n    handler_invoked: list[bool] = []\n\n    class Flow(Workflow):\n        attempts = 0\n\n        @step(retry_policy=_retry(3))\n        async def flaky(self, ev: StartEvent) -> StopEvent:\n            self.attempts += 1\n            if self.attempts < 2:\n                raise ValueError(\"transient\")\n            return StopEvent(result=\"recovered\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            handler_invoked.append(True)\n            return StopEvent(result=\"caught\")\n\n    result = await Flow(timeout=5).run()\n    assert result == \"recovered\"\n    assert handler_invoked == []\n\n\n@pytest.mark.asyncio\nasync def test_step_failed_event_fields() -> None:\n    captured: dict[str, StepFailedEvent] = {}\n\n    class Flow(Workflow):\n        @step(retry_policy=_retry(2))\n        async def flaky(self, ev: _InputStart) -> StopEvent:\n            raise RuntimeError(f\"bad:{ev.query}\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            captured[\"ev\"] = ev\n            return StopEvent(result=\"caught\")\n\n    await Flow(timeout=5).run(start_event=_InputStart(query=\"hello\"))\n    ev = captured[\"ev\"]\n    assert ev.step_name == \"flaky\"\n    assert isinstance(ev.input_event, _InputStart)\n    assert ev.input_event.query == \"hello\"\n    assert isinstance(ev.exception, RuntimeError)\n    assert str(ev.exception) == \"bad:hello\"\n    assert ev.attempts == 2\n    assert ev.elapsed_seconds >= 0.0\n    assert isinstance(ev.failed_at, datetime)\n    assert ev.failed_at.tzinfo is not None\n\n\n@pytest.mark.asyncio\nasync def test_catch_error_not_invoked_on_timeout() -> None:\n    handler_invoked: list[bool] = []\n\n    class Flow(Workflow):\n        @step\n        async def slow(self, ev: StartEvent) -> StopEvent:\n            await asyncio.sleep(5)\n            return StopEvent(result=\"unreachable\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            handler_invoked.append(True)\n            return StopEvent(result=\"caught\")\n\n    with pytest.raises(WorkflowTimeoutError):\n        await Flow(timeout=0.1).run()\n    assert handler_invoked == []\n\n\n@pytest.mark.asyncio\nasync def test_baseline_without_catch_error_still_fails() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def flaky(self, ev: StartEvent) -> StopEvent:\n            raise ValueError(\"boom\")\n\n    handler = Flow(timeout=5).run()\n    events: list[Event] = []\n    async for ev in handler.stream_events():\n        events.append(ev)\n    with pytest.raises(ValueError, match=\"boom\"):\n        await handler\n    assert any(isinstance(ev, WorkflowFailedEvent) for ev in events)\n\n\n@pytest.mark.asyncio\nasync def test_catch_error_can_read_context_state() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def flaky(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            await ctx.store.set(\"progress\", \"halfway\")\n            raise RuntimeError(\"boom\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            progress = await ctx.store.get(\"progress\", default=\"unset\")\n            return StopEvent(result={\"progress\": progress, \"step\": ev.step_name})\n\n    result = await Flow(timeout=5).run()\n    assert result == {\"progress\": \"halfway\", \"step\": \"flaky\"}\n\n\n# ---------------------------------------------------------------------------\n# Scoping: for_steps + wildcard interaction\n# ---------------------------------------------------------------------------\n\n\nclass _BEvent(Event):\n    pass\n\n\n@pytest.mark.asyncio\nasync def test_scoped_handler_catches_listed_step() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def a(self, ev: StartEvent) -> _BEvent:\n            raise RuntimeError(\"a failed\")\n\n        @step\n        async def b(self, ev: _BEvent) -> StopEvent:\n            return StopEvent(result=\"b-ran\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def handle_a(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result={\"caught\": ev.step_name})\n\n    result = await Flow(timeout=5).run()\n    assert result == {\"caught\": \"a\"}\n\n\n@pytest.mark.asyncio\nasync def test_scoped_handler_does_not_catch_other_step() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def a(self, ev: StartEvent) -> _BEvent:\n            return _BEvent()\n\n        @step(retry_policy=_retry(1))\n        async def b(self, ev: _BEvent) -> StopEvent:\n            raise ValueError(\"b failed\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def handle_a(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"caught\")\n\n    handler_run = Flow(timeout=5).run()\n    with pytest.raises(ValueError, match=\"b failed\"):\n        await handler_run\n\n\nclass _AFailedMarker(Event):\n    pass\n\n\nclass _BFailedMarker(Event):\n    pass\n\n\n@pytest.mark.asyncio\nasync def test_multiple_scoped_handlers_each_own_step() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def a(self, ev: StartEvent) -> _BEvent:\n            raise RuntimeError(\"a\")\n\n        @step(retry_policy=_retry(1))\n        async def b(self, ev: _BEvent) -> StopEvent:\n            raise RuntimeError(\"b\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def handle_a(self, ctx: Context, ev: StepFailedEvent) -> _AFailedMarker:\n            return _AFailedMarker()\n\n        @catch_error(for_steps=[\"b\"])\n        async def handle_b(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result={\"recovered\": ev.step_name})\n\n        @step\n        async def finish_a(self, ev: _AFailedMarker) -> _BEvent:\n            return _BEvent()\n\n    result = await Flow(timeout=5).run()\n    assert result == {\"recovered\": \"b\"}\n\n\n@pytest.mark.asyncio\nasync def test_scoped_and_wildcard_mix() -> None:\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def a(self, ev: StartEvent) -> _BEvent:\n            raise RuntimeError(\"a-fail\")\n\n        @step(retry_policy=_retry(1))\n        async def b(self, ev: _BEvent) -> StopEvent:\n            raise RuntimeError(\"b-fail\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def handle_a(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result={\"via\": \"scoped\", \"step\": ev.step_name})\n\n        @catch_error\n        async def wildcard(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result={\"via\": \"wildcard\", \"step\": ev.step_name})\n\n    result = await Flow(timeout=5).run()\n    assert result == {\"via\": \"scoped\", \"step\": \"a\"}\n\n\n# ---------------------------------------------------------------------------\n# max_recoveries budget\n# ---------------------------------------------------------------------------\n\n\nclass _RetryEvent(Event):\n    pass\n\n\n@pytest.mark.asyncio\nasync def test_max_recoveries_default_fails_on_second_entry() -> None:\n    calls: list[str] = []\n\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def a(self, ev: StartEvent) -> StopEvent:\n            calls.append(\"a\")\n            raise RuntimeError(\"a-fail\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def handle_a(self, ctx: Context, ev: StepFailedEvent) -> _RetryEvent:\n            calls.append(\"handle_a\")\n            return _RetryEvent()\n\n        @step(retry_policy=_retry(1))\n        async def retry_a(self, ev: _RetryEvent) -> StopEvent:\n            calls.append(\"retry_a\")\n            raise RuntimeError(\"retry_a-fail\")\n\n    with pytest.raises(RuntimeError):\n        await Flow(timeout=5).run()\n    # a runs once, handle_a runs once (count=1, <= 1 allowed), retry_a runs\n    # once and fails. On its failure, handle_a would be entered a 2nd time\n    # (count would be 2, > 1) → workflow fails instead.\n    assert calls.count(\"handle_a\") == 1\n\n\n@pytest.mark.asyncio\nasync def test_max_recoveries_two_allows_second_entry() -> None:\n    calls: list[str] = []\n\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def a(self, ev: StartEvent) -> StopEvent:\n            calls.append(\"a\")\n            raise RuntimeError(\"a-fail\")\n\n        @catch_error(for_steps=[\"a\", \"retry_a\"], max_recoveries=2)\n        async def handle_a(self, ctx: Context, ev: StepFailedEvent) -> _RetryEvent:\n            calls.append(\"handle_a\")\n            if len([c for c in calls if c == \"handle_a\"]) >= 2:\n                # stop re-trying on the 2nd invocation\n                raise RuntimeError(\"giving up\")\n            return _RetryEvent()\n\n        @step(retry_policy=_retry(1))\n        async def retry_a(self, ev: _RetryEvent) -> StopEvent:\n            calls.append(\"retry_a\")\n            raise RuntimeError(\"retry_a-fail\")\n\n    with pytest.raises(RuntimeError):\n        await Flow(timeout=5).run()\n    assert calls.count(\"handle_a\") == 2\n\n\n# ---------------------------------------------------------------------------\n# Handler's own failure falls through\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_handler_own_failure_falls_through() -> None:\n    events: list[Event] = []\n\n    class Flow(Workflow):\n        @step(retry_policy=_retry(1))\n        async def a(self, ev: StartEvent) -> StopEvent:\n            raise RuntimeError(\"a-fail\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            raise ValueError(\"handler-fail\")\n\n    handler_run = Flow(timeout=5).run()\n    async for ev in handler_run.stream_events():\n        events.append(ev)\n    with pytest.raises(ValueError, match=\"handler-fail\"):\n        await handler_run\n    failed = [ev for ev in events if isinstance(ev, WorkflowFailedEvent)]\n    assert len(failed) == 1\n    assert failed[0].step_name == \"handler\"\n\n\n# ---------------------------------------------------------------------------\n# recovery_counts serialization round-trip\n# ---------------------------------------------------------------------------\n\n\ndef test_recovery_counts_serialization_roundtrip() -> None:\n    class Flow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"caught\")\n\n    wf = Flow()\n    wf._validate()\n    state = BrokerState.from_workflow(wf)\n    state.workers[\"a\"].queue.append(\n        EventAttempt(\n            event=StartEvent(),\n            attempts=1,\n            first_attempt_at=100.0,\n            recovery_counts={\"handler\": 1},\n        )\n    )\n    serialized = state.to_serialized(JsonSerializer())\n    restored = BrokerState.from_serialized(serialized, wf, JsonSerializer())\n    restored_attempt = restored.workers[\"a\"].queue[0]\n    assert restored_attempt.recovery_counts == {\"handler\": 1}\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_child_state_inheritance.py",
    "content": "# ty: ignore[invalid-argument-type]\n\"\"\"\nTests for workflow state inheritance behavior.\n\nThis module tests the behavior when:\n1. A base workflow class uses Context[BaseState]\n2. A child workflow class uses Context[ChildState] (where ChildState extends BaseState)\n\nKey behavior:\n- Subtype relationships are allowed (BaseState + ChildState work together)\n- The most derived type (ChildState) is used as the actual state type\n- When a base class step calls set_state with a BaseState, the child fields\n  are preserved through merging (not obliterated)\n\"\"\"\n\nimport pytest\nfrom pydantic import BaseModel, Field\nfrom workflows import Context, Workflow\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.testing import WorkflowTestRunner\n\n# ============================================================================\n# State models for testing inheritance\n# ============================================================================\n\n\nclass BaseState(BaseModel):\n    \"\"\"Base state with a single field.\"\"\"\n\n    base_field: str = Field(default=\"base_default\")\n\n\nclass ChildState(BaseState):\n    \"\"\"Child state that extends BaseState with additional fields.\"\"\"\n\n    child_field: str = Field(default=\"child_default\")\n    extra_counter: int = Field(default=0)\n\n\nclass UnrelatedState(BaseModel):\n    \"\"\"State that is NOT in the BaseState/ChildState hierarchy.\"\"\"\n\n    unrelated_field: str = Field(default=\"unrelated\")\n\n\n# ============================================================================\n# Events for multi-step workflows\n# ============================================================================\n\n\nclass MiddleEvent(Event):\n    \"\"\"Event to pass control between steps.\"\"\"\n\n    pass\n\n\n# ============================================================================\n# Test: Subtype state inheritance works correctly\n# ============================================================================\n\n\nclass BaseWorkflowWithBaseState(Workflow):\n    \"\"\"Base workflow using Context[BaseState].\"\"\"\n\n    @step\n    async def base_step(self, ctx: Context[BaseState], ev: StartEvent) -> MiddleEvent:\n        # Base step works with BaseState type, sets base field\n        await ctx.store.set(\"base_field\", \"set_by_base_step\")\n        return MiddleEvent()\n\n\nclass ChildWorkflowWithChildState(BaseWorkflowWithBaseState):\n    \"\"\"Child workflow that uses Context[ChildState] - now compatible with base.\"\"\"\n\n    @step\n    async def child_step(self, ctx: Context[ChildState], ev: MiddleEvent) -> StopEvent:\n        # Child step can access both base and child fields\n        await ctx.store.set(\"child_field\", \"set_by_child_step\")\n        # Return state as result for testing\n        state = await ctx.store.get_state()\n        return StopEvent(result=state)\n\n\n@pytest.mark.asyncio\nasync def test_subtype_inheritance_works() -> None:\n    \"\"\"\n    Test that subtype state inheritance works correctly.\n\n    When a base workflow step uses Context[BaseState] and a child workflow step\n    uses Context[ChildState], the system should:\n    1. Use ChildState (most derived) as the actual state type\n    2. Allow both steps to work with the state\n    3. Preserve all fields from both base and child\n    \"\"\"\n    workflow = ChildWorkflowWithChildState()\n    test_runner = WorkflowTestRunner(workflow)\n\n    result = await test_runner.run()\n\n    state = result.result\n\n    # Verify state is ChildState\n    assert isinstance(state, ChildState)\n    # Both base and child fields should be properly set\n    assert state.base_field == \"set_by_base_step\"\n    assert state.child_field == \"set_by_child_step\"\n\n\n# ============================================================================\n# Test: set_state with parent type merges fields (doesn't obliterate)\n# ============================================================================\n\n\nclass WorkflowWithBaseStateSetState(Workflow):\n    \"\"\"Workflow where base step calls set_state with BaseState.\"\"\"\n\n    @step\n    async def init_step(self, ctx: Context[ChildState], ev: StartEvent) -> MiddleEvent:\n        # Initialize all fields including child-specific ones\n        await ctx.store.set(\"base_field\", \"initial_base\")\n        await ctx.store.set(\"child_field\", \"initial_child\")\n        await ctx.store.set(\"extra_counter\", 100)\n        return MiddleEvent()\n\n    @step\n    async def base_step(self, ctx: Context[BaseState], ev: MiddleEvent) -> StopEvent:\n        # This step only knows about BaseState, creates a new BaseState\n        # and sets it. This should merge, not obliterate child fields.\n        new_state = BaseState(base_field=\"modified_by_base_step\")\n        await ctx.store.set_state(new_state)  # type: ignore[arg-type]\n        # Return state as result for testing\n        state = await ctx.store.get_state()\n        return StopEvent(result=state)\n\n\n@pytest.mark.asyncio\nasync def test_set_state_with_parent_type_merges_fields() -> None:\n    \"\"\"\n    Test that set_state with a parent type merges fields, not obliterates.\n\n    When a base class step creates a new BaseState and calls set_state,\n    the child fields (child_field, extra_counter) should be preserved\n    while the base field is updated.\n    \"\"\"\n    workflow = WorkflowWithBaseStateSetState()\n    test_runner = WorkflowTestRunner(workflow)\n\n    result = await test_runner.run()\n\n    state = result.result\n\n    # The base field was modified\n    assert state.base_field == \"modified_by_base_step\"\n    # Child fields should be PRESERVED (not reset to defaults)\n    assert state.child_field == \"initial_child\"\n    assert state.extra_counter == 100\n\n\n# ============================================================================\n# Test: Incompatible state types still raise error\n# ============================================================================\n\n\nclass WorkflowWithUnrelatedState(Workflow):\n    \"\"\"Workflow with an unrelated state type.\"\"\"\n\n    @step\n    async def step_one(self, ctx: Context[BaseState], ev: StartEvent) -> MiddleEvent:\n        return MiddleEvent()\n\n    @step\n    async def step_two(\n        self, ctx: Context[UnrelatedState], ev: MiddleEvent\n    ) -> StopEvent:\n        return StopEvent()\n\n\n@pytest.mark.asyncio\nasync def test_incompatible_state_types_raises_error() -> None:\n    \"\"\"\n    Test that incompatible state types (not in same hierarchy) raise ValueError.\n\n    When state types are not in a parent-child relationship, they are\n    incompatible and should raise an error.\n    \"\"\"\n    workflow = WorkflowWithUnrelatedState()\n    test_runner = WorkflowTestRunner(workflow)\n\n    with pytest.raises(ValueError) as exc_info:\n        await test_runner.run()\n\n    # Verify the error message mentions incompatible hierarchy\n    assert \"not in a compatible inheritance hierarchy\" in str(exc_info.value)\n\n\n# ============================================================================\n# Test: Sibling state types (both inherit from same base) raise error\n# ============================================================================\n\n\nclass SiblingStateOne(BaseState):\n    \"\"\"First sibling state extending BaseState.\"\"\"\n\n    sibling_one_field: int = Field(default=1)\n\n\nclass SiblingStateTwo(BaseState):\n    \"\"\"Second sibling state extending BaseState - incompatible with SiblingStateOne.\"\"\"\n\n    sibling_two_field: str = Field(default=\"two\")\n\n\nclass WorkflowWithSiblingStates(Workflow):\n    \"\"\"Workflow with two sibling state types that share a common base.\"\"\"\n\n    @step\n    async def step_one(\n        self, ctx: Context[SiblingStateOne], ev: StartEvent\n    ) -> MiddleEvent:\n        return MiddleEvent()\n\n    @step\n    async def step_two(\n        self, ctx: Context[SiblingStateTwo], ev: MiddleEvent\n    ) -> StopEvent:\n        return StopEvent()\n\n\n@pytest.mark.asyncio\nasync def test_sibling_state_types_raises_error() -> None:\n    \"\"\"\n    Test that sibling state types (both inherit from same base) raise ValueError.\n\n    When two state types both inherit from the same base but neither is a\n    subclass of the other (they're siblings), they are incompatible.\n\n    Example:\n        BaseState\n           ├── SiblingStateOne (has sibling_one_field: int)\n           └── SiblingStateTwo (has sibling_two_field: str)\n\n    Neither sibling is a subclass of the other, so they can't be used together.\n    \"\"\"\n    workflow = WorkflowWithSiblingStates()\n    test_runner = WorkflowTestRunner(workflow)\n\n    with pytest.raises(ValueError) as exc_info:\n        await test_runner.run()\n\n    # Verify the error message mentions incompatible hierarchy\n    assert \"not in a compatible inheritance hierarchy\" in str(exc_info.value)\n    assert \"SiblingStateOne\" in str(exc_info.value)\n    assert \"SiblingStateTwo\" in str(exc_info.value)\n\n\n# ============================================================================\n# Test: Using child state type everywhere still works\n# ============================================================================\n\n\nclass BaseWorkflowConsistent(Workflow):\n    \"\"\"Base workflow using Context[ChildState] (the more specific type).\"\"\"\n\n    @step\n    async def start_step(self, ctx: Context[ChildState], ev: StartEvent) -> MiddleEvent:\n        # Base class step modifies state\n        await ctx.store.set(\"base_field\", \"modified_by_base_step\")\n        return MiddleEvent()\n\n\nclass ChildWorkflowConsistent(BaseWorkflowConsistent):\n    \"\"\"Child workflow that also uses Context[ChildState] - COMPATIBLE.\"\"\"\n\n    @step\n    async def end_step(self, ctx: Context[ChildState], ev: MiddleEvent) -> StopEvent:\n        # Child step can access and modify the same state\n        state = await ctx.store.get_state()\n        await ctx.store.set(\"child_field\", \"modified_by_child_step\")\n        await ctx.store.set(\"extra_counter\", state.extra_counter + 1)\n        # Return state as result for testing\n        final_state = await ctx.store.get_state()\n        return StopEvent(result=final_state)\n\n\n@pytest.mark.asyncio\nasync def test_consistent_child_state_works() -> None:\n    \"\"\"\n    Test that using the same child state type in both base and child works.\n\n    When all steps (inherited and new) use the same state type, the workflow\n    should execute without errors and both base and child fields should be\n    accessible.\n    \"\"\"\n    workflow = ChildWorkflowConsistent()\n    test_runner = WorkflowTestRunner(workflow)\n\n    result = await test_runner.run()\n\n    state = result.result\n\n    # Both base and child fields should be properly set\n    assert state.base_field == \"modified_by_base_step\"\n    assert state.child_field == \"modified_by_child_step\"\n    assert state.extra_counter == 1\n\n\n# ============================================================================\n# Test: set_state with same type works (direct replacement)\n# ============================================================================\n\n\nclass SetStateWorkflow(Workflow):\n    \"\"\"Workflow that tests set_state with same type.\"\"\"\n\n    @step\n    async def init_step(self, ctx: Context[ChildState], ev: StartEvent) -> MiddleEvent:\n        # Initialize all fields including child-specific ones\n        state = await ctx.store.get_state()\n        state.base_field = \"initial_base\"\n        state.child_field = \"initial_child\"\n        state.extra_counter = 100\n        await ctx.store.set_state(state)\n        return MiddleEvent()\n\n    @step\n    async def modify_step(self, ctx: Context[ChildState], ev: MiddleEvent) -> StopEvent:\n        # Get state, modify, and set back (same type)\n        state = await ctx.store.get_state()\n        state.base_field = \"modified_base\"\n        await ctx.store.set_state(state)\n        # Return state as result for testing\n        final_state = await ctx.store.get_state()\n        return StopEvent(result=final_state)\n\n\n@pytest.mark.asyncio\nasync def test_set_state_same_type_preserves_fields() -> None:\n    \"\"\"\n    Test that calling set_state with same type preserves all fields.\n\n    When a step gets the state, modifies only base fields, and calls set_state,\n    the child fields should be preserved because it's the same ChildState object.\n    \"\"\"\n    workflow = SetStateWorkflow()\n    test_runner = WorkflowTestRunner(workflow)\n\n    result = await test_runner.run()\n\n    state = result.result\n\n    # The base field was modified\n    assert state.base_field == \"modified_base\"\n    # But child fields should be PRESERVED\n    assert state.child_field == \"initial_child\"\n    assert state.extra_counter == 100\n\n\n# ============================================================================\n# Test: set_state with unrelated type raises error\n# ============================================================================\n\n\n@pytest.mark.asyncio\nasync def test_set_state_unrelated_type_raises_error() -> None:\n    \"\"\"\n    Test that set_state raises ValueError when setting unrelated state type.\n\n    If someone tries to set an UnrelatedState when ChildState is expected,\n    it should fail because they are not in the same inheritance hierarchy.\n    \"\"\"\n    from workflows.context.state_store import InMemoryStateStore\n\n    # Create a store with ChildState\n    store = InMemoryStateStore(ChildState())\n\n    # Try to set an UnrelatedState - should fail\n    with pytest.raises(ValueError) as exc_info:\n        await store.set_state(UnrelatedState(unrelated_field=\"test\"))  # type: ignore[arg-type]\n\n    assert \"must be of type\" in str(exc_info.value)\n\n\n# ============================================================================\n# Test: set_state with parent type at store level\n# ============================================================================\n\n\n@pytest.mark.asyncio\nasync def test_set_state_parent_type_merges_at_store_level() -> None:\n    \"\"\"\n    Test that set_state with parent type merges fields at store level.\n\n    Directly test the InMemoryStateStore behavior when setting a parent\n    type onto a child state.\n    \"\"\"\n    from workflows.context.state_store import InMemoryStateStore\n\n    # Create a store with ChildState and set some initial values\n    initial_state = ChildState(\n        base_field=\"initial_base\", child_field=\"initial_child\", extra_counter=42\n    )\n    store = InMemoryStateStore(initial_state)\n\n    # Set a BaseState (parent type) - should merge, not replace\n    new_base_state = BaseState(base_field=\"updated_base\")\n    await store.set_state(new_base_state)  # type: ignore[arg-type]\n\n    # Verify merging behavior\n    result = await store.get_state()\n    assert isinstance(result, ChildState)\n    assert result.base_field == \"updated_base\"  # Updated from parent\n    assert result.child_field == \"initial_child\"  # Preserved\n    assert result.extra_counter == 42  # Preserved\n\n\n# ============================================================================\n# Test: Using DictState as a flexible alternative\n# ============================================================================\n\n\nclass BaseWorkflowDictState(Workflow):\n    \"\"\"Base workflow using untyped Context (DictState).\"\"\"\n\n    @step\n    async def start_step(self, ctx: Context, ev: StartEvent) -> MiddleEvent:\n        await ctx.store.set(\"base_field\", \"set_by_base\")\n        return MiddleEvent()\n\n\nclass ChildWorkflowDictState(BaseWorkflowDictState):\n    \"\"\"Child workflow that also uses untyped Context.\"\"\"\n\n    @step\n    async def end_step(self, ctx: Context, ev: MiddleEvent) -> StopEvent:\n        await ctx.store.set(\"child_field\", \"set_by_child\")\n        # Return field values as result for testing\n        base_field = await ctx.store.get(\"base_field\")\n        child_field = await ctx.store.get(\"child_field\")\n        return StopEvent(result={\"base_field\": base_field, \"child_field\": child_field})\n\n\n@pytest.mark.asyncio\nasync def test_dict_state_allows_flexible_inheritance() -> None:\n    \"\"\"\n    Test that using DictState (untyped Context) allows flexible inheritance.\n\n    When workflows don't specify a state type, DictState is used which allows\n    any fields to be set dynamically. This is a valid pattern for inheritance\n    when type safety is not required.\n    \"\"\"\n    workflow = ChildWorkflowDictState()\n    test_runner = WorkflowTestRunner(workflow)\n\n    result = await test_runner.run()\n\n    fields = result.result\n\n    assert fields[\"base_field\"] == \"set_by_base\"\n    assert fields[\"child_field\"] == \"set_by_child\"\n\n\n# ============================================================================\n# Test: edit_state context manager also preserves fields\n# ============================================================================\n\n\nclass EditStateWorkflow(Workflow):\n    \"\"\"Workflow testing edit_state preserves child fields.\"\"\"\n\n    @step\n    async def init_step(self, ctx: Context[ChildState], ev: StartEvent) -> MiddleEvent:\n        async with ctx.store.edit_state() as state:\n            state.base_field = \"initial_base\"\n            state.child_field = \"initial_child\"\n            state.extra_counter = 50\n        return MiddleEvent()\n\n    @step\n    async def modify_step(self, ctx: Context[ChildState], ev: MiddleEvent) -> StopEvent:\n        # Use edit_state to modify only base field\n        async with ctx.store.edit_state() as state:\n            state.base_field = \"edited_base\"\n            # Not touching child fields\n        # Return state as result for testing\n        final_state = await ctx.store.get_state()\n        return StopEvent(result=final_state)\n\n\n@pytest.mark.asyncio\nasync def test_edit_state_preserves_child_fields() -> None:\n    \"\"\"\n    Test that edit_state context manager preserves unmodified child fields.\n\n    When using the edit_state context manager, only the fields that are\n    explicitly modified should change; other fields remain intact.\n    \"\"\"\n    workflow = EditStateWorkflow()\n    test_runner = WorkflowTestRunner(workflow)\n\n    result = await test_runner.run()\n\n    state = result.result\n\n    # Base field was modified\n    assert state.base_field == \"edited_base\"\n    # Child fields should be preserved\n    assert state.child_field == \"initial_child\"\n    assert state.extra_counter == 50\n\n\n# ============================================================================\n# Test: Three-level inheritance hierarchy\n# ============================================================================\n\n\nclass GrandchildState(ChildState):\n    \"\"\"Grandchild state with an additional field.\"\"\"\n\n    grandchild_field: str = Field(default=\"grandchild_default\")\n\n\nclass BaseWorkflowThreeLevel(Workflow):\n    \"\"\"Base workflow using BaseState.\"\"\"\n\n    @step\n    async def level1_step(self, ctx: Context[BaseState], ev: StartEvent) -> MiddleEvent:\n        await ctx.store.set(\"base_field\", \"set_at_level1\")\n        return MiddleEvent()\n\n\nclass ChildWorkflowThreeLevel(BaseWorkflowThreeLevel):\n    \"\"\"Middle-level workflow using ChildState.\"\"\"\n\n    @step\n    async def level2_step(\n        self, ctx: Context[ChildState], ev: MiddleEvent\n    ) -> MiddleEvent:\n        await ctx.store.set(\"child_field\", \"set_at_level2\")\n        return MiddleEvent()\n\n\nclass GrandchildWorkflowThreeLevel(ChildWorkflowThreeLevel):\n    \"\"\"Leaf workflow using GrandchildState.\"\"\"\n\n    @step\n    async def level3_step(\n        self, ctx: Context[GrandchildState], ev: MiddleEvent\n    ) -> StopEvent:\n        await ctx.store.set(\"grandchild_field\", \"set_at_level3\")\n        # Return state as result for testing\n        state = await ctx.store.get_state()\n        return StopEvent(result=state)\n\n\n@pytest.mark.asyncio\nasync def test_three_level_inheritance_works() -> None:\n    \"\"\"\n    Test that three-level state inheritance works correctly.\n\n    When workflows have a three-level inheritance hierarchy\n    (BaseState -> ChildState -> GrandchildState), the most derived type\n    should be used and all fields should be accessible.\n    \"\"\"\n    workflow = GrandchildWorkflowThreeLevel()\n    test_runner = WorkflowTestRunner(workflow)\n\n    result = await test_runner.run()\n\n    state = result.result\n\n    # Verify state is GrandchildState\n    assert isinstance(state, GrandchildState)\n    # All fields from all levels should be properly set\n    assert state.base_field == \"set_at_level1\"\n    assert state.child_field == \"set_at_level2\"\n    assert state.grandchild_field == \"set_at_level3\"\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_decorator.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nimport re\n\nimport pytest\nfrom workflows.decorators import step\nfrom workflows.errors import WorkflowValidationError\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.workflow import Workflow\n\n\ndef test_decorated_config() -> None:\n    class LocalWorkflow(Workflow):\n        @step\n        async def entry(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    def f(self, ev: Event) -> Event:  # noqa: ANN001\n        return Event()\n\n    res = step(workflow=LocalWorkflow)(f)\n    config = res._step_config\n    assert config.accepted_events == [Event]\n    assert config.event_name == \"ev\"\n    assert config.return_types == [Event]\n\n\ndef test_decorate_method() -> None:\n    class TestWorkflow(Workflow):\n        @step\n        def f1(self, ev: StartEvent) -> Event:\n            return ev\n\n        @step\n        def f2(self, ev: Event) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow()\n    assert wf.f1._step_config\n    assert wf.f2._step_config\n\n\ndef test_decorate_wrong_signature() -> None:\n    def f() -> None:\n        pass\n\n    with pytest.raises(WorkflowValidationError):\n        step()(f)\n\n\ndef test_decorate_free_function() -> None:\n    class TestWorkflow(Workflow):\n        pass\n\n    @step(workflow=TestWorkflow)\n    def f(ev: Event) -> Event:\n        return Event()\n\n    assert TestWorkflow._step_functions == {\"f\": f}\n\n\ndef test_decorate_free_function_wrong_decorator() -> None:\n    with pytest.raises(\n        WorkflowValidationError,\n        match=re.escape(\n            \"To decorate f please pass a workflow class to the @step decorator.\"\n        ),\n    ):\n\n        @step\n        def f(ev: Event) -> Event:\n            return Event()\n\n\ndef test_decorate_free_function_wrong_num_workers() -> None:\n    class TestWorkflow(Workflow):\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError, match=\"num_workers must be an integer greater than 0\"\n    ):\n\n        @step(workflow=TestWorkflow, num_workers=0)\n        def f1(ev: Event) -> Event:\n            return Event()\n\n    with pytest.raises(\n        WorkflowValidationError, match=\"num_workers must be an integer greater than 0\"\n    ):\n\n        @step(workflow=TestWorkflow, num_workers=0.5)  # type: ignore\n        def f2(ev: Event) -> Event:\n            return Event()\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_event.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom http.client import HTTPException\nfrom typing import Any, cast\n\nimport pytest\nfrom pydantic import PrivateAttr\nfrom workflows.context import JsonSerializer\nfrom workflows.events import (\n    Event,\n    StopEvent,\n    WorkflowCancelledEvent,\n    WorkflowFailedEvent,\n    WorkflowTimedOutEvent,\n)\n\n\nclass _TestEvent(Event):\n    param: str\n    _private_param_1: str = PrivateAttr()\n    _private_param_2: str = PrivateAttr(default_factory=str)\n\n\nclass _TestEvent2(Event):\n    \"\"\"\n    Custom Test Event.\n\n    Private Attrs:\n        _private_param: doesn't get modified during construction\n        _modified_private_param: gets processed before being set\n    \"\"\"\n\n    _private_param: int = PrivateAttr()\n    _modified_private_param: int = PrivateAttr()\n\n    def __init__(self, _modified_private_param: int, **params: Any):\n        super().__init__(**params)\n        self._modified_private_param = _modified_private_param * 2\n\n\ndef test_event_init_basic() -> None:\n    evt = Event(a=1, b=2, c=\"c\")\n\n    assert evt.a == 1\n    assert evt.b == 2\n    assert evt.c == \"c\"\n    assert evt[\"a\"] == evt.a\n    assert evt[\"b\"] == evt.b\n    assert evt[\"c\"] == evt.c\n    assert evt.keys() == {\"a\": 1, \"b\": 2, \"c\": \"c\"}.keys()\n\n\ndef test_custom_event_with_fields_and_private_params() -> None:\n    evt = _TestEvent(a=1, param=\"test_param\", _private_param_1=\"test_private_param_1\")  # type: ignore\n\n    assert evt.a == 1\n    assert evt[\"a\"] == evt.a\n    assert evt.param == \"test_param\"\n    assert evt._data == {\"a\": 1}\n    assert evt._private_param_1 == \"test_private_param_1\"\n    assert evt._private_param_2 == \"\"\n\n\ndef test_custom_event_override_init() -> None:\n    evt = _TestEvent2(a=1, b=2, _private_param=2, _modified_private_param=2)\n\n    assert evt.a == 1\n    assert evt.b == 2\n    assert evt._data == {\"a\": 1, \"b\": 2}\n    assert evt._private_param == 2\n    assert evt._modified_private_param == 4\n\n\ndef test_event_missing_key() -> None:\n    ev = _TestEvent(param=\"bar\")\n    with pytest.raises(AttributeError):\n        ev.wrong_key\n\n\ndef test_event_not_a_field() -> None:\n    ev = _TestEvent(param=\"foo\", not_a_field=\"bar\")  # type: ignore\n    assert ev._data[\"not_a_field\"] == \"bar\"\n    ev.not_a_field = \"baz\"\n    assert ev._data[\"not_a_field\"] == \"baz\"\n    ev[\"not_a_field\"] = \"barbaz\"\n    assert ev._data[\"not_a_field\"] == \"barbaz\"\n    assert ev.get(\"not_a_field\") == \"barbaz\"\n\n\ndef test_event_dict_api() -> None:\n    ev = _TestEvent(param=\"foo\")\n    assert len(ev) == 0\n    ev[\"a_new_key\"] = \"bar\"\n    assert len(ev) == 1\n    assert list(ev.values()) == [\"bar\"]\n    k, v = next(iter(ev.items()))\n    assert k == \"a_new_key\"\n    assert v == \"bar\"\n    assert next(iter(ev)) == \"a_new_key\"\n    assert ev.to_dict() == {\"a_new_key\": \"bar\"}\n\n\ndef test_event_serialization() -> None:\n    ev = _TestEvent(param=\"foo\", not_a_field=\"bar\")  # type: ignore\n    serializer = JsonSerializer()\n    serialized_ev = serializer.serialize(ev)\n    deseriazlied_ev = serializer.deserialize(serialized_ev)\n\n    assert type(deseriazlied_ev).__name__ == type(ev).__name__\n    deseriazlied_ev = cast(\n        _TestEvent,\n        deseriazlied_ev,\n    )\n    assert ev.param == deseriazlied_ev.param\n    assert ev._data == deseriazlied_ev._data\n\n\ndef test_bool() -> None:\n    assert bool(_TestEvent(param=\"foo\")) is True\n\n\ndef test_stop_event_serialization() -> None:\n    ev = StopEvent(result=\"foo\")\n    data_dict = ev.model_dump()\n    assert data_dict == {\"result\": \"foo\"}\n\n    serializer = JsonSerializer()\n    serialized_ev = serializer.serialize(ev)\n    deseriazlied_ev = serializer.deserialize(serialized_ev)\n\n    assert type(deseriazlied_ev).__name__ == type(ev).__name__\n    deseriazlied_ev = cast(\n        StopEvent,\n        deseriazlied_ev,\n    )\n    assert ev.result == deseriazlied_ev.result\n\n\nclass CustomStopEvent(StopEvent):\n    foo: str\n    bar: int\n\n\ndef test_custom_stop_event_serialization() -> None:\n    ev = CustomStopEvent(foo=\"foo\", bar=42)\n    data_dict = ev.model_dump()\n    assert data_dict == {\"foo\": \"foo\", \"bar\": 42}\n\n    serializer = JsonSerializer()\n    serialized_ev = serializer.serialize(ev)\n    deserialized_ev = serializer.deserialize(serialized_ev)\n\n    assert type(deserialized_ev).__name__ == type(ev).__name__\n    deserialized_ev = cast(\n        CustomStopEvent,\n        deserialized_ev,\n    )\n    assert ev.foo == deserialized_ev.foo\n    assert ev.bar == deserialized_ev.bar\n\n\ndef test_stop_event_repr() -> None:\n    ev = StopEvent(foo=\"foo\", result=42)\n    assert repr(ev) == \"StopEvent(foo='foo', result=42)\"\n\n\ndef test_custom_stop_event_repr_no_result() -> None:\n    ev = CustomStopEvent(foo=\"foo\", bar=42)\n    rep = repr(ev)\n    assert rep == \"CustomStopEvent(foo='foo', bar=42)\"\n\n\n# Tests for workflow termination event subclasses\n\n\ndef test_workflow_termination_events_are_stop_events() -> None:\n    \"\"\"Verify workflow termination events are subclasses of StopEvent.\"\"\"\n    assert issubclass(WorkflowTimedOutEvent, StopEvent)\n    assert issubclass(WorkflowCancelledEvent, StopEvent)\n    assert issubclass(WorkflowFailedEvent, StopEvent)\n\n\ndef test_workflow_timed_out_event() -> None:\n    \"\"\"Test WorkflowTimedOutEvent creation and attributes.\"\"\"\n    ev = WorkflowTimedOutEvent(timeout=30.0, active_steps=[\"step1\", \"step2\"])\n    assert ev.timeout == 30.0\n    assert ev.active_steps == [\"step1\", \"step2\"]\n    assert isinstance(ev, StopEvent)\n\n\ndef test_workflow_timed_out_event_empty_active_steps() -> None:\n    \"\"\"Test WorkflowTimedOutEvent with no active steps.\"\"\"\n    ev = WorkflowTimedOutEvent(timeout=5.0, active_steps=[])\n    assert ev.timeout == 5.0\n    assert ev.active_steps == []\n\n\ndef test_workflow_timed_out_event_serialization() -> None:\n    \"\"\"Test WorkflowTimedOutEvent serialization and deserialization.\"\"\"\n    ev = WorkflowTimedOutEvent(timeout=30.0, active_steps=[\"step1\", \"step2\"])\n    data_dict = ev.model_dump()\n    assert data_dict == {\"timeout\": 30.0, \"active_steps\": [\"step1\", \"step2\"]}\n\n    serializer = JsonSerializer()\n    serialized_ev = serializer.serialize(ev)\n    deserialized_ev = serializer.deserialize(serialized_ev)\n\n    assert type(deserialized_ev).__name__ == type(ev).__name__\n    deserialized_ev = cast(WorkflowTimedOutEvent, deserialized_ev)\n    assert ev.timeout == deserialized_ev.timeout\n    assert ev.active_steps == deserialized_ev.active_steps\n\n\ndef test_workflow_timed_out_event_repr() -> None:\n    \"\"\"Test WorkflowTimedOutEvent string representation.\"\"\"\n    ev = WorkflowTimedOutEvent(timeout=10.0, active_steps=[\"my_step\"])\n    rep = repr(ev)\n    assert \"WorkflowTimedOutEvent\" in rep\n    assert \"timeout=10.0\" in rep\n    assert \"active_steps=['my_step']\" in rep\n\n\ndef test_workflow_cancelled_event() -> None:\n    \"\"\"Test WorkflowCancelledEvent creation.\"\"\"\n    ev = WorkflowCancelledEvent()\n    assert isinstance(ev, StopEvent)\n\n\ndef test_workflow_cancelled_event_serialization() -> None:\n    \"\"\"Test WorkflowCancelledEvent serialization and deserialization.\"\"\"\n    ev = WorkflowCancelledEvent()\n    data_dict = ev.model_dump()\n    assert data_dict == {}\n\n    serializer = JsonSerializer()\n    serialized_ev = serializer.serialize(ev)\n    deserialized_ev = serializer.deserialize(serialized_ev)\n\n    assert type(deserialized_ev).__name__ == type(ev).__name__\n\n\ndef test_workflow_cancelled_event_repr() -> None:\n    \"\"\"Test WorkflowCancelledEvent string representation.\"\"\"\n    ev = WorkflowCancelledEvent()\n    rep = repr(ev)\n    assert rep == \"WorkflowCancelledEvent()\"\n\n\ndef test_workflow_failed_event() -> None:\n    \"\"\"Test WorkflowFailedEvent creation and attributes.\"\"\"\n    ev = WorkflowFailedEvent(\n        step_name=\"my_step\",\n        exception=ValueError(\"Something went wrong\"),\n        attempts=3,\n        elapsed_seconds=1.5,\n    )\n    assert ev.step_name == \"my_step\"\n    assert isinstance(ev.exception, ValueError)\n    assert str(ev.exception) == \"Something went wrong\"\n    assert ev.attempts == 3\n    assert ev.elapsed_seconds == 1.5\n    assert isinstance(ev, StopEvent)\n\n\ndef test_workflow_failed_event_serialization() -> None:\n    \"\"\"Test WorkflowFailedEvent serialization and deserialization.\"\"\"\n    ev = WorkflowFailedEvent(\n        step_name=\"failing_step\",\n        exception=RuntimeError(\"Test failure\"),\n        attempts=2,\n        elapsed_seconds=0.5,\n    )\n    data_dict = ev.model_dump()\n    assert data_dict == {\n        \"step_name\": \"failing_step\",\n        \"exception\": {\n            \"exception_type\": \"builtins.RuntimeError\",\n            \"exception_message\": \"Test failure\",\n        },\n        \"attempts\": 2,\n        \"elapsed_seconds\": 0.5,\n    }\n\n    serializer = JsonSerializer()\n    serialized_ev = serializer.serialize(ev)\n    deserialized_ev = serializer.deserialize(serialized_ev)\n\n    assert type(deserialized_ev).__name__ == type(ev).__name__\n    deserialized_ev = cast(WorkflowFailedEvent, deserialized_ev)\n    assert ev.step_name == deserialized_ev.step_name\n    assert type(ev.exception) is type(deserialized_ev.exception)\n    assert str(ev.exception) == str(deserialized_ev.exception)\n    assert ev.attempts == deserialized_ev.attempts\n    assert ev.elapsed_seconds == deserialized_ev.elapsed_seconds\n\n\ndef test_workflow_failed_event_repr() -> None:\n    \"\"\"Test WorkflowFailedEvent string representation.\"\"\"\n    ev = WorkflowFailedEvent(\n        step_name=\"my_step\",\n        exception=ValueError(\"error msg\"),\n        attempts=1,\n        elapsed_seconds=0.1,\n    )\n    rep = repr(ev)\n    assert \"WorkflowFailedEvent\" in rep\n    assert \"step_name='my_step'\" in rep\n    assert \"error msg\" in rep\n\n\ndef test_workflow_failed_event_with_nested_exception_type() -> None:\n    \"\"\"Test WorkflowFailedEvent with a qualified exception type name.\"\"\"\n    ev = WorkflowFailedEvent(\n        step_name=\"api_step\",\n        exception=HTTPException(\"Connection refused\"),\n        attempts=5,\n        elapsed_seconds=10.0,\n    )\n    assert isinstance(ev.exception, HTTPException)\n    assert ev.attempts == 5\n    assert ev.elapsed_seconds == 10.0\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_event_summary.py",
    "content": "# ty: ignore[unknown-argument]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport pytest\nfrom workflows._event_summary import summarize_event\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\nclass RetrievalEvent(Event):\n    query: str\n    top_k: int = 5\n\n\nclass CustomStopEvent(StopEvent):\n    summary: str = \"\"\n\n\n@pytest.fixture\ndef long_string() -> str:\n    return \"a\" * 200\n\n\n@pytest.fixture\ndef large_list() -> list[int]:\n    return list(range(20))\n\n\n@pytest.fixture\ndef large_dict() -> dict[str, int]:\n    return {f\"key_{i}\": i for i in range(15)}\n\n\ndef test_simple_start_event_with_kwargs() -> None:\n    ev = StartEvent(topic=\"Pirates\")  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    assert result == \"StartEvent(topic='Pirates')\"\n\n\ndef test_stop_event_includes_result() -> None:\n    ev = StopEvent(result=\"hello\")\n    result = summarize_event(ev)\n    assert \"result=\" in result\n    assert \"hello\" in result\n    assert result.startswith(\"StopEvent(\")\n\n\ndef test_custom_event_with_pydantic_fields() -> None:\n    ev = RetrievalEvent(query=\"what is the meaning of life?\", top_k=5)\n    result = summarize_event(ev)\n    assert result.startswith(\"RetrievalEvent(\")\n    assert \"query=\" in result\n    assert \"top_k=5\" in result\n\n\ndef test_long_string_value_truncated(long_string: str) -> None:\n    ev = StartEvent(content=long_string)  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    # The full 200-char string should not appear\n    assert long_string not in result\n    # Should contain truncation indicator\n    assert \"...\" in result\n\n\ndef test_large_list_shows_item_count(large_list: list[int]) -> None:\n    ev = StartEvent(items=large_list)  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    assert \"[20 items]\" in result\n\n\ndef test_large_dict_shows_key_count(large_dict: dict[str, int]) -> None:\n    ev = StartEvent(mapping=large_dict)  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    assert \"{15 keys}\" in result\n\n\ndef test_overall_output_truncated_to_max_length() -> None:\n    ev = RetrievalEvent(\n        query=\"a very long query string that keeps going and going\",\n        top_k=10,\n        extra_field_1=\"some data\",  # type: ignore[unknown-argument]\n        extra_field_2=\"more data\",  # type: ignore[unknown-argument]\n        extra_field_3=\"even more data\",  # type: ignore[unknown-argument]\n    )\n    max_len = 50\n    result = summarize_event(ev, max_length=max_len)\n    assert len(result) <= max_len\n    assert result.endswith(\"...\")\n\n\ndef test_mixed_pydantic_fields_and_data_entries() -> None:\n    ev = RetrievalEvent(query=\"hello world\", top_k=3, source=\"wikipedia\", page=7)  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    assert \"query='hello world'\" in result\n    assert \"top_k=3\" in result\n    assert \"source=\" in result\n    assert \"page=\" in result\n\n\ndef test_stop_event_subclass_includes_result() -> None:\n    ev = CustomStopEvent(result=42, summary=\"done\")  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    assert result.startswith(\"CustomStopEvent(\")\n    assert \"result=\" in result\n    assert \"summary=\" in result\n\n\ndef test_stop_event_with_none_result() -> None:\n    ev = StopEvent()\n    result = summarize_event(ev)\n    assert result == \"StopEvent()\"\n    assert \"result=\" not in result\n\n\ndef test_empty_event() -> None:\n    ev = StartEvent()\n    result = summarize_event(ev)\n    assert result == \"StartEvent()\"\n\n\ndef test_small_list_shown_inline() -> None:\n    ev = StartEvent(items=[1, 2, 3])  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    # Small lists should be shown inline, not as \"[3 items]\"\n    assert \"[1, 2, 3]\" in result or \"items=\" in result\n\n\ndef test_small_dict_shown_inline() -> None:\n    ev = StartEvent(payload={\"a\": 1})  # type: ignore[unknown-argument]\n    result = summarize_event(ev)\n    assert \"payload=\" in result\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_graph_validation.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom workflows.decorators import WorkflowGraphCheck, step\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.representation.validate import (\n    GraphValidationError,\n    build_step_graph,\n    validate_graph,\n)\nfrom workflows.workflow import Workflow\n\n# -- Helpers ------------------------------------------------------------------\n\n\ndef _validate(\n    wf: Workflow,\n    skip_checks: set[WorkflowGraphCheck] | None = None,\n) -> list[GraphValidationError]:\n    step_configs = {name: func._step_config for name, func in wf._get_steps().items()}\n    return validate_graph(\n        steps=step_configs,\n        start_event_class=wf._start_event_class,\n        skip_checks=skip_checks,\n    )\n\n\ndef _errors_by_check(\n    errors: list[GraphValidationError], check: WorkflowGraphCheck\n) -> list[GraphValidationError]:\n    return [e for e in errors if e.check == check]\n\n\n# -- Event classes ------------------------------------------------------------\n\n\nclass IslandEvent(Event):\n    pass\n\n\nclass ProcessedEvent(Event):\n    pass\n\n\nclass CycleA(Event):\n    pass\n\n\nclass CycleB(Event):\n    pass\n\n\nclass LoopEvent(Event):\n    pass\n\n\n# -- Tests: validate_graph ----------------------------------------------------\n\n\ndef test_validate_simple_valid() -> None:\n    class Simple(Workflow):\n        @step\n        async def process(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    assert _validate(Simple()) == []\n\n\ndef test_validate_unreachable_step() -> None:\n    class Unreachable(Workflow):\n        @step\n        async def process(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step\n        async def island(self, ev: IslandEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    errors = _validate(Unreachable())\n    reachability = _errors_by_check(errors, \"reachability\")\n    assert len(reachability) == 1\n    assert \"island\" in reachability[0].step_names\n\n\ndef test_validate_human_response_reachable() -> None:\n    class MyHumanResponse(HumanResponseEvent):\n        pass\n\n    class HumanLoop(Workflow):\n        @step\n        async def start_step(self, ev: StartEvent) -> InputRequiredEvent:\n            return InputRequiredEvent()\n\n        @step\n        async def human_step(self, ev: MyHumanResponse) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    assert _validate(HumanLoop()) == []\n\n\ndef test_validate_human_response_mutation_allowed() -> None:\n    \"\"\"A HumanResponseEvent step returning None is valid (mutation-only).\"\"\"\n\n    class MyHumanResponse(HumanResponseEvent):\n        pass\n\n    class MutationOnly(Workflow):\n        @step\n        async def start_step(self, ev: StartEvent) -> InputRequiredEvent:\n            return InputRequiredEvent()\n\n        @step\n        async def human_step(self, ev: MyHumanResponse) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step\n        async def mutation(self, ev: MyHumanResponse) -> None:\n            pass\n\n    assert _validate(MutationOnly()) == []\n\n\ndef test_validate_terminal_non_output_event() -> None:\n    class Dangling(Workflow):\n        @step\n        async def process(self, ev: StartEvent) -> ProcessedEvent | StopEvent:\n            return ProcessedEvent()\n\n    errors = _validate(Dangling())\n    terminal = _errors_by_check(errors, \"terminal_event\")\n    assert len(terminal) == 1\n    assert \"ProcessedEvent\" in terminal[0].message\n\n\ndef test_validate_terminal_event_accumulated() -> None:\n    \"\"\"Multiple dangling events in a single terminal_event error.\"\"\"\n\n    class DanglingA(Event):\n        pass\n\n    class DanglingB(Event):\n        pass\n\n    class MultiDangling(Workflow):\n        @step\n        async def process(self, ev: StartEvent) -> DanglingA | DanglingB | StopEvent:\n            return DanglingA()\n\n    errors = _validate(MultiDangling())\n    terminal = _errors_by_check(errors, \"terminal_event\")\n    assert len(terminal) == 1\n    assert (\n        terminal[0].message\n        == \"Events produced but never consumed: DanglingA, DanglingB\"\n    )\n\n\ndef test_validate_dead_end_cycle() -> None:\n    \"\"\"A cycle with no exit to StopEvent produces a dead_end error.\"\"\"\n\n    class DeadEndCycle(Workflow):\n        @step\n        async def entry(self, ev: StartEvent) -> CycleA | StopEvent:\n            return CycleA()\n\n        @step\n        async def step_b(self, ev: CycleA) -> CycleB:\n            return CycleB()\n\n        @step\n        async def step_c(self, ev: CycleB) -> CycleA:\n            return CycleA()\n\n    errors = _validate(DeadEndCycle())\n    dead_end = _errors_by_check(errors, \"dead_end\")\n    assert len(dead_end) == 1\n    # entry has a StopEvent branch so it's not a dead end, but step_b and step_c are\n    assert \"step_b\" in dead_end[0].step_names\n    assert \"step_c\" in dead_end[0].step_names\n\n\ndef test_validate_dead_end_with_exit_branch_passes() -> None:\n    \"\"\"A cycle where one branch reaches StopEvent passes.\"\"\"\n\n    class CycleWithExit(Workflow):\n        @step\n        async def step_a(self, ev: StartEvent) -> CycleA | StopEvent:\n            return CycleA()\n\n        @step\n        async def step_b(self, ev: CycleA) -> CycleB | StopEvent:\n            return CycleB()\n\n        @step\n        async def step_c(self, ev: CycleB) -> CycleA:\n            return CycleA()\n\n    assert _validate(CycleWithExit()) == []\n\n\ndef test_validate_skip_reachability_per_step() -> None:\n    class SkipReach(Workflow):\n        @step\n        async def process(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step(skip_graph_checks=[\"reachability\"])\n        async def island(self, ev: IslandEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    errors = _validate(SkipReach())\n    assert _errors_by_check(errors, \"reachability\") == []\n\n\ndef test_validate_skip_reachability_workflow_level() -> None:\n    class WithIsland(Workflow):\n        @step\n        async def process(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step\n        async def island(self, ev: IslandEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    errors = _validate(WithIsland(), skip_checks={\"reachability\"})\n    assert _errors_by_check(errors, \"reachability\") == []\n\n\ndef test_validate_skip_terminal_event_workflow_level() -> None:\n    class DanglingWf(Workflow):\n        @step\n        async def process(self, ev: StartEvent) -> ProcessedEvent | StopEvent:\n            return ProcessedEvent()\n\n    errors = _validate(DanglingWf(), skip_checks={\"terminal_event\"})\n    assert _errors_by_check(errors, \"terminal_event\") == []\n\n\ndef test_validate_skip_dead_end_per_step() -> None:\n    class SkipDeadEnd(Workflow):\n        @step\n        async def entry(self, ev: StartEvent) -> CycleA | StopEvent:\n            return CycleA()\n\n        @step(skip_graph_checks=[\"dead_end\"])\n        async def step_b(self, ev: CycleA) -> CycleB:\n            return CycleB()\n\n        @step(skip_graph_checks=[\"dead_end\"])\n        async def step_c(self, ev: CycleB) -> CycleA:\n            return CycleA()\n\n    errors = _validate(SkipDeadEnd())\n    assert _errors_by_check(errors, \"dead_end\") == []\n\n\ndef test_validate_skip_dead_end_workflow_level() -> None:\n    class DeadEndWf(Workflow):\n        @step\n        async def entry(self, ev: StartEvent) -> CycleA | StopEvent:\n            return CycleA()\n\n        @step\n        async def step_b(self, ev: CycleA) -> CycleB:\n            return CycleB()\n\n        @step\n        async def step_c(self, ev: CycleB) -> CycleA:\n            return CycleA()\n\n    errors = _validate(DeadEndWf(), skip_checks={\"dead_end\"})\n    assert _errors_by_check(errors, \"dead_end\") == []\n\n\ndef test_validate_multiple_errors_accumulated() -> None:\n    \"\"\"A graph that fails both reachability and dead-end returns 2+ errors.\"\"\"\n\n    class MultiError(Workflow):\n        @step\n        async def cycle_a(self, ev: StartEvent) -> CycleA | StopEvent:\n            return CycleA()\n\n        @step\n        async def cycle_b(self, ev: CycleA) -> CycleB:\n            return CycleB()\n\n        @step\n        async def cycle_c(self, ev: CycleB) -> CycleA:\n            return CycleA()\n\n        @step\n        async def island(self, ev: IslandEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    errors = _validate(MultiError())\n    detail = \"\\n\".join(f\"  - [{e.check}] {e.message}\\n    {e.hint}\" for e in errors)\n    msg = f\"Graph validation failed:\\n{detail}\"\n    assert (\n        msg\n        == \"\"\"\\\nGraph validation failed:\n  - [reachability] Unreachable steps: island\n    Steps must be reachable from StartEvent or HumanResponseEvent.\n  - [dead_end] Dead-end steps: cycle_b, cycle_c\n    Steps must have a path to StopEvent or InputRequiredEvent.\"\"\"\n    )\n\n\n# -- Tests: build_step_graph -------------------------------------------------\n\n\ndef test_build_step_graph_empty() -> None:\n    graph = build_step_graph(steps={}, start_event_class=StartEvent)\n    assert graph.step_names == set()\n    assert graph.outgoing == {}\n    assert graph.event_types == set()\n    assert StartEvent in graph.forward_reachable\n\n\ndef test_build_step_graph_adjacency_list() -> None:\n    class Chain(Workflow):\n        @step\n        async def step_a(self, ev: StartEvent) -> ProcessedEvent:\n            return ProcessedEvent()\n\n        @step\n        async def step_b(self, ev: ProcessedEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = Chain()\n    step_configs = {name: func._step_config for name, func in wf._get_steps().items()}\n    graph = build_step_graph(step_configs, start_event_class=StartEvent)\n\n    assert graph.step_names == {\"step_a\", \"step_b\"}\n    assert \"step_a\" in graph.outgoing[StartEvent]\n    assert ProcessedEvent in graph.outgoing[\"step_a\"]\n    assert \"step_b\" in graph.outgoing[ProcessedEvent]\n    assert StopEvent in graph.outgoing[\"step_b\"]\n\n\ndef test_build_step_graph_event_types() -> None:\n    class WithEvents(Workflow):\n        @step\n        async def step_a(self, ev: StartEvent) -> CycleA | StopEvent:\n            return CycleA()\n\n    wf = WithEvents()\n    step_configs = {name: func._step_config for name, func in wf._get_steps().items()}\n    graph = build_step_graph(step_configs, start_event_class=StartEvent)\n\n    assert graph.event_types == {StartEvent, CycleA, StopEvent}\n\n\ndef test_build_step_graph_none_return_type_excluded() -> None:\n    \"\"\"Steps returning None should not add NoneType to the adjacency list.\"\"\"\n\n    class NoneReturn(Workflow):\n        @step\n        async def step_a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step\n        async def mutation(self, ev: StartEvent) -> None:\n            pass\n\n    wf = NoneReturn()\n    step_configs = {name: func._step_config for name, func in wf._get_steps().items()}\n    graph = build_step_graph(step_configs, start_event_class=StartEvent)\n\n    assert type(None) not in graph.event_types\n    assert \"mutation\" not in graph.outgoing\n\n\ndef test_build_step_graph_forward_reachable() -> None:\n    class WithIsland(Workflow):\n        @step\n        async def step_a(self, ev: StartEvent) -> ProcessedEvent:\n            return ProcessedEvent()\n\n        @step\n        async def step_b(self, ev: ProcessedEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step\n        async def island(self, ev: IslandEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WithIsland()\n    step_configs = {name: func._step_config for name, func in wf._get_steps().items()}\n    graph = build_step_graph(step_configs, start_event_class=StartEvent)\n\n    assert \"step_a\" in graph.forward_reachable\n    assert \"step_b\" in graph.forward_reachable\n    assert ProcessedEvent in graph.forward_reachable\n    assert \"island\" not in graph.forward_reachable\n    assert IslandEvent not in graph.forward_reachable\n\n\ndef test_build_step_graph_forward_reachable_human_response_seed() -> None:\n    \"\"\"HumanResponseEvent subclasses act as additional forward-reachability seeds.\"\"\"\n\n    class TestHumanResponse(HumanResponseEvent):\n        pass\n\n    class HumanLoop(Workflow):\n        @step\n        async def start_step(self, ev: StartEvent) -> InputRequiredEvent:\n            return InputRequiredEvent()\n\n        @step\n        async def human_step(self, ev: TestHumanResponse) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = HumanLoop()\n    step_configs = {name: func._step_config for name, func in wf._get_steps().items()}\n    graph = build_step_graph(step_configs, start_event_class=StartEvent)\n\n    assert \"human_step\" in graph.forward_reachable\n    assert TestHumanResponse in graph.forward_reachable\n\n\ndef test_build_step_graph_reverse_reachable() -> None:\n    class WithDeadEnd(Workflow):\n        @step\n        async def step_a(self, ev: StartEvent) -> ProcessedEvent:\n            return ProcessedEvent()\n\n        @step\n        async def step_b(self, ev: ProcessedEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step\n        async def dead_end(self, ev: StartEvent) -> LoopEvent:\n            return LoopEvent()\n\n        @step\n        async def loop(self, ev: LoopEvent) -> LoopEvent:\n            return LoopEvent()\n\n    wf = WithDeadEnd()\n    step_configs = {name: func._step_config for name, func in wf._get_steps().items()}\n    graph = build_step_graph(step_configs, start_event_class=StartEvent)\n\n    assert \"step_a\" in graph.reverse_reachable\n    assert \"step_b\" in graph.reverse_reachable\n    assert \"dead_end\" not in graph.reverse_reachable\n    assert \"loop\" not in graph.reverse_reachable\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_handler.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom unittest import mock\n\nimport pytest\nfrom workflows.errors import WorkflowRuntimeError\nfrom workflows.handler import WorkflowHandler\nfrom workflows.workflow import Workflow\n\nfrom tests.runtime.conftest import MockRunAdapter\n\n\ndef _create_mock_handler() -> WorkflowHandler:\n    \"\"\"Create a WorkflowHandler with MockRunAdapter.\"\"\"\n    workflow = mock.MagicMock(spec=Workflow)\n    workflow.workflow_name = \"TestWorkflow\"\n    adapter = MockRunAdapter(run_id=\"test-run-id\")\n    return WorkflowHandler(workflow=workflow, external_adapter=adapter)\n\n\n@pytest.mark.asyncio\nasync def test_str() -> None:\n    h = _create_mock_handler()\n    # The str representation shows workflow name, run_id, and result\n    assert \"TestWorkflow\" in str(h)\n    assert \"test-run-id\" in str(h)\n\n\n@pytest.mark.asyncio\nasync def test_stream_events_consume_only_once() -> None:\n    h = _create_mock_handler()\n    h._all_events_consumed = True\n\n    with pytest.raises(\n        WorkflowRuntimeError,\n        match=\"All the streamed events have already been consumed.\",\n    ):\n        async for _ in h.stream_events():\n            pass\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_llama_agents_alias.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n# pyright: reportMissingImports=false\n\"\"\"Tests that the llama_agents.workflows alias resolves correctly.\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\n\n\ndef test_top_level_import() -> None:\n    from llama_agents.workflows import Context, Workflow, step\n\n    assert Workflow is not None\n    assert Context is not None\n    assert callable(step)\n\n\ndef test_top_level_identity() -> None:\n    import llama_agents.workflows as alias\n    import workflows\n\n    assert alias.Workflow is workflows.Workflow\n    assert alias.Context is workflows.Context\n    assert alias.step is workflows.step\n\n\ndef test_events_submodule() -> None:\n    from llama_agents.workflows.events import (\n        Event,\n        StartEvent,\n        StopEvent,\n    )\n\n    assert Event is not None\n    assert issubclass(StartEvent, Event)\n    assert issubclass(StopEvent, Event)\n\n\ndef test_events_submodule_identity() -> None:\n    import llama_agents.workflows.events as alias_events\n    import workflows.events\n\n    assert alias_events is workflows.events\n    assert alias_events.Event is workflows.events.Event\n\n\ndef test_context_submodule() -> None:\n    from llama_agents.workflows.context import Context\n\n    assert Context is not None\n\n\ndef test_context_submodule_identity() -> None:\n    import llama_agents.workflows.context as alias_ctx\n    import workflows.context\n\n    assert alias_ctx is workflows.context\n\n\ndef test_workflow_submodule() -> None:\n    from llama_agents.workflows.workflow import Workflow\n\n    assert Workflow is not None\n\n\ndef test_decorators_submodule() -> None:\n    from llama_agents.workflows.decorators import step\n\n    assert callable(step)\n\n\ndef test_errors_submodule() -> None:\n    from llama_agents.workflows.errors import (\n        WorkflowRuntimeError,\n        WorkflowTimeoutError,\n        WorkflowValidationError,\n    )\n\n    assert issubclass(WorkflowRuntimeError, Exception)\n    assert issubclass(WorkflowTimeoutError, Exception)\n    assert issubclass(WorkflowValidationError, Exception)\n\n\ndef test_handler_submodule() -> None:\n    from llama_agents.workflows.handler import WorkflowHandler\n\n    assert WorkflowHandler is not None\n\n\ndef test_testing_submodule() -> None:\n    from llama_agents.workflows.testing import WorkflowTestRunner\n\n    assert WorkflowTestRunner is not None\n\n\ndef test_deep_submodule() -> None:\n    from llama_agents.workflows.context.context import Context\n\n    assert Context is not None\n\n\ndef test_dunder_all_reexported() -> None:\n    import llama_agents.workflows as alias\n    import workflows\n\n    assert alias.__all__ == workflows.__all__\n\n\n@pytest.mark.asyncio\nasync def test_alias_workflow_runs() -> None:\n    \"\"\"A workflow defined via the alias module actually executes.\"\"\"\n    from llama_agents.workflows import Context, Workflow, step\n    from llama_agents.workflows.events import StartEvent, StopEvent\n\n    class HelloWorkflow(Workflow):\n        @step\n        async def say_hello(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"hello\")\n\n    wf = HelloWorkflow()\n    result = await wf.run()\n    assert result == \"hello\"\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_nanoid.py",
    "content": "from workflows.utils import _nanoid as nanoid\n\n\ndef test_nanoid_default_length() -> None:\n    \"\"\"Test nanoid with default length.\"\"\"\n    result = nanoid()\n    assert len(result) == 10\n    assert isinstance(result, str)\n\n\ndef test_nanoid_custom_length() -> None:\n    \"\"\"Test nanoid with custom length.\"\"\"\n    result = nanoid(3)\n    assert len(result) == 3\n\n\ndef test_nanoid_zero_length() -> None:\n    \"\"\"Test nanoid with zero length.\"\"\"\n    result = nanoid(0)\n    assert len(result) == 0\n    assert result == \"\"\n\n\ndef test_nanoid_uniqueness() -> None:\n    \"\"\"Test that nanoid generates unique IDs.\"\"\"\n    # Generate multiple IDs and check for uniqueness\n    ids = [nanoid() for _ in range(1000)]\n    unique_ids = set(ids)\n\n    # Should be very unlikely to have duplicates with 10-char alphanumeric\n    # With 62^10 possible combinations, 1000 IDs should be unique\n    assert len(ids) == len(unique_ids)\n\n\ndef test_nanoid_negative_length() -> None:\n    \"\"\"Test nanoid behavior with negative length.\"\"\"\n    # Python range() with negative values returns empty range\n    result = nanoid(-1)\n    assert result == \"\"\n\n    result = nanoid(-10)\n    assert result == \"\"\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_representation_utils.py",
    "content": "import json\nfrom collections.abc import Iterable, Mapping\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport pytest\nfrom pydantic import BaseModel\nfrom workflows.decorators import step\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.representation import (\n    WorkflowEventNode,\n    WorkflowExternalNode,\n    WorkflowGraph,\n    WorkflowGraphEdge,\n    WorkflowGraphNode,\n    WorkflowResourceConfigNode,\n    WorkflowResourceNode,\n    WorkflowStepNode,\n    get_workflow_representation,\n)\nfrom workflows.resource import Resource, ResourceConfig\nfrom workflows.workflow import Workflow\n\nfrom .conftest import DummyWorkflow  # type: ignore[import]\n\n\ndef _nodes_of_type(graph: WorkflowGraph, node_type: str) -> list[WorkflowGraphNode]:\n    return [node for node in graph.nodes if node.node_type == node_type]\n\n\ndef _resource_nodes(graph: WorkflowGraph) -> list[WorkflowResourceNode]:\n    return [node for node in graph.nodes if isinstance(node, WorkflowResourceNode)]\n\n\ndef _resource_config_nodes(graph: WorkflowGraph) -> list[WorkflowResourceConfigNode]:\n    return [\n        node for node in graph.nodes if isinstance(node, WorkflowResourceConfigNode)\n    ]\n\n\ndef _edges_as_tuples(graph: WorkflowGraph) -> set[tuple[str, str, str | None]]:\n    return {(edge.source, edge.target, edge.label) for edge in graph.edges}\n\n\ndef _find_edges(\n    graph: WorkflowGraph,\n    *,\n    source: str | None = None,\n    target_prefix: str | None = None,\n    label: str | None = None,\n) -> list[WorkflowGraphEdge]:\n    edges: Iterable[WorkflowGraphEdge] = graph.edges\n    if source is not None:\n        edges = [edge for edge in edges if edge.source == source]\n    if target_prefix is not None:\n        edges = [edge for edge in edges if edge.target.startswith(target_prefix)]\n    if label is not None:\n        edges = [edge for edge in edges if edge.label == label]\n    return list(edges)\n\n\n@pytest.fixture()\ndef ground_truth_repr() -> WorkflowGraph:\n    return WorkflowGraph(\n        name=\"DummyWorkflow\",\n        nodes=[\n            WorkflowStepNode(\n                id=\"end_step\",\n                label=\"end_step\",\n            ),\n            WorkflowEventNode(\n                id=\"LastEvent\",\n                label=\"LastEvent\",\n                event_type=\"LastEvent\",\n                event_types=[\"LastEvent\"],\n            ),\n            WorkflowEventNode(\n                id=\"StopEvent\",\n                label=\"StopEvent\",\n                event_type=\"StopEvent\",\n                event_types=[\"StopEvent\"],\n            ),\n            WorkflowStepNode(\n                id=\"middle_step\",\n                label=\"middle_step\",\n            ),\n            WorkflowEventNode(\n                id=\"OneTestEvent\",\n                label=\"OneTestEvent\",\n                event_type=\"OneTestEvent\",\n                event_types=[\"OneTestEvent\"],\n            ),\n            WorkflowStepNode(\n                id=\"start_step\",\n                label=\"start_step\",\n            ),\n            WorkflowEventNode(\n                id=\"StartEvent\",\n                label=\"StartEvent\",\n                event_type=\"StartEvent\",\n                event_types=[\"StartEvent\"],\n            ),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"end_step\", target=\"StopEvent\"),\n            WorkflowGraphEdge(source=\"LastEvent\", target=\"end_step\"),\n            WorkflowGraphEdge(source=\"middle_step\", target=\"LastEvent\"),\n            WorkflowGraphEdge(source=\"OneTestEvent\", target=\"middle_step\"),\n            WorkflowGraphEdge(source=\"start_step\", target=\"OneTestEvent\"),\n            WorkflowGraphEdge(source=\"StartEvent\", target=\"start_step\"),\n        ],\n    )\n\n\ndef test_get_workflow_representation(ground_truth_repr: WorkflowGraph) -> None:\n    wf = DummyWorkflow()\n    graph = get_workflow_representation(workflow=wf)\n    assert isinstance(graph, WorkflowGraph)\n    assert sorted(\n        node.id for node in _nodes_of_type(ground_truth_repr, \"step\")\n    ) == sorted(node.id for node in _nodes_of_type(graph, \"step\"))\n    assert sorted(\n        node.id for node in _nodes_of_type(ground_truth_repr, \"event\")\n    ) == sorted(node.id for node in _nodes_of_type(graph, \"event\"))\n    assert _edges_as_tuples(graph) >= _edges_as_tuples(ground_truth_repr)\n\n\ndef test_representation_hitl_includes_external_step_bridge() -> None:\n    \"\"\"HITL workflows get external_step node and bridging edges for graph validation.\"\"\"\n\n    class HITLWorkflow(Workflow):\n        @step\n        async def ask(self, ev: StartEvent) -> InputRequiredEvent:\n            return InputRequiredEvent()\n\n        @step\n        async def handle(self, ev: HumanResponseEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n    wf = HITLWorkflow()\n    graph = get_workflow_representation(workflow=wf)\n    node_ids = {n.id for n in graph.nodes}\n    assert \"external_step\" in node_ids\n    edges = _edges_as_tuples(graph)\n    assert (\"InputRequiredEvent\", \"external_step\", None) in edges or any(\n        e[0] == \"InputRequiredEvent\" and e[1] == \"external_step\" for e in edges\n    )\n    assert (\"external_step\", \"HumanResponseEvent\", None) in edges or any(\n        e[0] == \"external_step\" and e[1] == \"HumanResponseEvent\" for e in edges\n    )\n\n\ndef test_truncated_label() -> None:\n    \"\"\"Test that truncated_label method works correctly.\"\"\"\n    node = WorkflowStepNode(id=\"my_step\", label=\"my_long_step_name\")\n    assert node.truncated_label(5) == \"my_l*\"\n    assert node.truncated_label(20) == \"my_long_step_name\"\n    assert node.truncated_label(17) == \"my_long_step_name\"\n\n\ndef test_graph_serialization() -> None:\n    \"\"\"Test that WorkflowGraph serializes and restores node types.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"test\", label=\"test\"),\n            WorkflowEventNode(\n                id=\"OneTestEvent\",\n                label=\"OneTestEvent\",\n                event_type=\"OneTestEvent\",\n                event_types=[\"OneTestEvent\"],\n            ),\n        ],\n        edges=[WorkflowGraphEdge(source=\"test\", target=\"OneTestEvent\")],\n    )\n\n    data = graph.model_dump()\n    restored = WorkflowGraph.model_validate(data)\n\n    assert len(restored.nodes) == 2\n    event_node = next(\n        node for node in restored.nodes if isinstance(node, WorkflowEventNode)\n    )\n    assert event_node.event_type == \"OneTestEvent\"\n    assert event_node.is_subclass_of(\"OneTestEvent\")\n\n\n# --- Resource node tests ---\n\n\nclass DatabaseClient:\n    \"\"\"A mock database client for testing resources.\"\"\"\n\n    pass\n\n\ndef get_database_client() -> DatabaseClient:\n    \"\"\"Factory function to create a database client.\n\n    This docstring should appear in the resource metadata.\n    \"\"\"\n    return DatabaseClient()\n\n\nclass MiddleEvent(Event):\n    pass\n\n\nclass WorkflowWithResources(Workflow):\n    @step\n    async def start_step(self, ev: StartEvent) -> MiddleEvent:\n        return MiddleEvent()\n\n    @step\n    async def step_with_resource(\n        self,\n        ev: MiddleEvent,\n        db_client: Annotated[DatabaseClient, Resource(get_database_client)],\n    ) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\ndef test_get_workflow_representation_with_resources() -> None:\n    \"\"\"Resource node metadata and step -> resource edge label are derived from factory.\"\"\"\n    wf = WorkflowWithResources()\n    graph = get_workflow_representation(workflow=wf)\n\n    resource_nodes = _resource_nodes(graph)\n    assert len(resource_nodes) == 1\n    resource_node = resource_nodes[0]\n    assert resource_node.type_name == \"DatabaseClient\"\n    assert resource_node.getter_name == \"get_database_client\"\n    assert resource_node.description is not None\n    assert resource_node.source_file is not None\n    assert resource_node.source_line is not None\n    edges = _find_edges(\n        graph,\n        source=\"step_with_resource\",\n        target_prefix=\"resource_\",\n        label=\"db_client\",\n    )\n    assert len(edges) == 1\n\n\ndef test_resource_nodes_are_deduplicated() -> None:\n    \"\"\"Test that the same resource used in multiple steps appears only once.\"\"\"\n\n    class StepEvent(Event):\n        pass\n\n    class WorkflowWithSharedResource(Workflow):\n        @step\n        async def start_step(self, ev: StartEvent) -> StepEvent:\n            return StepEvent()\n\n        @step\n        async def step_one(\n            self,\n            ev: StepEvent,\n            db: Annotated[DatabaseClient, Resource(get_database_client)],\n        ) -> MiddleEvent:\n            return MiddleEvent()\n\n        @step\n        async def step_two(\n            self,\n            ev: MiddleEvent,\n            db: Annotated[DatabaseClient, Resource(get_database_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithSharedResource()\n    graph = get_workflow_representation(workflow=wf)\n\n    # Should have only one resource node (deduplicated)\n    assert len(_resource_nodes(graph)) == 1\n\n    # But should have two edges (one from each step)\n    resource_edges = _find_edges(graph, target_prefix=\"resource_\", label=\"db\")\n    assert len(resource_edges) == 2\n\n\ndef test_multiple_different_resources() -> None:\n    \"\"\"Test workflow with multiple different resources.\"\"\"\n\n    class CacheClient:\n        pass\n\n    def get_cache_client() -> CacheClient:\n        return CacheClient()\n\n    class WorkflowWithMultipleResources(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            db: Annotated[DatabaseClient, Resource(get_database_client)],\n            cache: Annotated[CacheClient, Resource(get_cache_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithMultipleResources()\n    graph = get_workflow_representation(workflow=wf)\n\n    # Should have two different resource nodes\n    resource_nodes = _resource_nodes(graph)\n    assert len(resource_nodes) == 2\n\n    type_names = {rn.type_name for rn in resource_nodes}\n    assert type_names == {\"DatabaseClient\", \"CacheClient\"}\n\n    # Should have two edges with different labels\n    resource_edges = _find_edges(graph, target_prefix=\"resource_\")\n    assert len(resource_edges) == 2\n\n    labels = {e.label for e in resource_edges}\n    assert labels == {\"db\", \"cache\"}\n\n\ndef test_edge_with_label() -> None:\n    \"\"\"Test that WorkflowGraphEdge with label works correctly.\"\"\"\n    edge = WorkflowGraphEdge(source=\"resource_123\", target=\"my_step\", label=\"my_var\")\n\n    assert edge.source == \"resource_123\"\n    assert edge.target == \"my_step\"\n    assert edge.label == \"my_var\"\n\n\ndef test_edge_without_label() -> None:\n    \"\"\"Test that WorkflowGraphEdge without label works correctly.\"\"\"\n    edge = WorkflowGraphEdge(source=\"event_A\", target=\"step_B\")\n\n    assert edge.source == \"event_A\"\n    assert edge.target == \"step_B\"\n    assert edge.label is None\n\n\ndef test_graph_with_all_node_types_serialization() -> None:\n    \"\"\"Test full graph serialization/deserialization with all node types.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowEventNode(\n                id=\"StartEvent\",\n                label=\"StartEvent\",\n                event_type=\"StartEvent\",\n                event_types=[\"StartEvent\"],\n            ),\n            WorkflowExternalNode(id=\"external\", label=\"External\"),\n            WorkflowResourceNode(\n                id=\"resource_123\",\n                label=\"DB\",\n                type_name=\"DatabaseClient\",\n                getter_name=\"get_db\",\n            ),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"StartEvent\", target=\"step1\"),\n            WorkflowGraphEdge(source=\"step1\", target=\"resource_123\", label=\"db\"),\n        ],\n    )\n\n    # Serialize\n    data = graph.model_dump()\n    assert len(data[\"nodes\"]) == 4\n    assert len(data[\"edges\"]) == 2\n\n    # Check discriminator values are present\n    node_types = {n[\"node_type\"] for n in data[\"nodes\"]}\n    assert node_types == {\"step\", \"event\", \"external\", \"resource\"}\n\n    # Deserialize\n    restored = WorkflowGraph.model_validate(data)\n    assert len(restored.nodes) == 4\n    assert len(restored.edges) == 2\n\n    # Check correct types restored\n    step_nodes = [n for n in restored.nodes if isinstance(n, WorkflowStepNode)]\n    event_nodes = [n for n in restored.nodes if isinstance(n, WorkflowEventNode)]\n    external_nodes = [n for n in restored.nodes if isinstance(n, WorkflowExternalNode)]\n    resource_nodes = [n for n in restored.nodes if isinstance(n, WorkflowResourceNode)]\n\n    assert len(step_nodes) == 1\n    assert len(event_nodes) == 1\n    assert len(external_nodes) == 1\n    assert len(resource_nodes) == 1\n\n    # Verify event node has its method\n    assert event_nodes[0].is_subclass_of(\"StartEvent\")\n\n    # Verify resource node has its fields\n    assert resource_nodes[0].type_name == \"DatabaseClient\"\n    assert resource_nodes[0].getter_name == \"get_db\"\n\n\ndef test_graph_deserialization_from_raw_json() -> None:\n    \"\"\"Test that graph can be deserialized from raw JSON dict.\"\"\"\n    raw_data = {\n        \"name\": \"TestWorkflow\",\n        \"nodes\": [\n            {\"id\": \"step1\", \"label\": \"Step 1\", \"node_type\": \"step\"},\n            {\n                \"id\": \"MyEvent\",\n                \"label\": \"MyEvent\",\n                \"node_type\": \"event\",\n                \"event_type\": \"MyEvent\",\n                \"event_types\": [\"MyEvent\"],\n            },\n            {\"id\": \"external\", \"label\": \"External\", \"node_type\": \"external\"},\n            {\n                \"id\": \"resource_xyz\",\n                \"label\": \"Resource\",\n                \"node_type\": \"resource\",\n                \"type_name\": \"SomeType\",\n            },\n            {\n                \"id\": \"resource_config_456\",\n                \"label\": \"ConfigModel\",\n                \"node_type\": \"resource_config\",\n                \"type_name\": \"ConfigModel\",\n                \"config_file\": \"config.json\",\n                \"path_selector\": \"settings\",\n                \"config_schema\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"key\": {\"type\": \"string\"}},\n                },\n                \"config_value\": {\"key\": \"value\"},\n            },\n        ],\n        \"edges\": [{\"source\": \"MyEvent\", \"target\": \"step1\"}],\n    }\n\n    graph = WorkflowGraph.model_validate(raw_data)\n\n    assert len(graph.nodes) == 5\n    node_types = {node.node_type for node in graph.nodes}\n    assert node_types == {\"step\", \"event\", \"external\", \"resource\", \"resource_config\"}\n\n\n# --- filter_by_node_type tests ---\n\n\ndef test_filter_by_node_type_removes_nodes() -> None:\n    \"\"\"Test that filter_by_node_type removes specified node types.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"EventA\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"EventA\"),\n            WorkflowGraphEdge(source=\"EventA\", target=\"step2\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    # Event nodes should be removed\n    assert len(filtered.nodes) == 2\n    assert all(n.node_type == \"step\" for n in filtered.nodes)\n    node_ids = {n.id for n in filtered.nodes}\n    assert node_ids == {\"step1\", \"step2\"}\n\n\ndef test_filter_by_node_type_resolves_edges() -> None:\n    \"\"\"Test that edges through filtered nodes are resolved.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"EventA\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"EventA\"),\n            WorkflowGraphEdge(source=\"EventA\", target=\"step2\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    # Edge should be resolved: step1 -> step2\n    assert len(filtered.edges) == 1\n    assert filtered.edges[0].source == \"step1\"\n    assert filtered.edges[0].target == \"step2\"\n\n\ndef test_filter_by_node_type_chain_of_filtered_nodes() -> None:\n    \"\"\"Test filtering handles chains of filtered nodes.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"First Filtered Node\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n            WorkflowEventNode(\n                id=\"EventB\",\n                label=\"Second Filtered Node\",\n                event_type=\"EventB\",\n                event_types=[\"EventB\"],\n            ),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"EventA\"),\n            WorkflowGraphEdge(source=\"EventA\", target=\"EventB\"),\n            WorkflowGraphEdge(source=\"EventB\", target=\"step2\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    # Chain resolved: step1 -> step2, with first filtered node's label\n    assert len(filtered.nodes) == 2\n    assert len(filtered.edges) == 1\n    assert filtered.edges[0].source == \"step1\"\n    assert filtered.edges[0].target == \"step2\"\n    assert filtered.edges[0].label == \"First Filtered Node\"\n\n\ndef test_filter_by_node_type_multiple_types() -> None:\n    \"\"\"Test filtering multiple node types at once.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"EventA\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n            WorkflowResourceNode(id=\"resource1\", label=\"Resource\"),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"EventA\"),\n            WorkflowGraphEdge(source=\"step1\", target=\"resource1\", label=\"db\"),\n            WorkflowGraphEdge(source=\"EventA\", target=\"step2\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\", \"resource\")\n\n    # Only step nodes remain\n    assert len(filtered.nodes) == 2\n    assert all(n.node_type == \"step\" for n in filtered.nodes)\n    # step1 -> step2 edge remains (resolved through EventA)\n    assert len(filtered.edges) == 1\n    assert filtered.edges[0].source == \"step1\"\n    assert filtered.edges[0].target == \"step2\"\n\n\ndef test_filter_by_node_type_preserves_direct_edges() -> None:\n    \"\"\"Test that direct edges between remaining nodes are preserved.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"EventA\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"step2\"),  # Direct edge\n            WorkflowGraphEdge(source=\"step2\", target=\"EventA\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    # Direct edge should be preserved\n    assert len(filtered.edges) == 1\n    assert filtered.edges[0].source == \"step1\"\n    assert filtered.edges[0].target == \"step2\"\n\n\ndef test_filter_by_node_type_uses_filtered_node_label() -> None:\n    \"\"\"Test that the first filtered node's label becomes the new edge label.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"My Event Label\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"EventA\"),\n            WorkflowGraphEdge(source=\"EventA\", target=\"step2\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    # Label from filtered node should be on the new edge\n    assert len(filtered.edges) == 1\n    assert filtered.edges[0].label == \"My Event Label\"\n\n\ndef test_filter_by_node_type_preserves_direct_edge_labels() -> None:\n    \"\"\"Test that labels on direct edges are preserved.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowResourceNode(id=\"resource1\", label=\"Resource\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"EventA\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"resource1\", label=\"db\"),\n            WorkflowGraphEdge(source=\"step1\", target=\"EventA\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    # Resource edge label should be preserved (it's a direct edge)\n    resource_edge = next(e for e in filtered.edges if e.target == \"resource1\")\n    assert resource_edge.label == \"db\"\n\n\ndef test_filter_by_node_type_no_matching_types() -> None:\n    \"\"\"Test filtering with types that don't exist in graph.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n        ],\n        edges=[WorkflowGraphEdge(source=\"step1\", target=\"step2\")],\n    )\n\n    filtered = graph.filter_by_node_type(\"nonexistent\")\n\n    # Graph should be unchanged\n    assert len(filtered.nodes) == 2\n    assert len(filtered.edges) == 1\n\n\ndef test_filter_by_node_type_preserves_description() -> None:\n    \"\"\"Test that the workflow description is preserved.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[WorkflowStepNode(id=\"step1\", label=\"Step 1\")],\n        edges=[],\n        description=\"My workflow description\",\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    assert filtered.description == \"My workflow description\"\n\n\ndef test_filter_by_node_type_deduplicates_edges() -> None:\n    \"\"\"Test that duplicate edges are not created.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowEventNode(\n                id=\"EventA\",\n                label=\"EventA\",\n                event_type=\"EventA\",\n                event_types=[\"EventA\"],\n            ),\n            WorkflowEventNode(\n                id=\"EventB\",\n                label=\"EventB\",\n                event_type=\"EventB\",\n                event_types=[\"EventB\"],\n            ),\n            WorkflowStepNode(id=\"step2\", label=\"Step 2\"),\n        ],\n        edges=[\n            # Both events lead to step2 from step1\n            WorkflowGraphEdge(source=\"step1\", target=\"EventA\"),\n            WorkflowGraphEdge(source=\"step1\", target=\"EventB\"),\n            WorkflowGraphEdge(source=\"EventA\", target=\"step2\"),\n            WorkflowGraphEdge(source=\"EventB\", target=\"step2\"),\n        ],\n    )\n\n    filtered = graph.filter_by_node_type(\"event\")\n\n    # Should only have one edge: step1 -> step2 (deduplicated)\n    assert len(filtered.edges) == 1\n    assert filtered.edges[0].source == \"step1\"\n    assert filtered.edges[0].target == \"step2\"\n\n\n# --- Resource config node tests ---\n\n\nclass ConfigData(BaseModel):\n    \"\"\"A config model for testing resource configs.\"\"\"\n\n    setting: str\n    value: int\n\n\ndef _write_config(tmp_path: Path, filename: str, data: Mapping[str, object]) -> str:\n    config_path = tmp_path / filename\n    with open(config_path, \"w\") as f:\n        json.dump(data, f)\n    return str(config_path)\n\n\ndef test_resource_config_nested_in_resource_factory(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Nested ResourceConfig should create resource + config nodes and an edge.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"setting\": \"test\", \"value\": 42}\n    config_path = _write_config(tmp_path, \"config.json\", config_data)\n\n    def get_configured_client(\n        my_config: Annotated[ConfigData, ResourceConfig(config_file=\"config.json\")],\n    ) -> DatabaseClient:\n        return DatabaseClient()\n\n    class WorkflowWithResourceConfig(Workflow):\n        @step\n        async def step_with_config(\n            self,\n            ev: StartEvent,\n            client: Annotated[DatabaseClient, Resource(get_configured_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithResourceConfig()\n    graph = get_workflow_representation(workflow=wf)\n\n    assert len(_resource_nodes(graph)) == 1\n    resource_config_nodes = _resource_config_nodes(graph)\n    assert len(resource_config_nodes) == 1\n\n    config_node = resource_config_nodes[0]\n    assert config_node.type_name == \"ConfigData\"\n    assert config_node.config_file == config_path\n    assert config_node.path_selector is None\n    assert config_node.config_schema is not None\n    assert {\"setting\", \"value\"} <= set(config_node.config_schema.get(\"properties\", {}))\n    assert config_node.config_value == config_data\n\n    edges = _find_edges(graph, target_prefix=\"resource_config_\", label=\"my_config\")\n    assert len(edges) == 1\n    assert edges[0].source.startswith(\"resource_\")\n\n\ndef test_recursive_resource_dependencies_with_config(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Nested resources should create resource->resource and resource->config edges.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"setting\": \"nested\", \"value\": 7}\n    config_path = _write_config(tmp_path, \"config.json\", config_data)\n\n    class DBConnection:\n        def __init__(self, config: ConfigData) -> None:\n            self.config = config\n\n    class Repository:\n        def __init__(self, db: DBConnection) -> None:\n            self.db = db\n\n    def get_db_connection(\n        config: Annotated[ConfigData, ResourceConfig(config_file=\"config.json\")],\n    ) -> DBConnection:\n        return DBConnection(config=config)\n\n    def get_repository(\n        db: Annotated[DBConnection, Resource(get_db_connection)],\n    ) -> Repository:\n        return Repository(db=db)\n\n    class WorkflowWithRecursiveResources(Workflow):\n        @step\n        async def start_step(\n            self,\n            ev: StartEvent,\n            repo: Annotated[Repository, Resource(get_repository)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithRecursiveResources()\n    graph = get_workflow_representation(workflow=wf)\n\n    assert len(_resource_nodes(graph)) == 2\n    resource_config_nodes = _resource_config_nodes(graph)\n    assert len(resource_config_nodes) == 1\n    assert resource_config_nodes[0].config_file == config_path\n    assert resource_config_nodes[0].config_value == config_data\n\n    def _resource_node_for_getter(suffix: str) -> WorkflowResourceNode:\n        return next(\n            node\n            for node in _resource_nodes(graph)\n            if node.getter_name is not None and node.getter_name.endswith(suffix)\n        )\n\n    repo_node = _resource_node_for_getter(\"get_repository\")\n    db_node = _resource_node_for_getter(\"get_db_connection\")\n\n    repo_edges = _find_edges(graph, source=repo_node.id, label=\"db\")\n    assert len(repo_edges) == 1\n    assert repo_edges[0].target == db_node.id\n\n    config_edges = _find_edges(graph, source=db_node.id, label=\"config\")\n    assert len(config_edges) == 1\n    assert config_edges[0].target.startswith(\"resource_config_\")\n\n\ndef test_resource_config_direct_in_step(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"ResourceConfig used directly in a step should only create a config node.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"setting\": \"direct\", \"value\": 99}\n    config_path = _write_config(tmp_path, \"direct_config.json\", config_data)\n\n    class WorkflowWithDirectConfig(Workflow):\n        @step\n        async def step_with_direct_config(\n            self,\n            ev: StartEvent,\n            config: Annotated[\n                ConfigData, ResourceConfig(config_file=\"direct_config.json\")\n            ],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithDirectConfig()\n    graph = get_workflow_representation(workflow=wf)\n\n    assert len(_resource_nodes(graph)) == 0\n    resource_config_nodes = _resource_config_nodes(graph)\n    assert len(resource_config_nodes) == 1\n    config_node = resource_config_nodes[0]\n    assert config_node.type_name == \"ConfigData\"\n    assert config_node.config_file == config_path\n    assert config_node.config_value == config_data\n\n    edges = _find_edges(\n        graph,\n        source=\"step_with_direct_config\",\n        target_prefix=\"resource_config_\",\n        label=\"config\",\n    )\n    assert len(edges) == 1\n\n\ndef test_resource_config_with_path_selector(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"ResourceConfig path selector is preserved in graph nodes.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"database\": {\"setting\": \"test\", \"value\": 42}}\n    config_path = _write_config(tmp_path, \"config.json\", config_data)\n\n    def get_configured_client(\n        config: Annotated[\n            ConfigData,\n            ResourceConfig(config_file=\"config.json\", path_selector=\"database\"),\n        ],\n    ) -> DatabaseClient:\n        return DatabaseClient()\n\n    class WorkflowWithResourceConfig(Workflow):\n        @step\n        async def step_with_config(\n            self,\n            ev: StartEvent,\n            client: Annotated[DatabaseClient, Resource(get_configured_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithResourceConfig()\n    graph = get_workflow_representation(workflow=wf)\n\n    resource_config_nodes = _resource_config_nodes(graph)\n    assert len(resource_config_nodes) == 1\n    config_node = resource_config_nodes[0]\n    assert config_node.config_file == config_path\n    assert config_node.path_selector == \"database\"\n\n\ndef test_resource_config_nodes_are_deduplicated(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Same resource config used by multiple resources appears once.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    _write_config(tmp_path, \"config.json\", {\"setting\": \"test\", \"value\": 42})\n\n    def get_client_one(\n        config: Annotated[ConfigData, ResourceConfig(config_file=\"config.json\")],\n    ) -> DatabaseClient:\n        return DatabaseClient()\n\n    def get_client_two(\n        config: Annotated[ConfigData, ResourceConfig(config_file=\"config.json\")],\n    ) -> DatabaseClient:\n        return DatabaseClient()\n\n    class WorkflowWithSharedConfig(Workflow):\n        @step\n        async def step_one(\n            self,\n            ev: StartEvent,\n            client: Annotated[DatabaseClient, Resource(get_client_one)],\n        ) -> MiddleEvent:\n            return MiddleEvent()\n\n        @step\n        async def step_two(\n            self,\n            ev: MiddleEvent,\n            client: Annotated[DatabaseClient, Resource(get_client_two)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithSharedConfig()\n    graph = get_workflow_representation(workflow=wf)\n\n    assert len(_resource_config_nodes(graph)) == 1\n    assert len(_resource_nodes(graph)) == 2\n    assert len(_find_edges(graph, target_prefix=\"resource_config_\")) == 2\n\n\ndef test_multiple_different_resource_configs(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Multiple configs create distinct config nodes.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    db_path = _write_config(tmp_path, \"db_config.json\", {\"setting\": \"db\", \"value\": 1})\n    cache_path = _write_config(\n        tmp_path, \"cache_config.json\", {\"setting\": \"cache\", \"value\": 2}\n    )\n\n    def get_db_client(\n        config: Annotated[ConfigData, ResourceConfig(config_file=\"db_config.json\")],\n    ) -> DatabaseClient:\n        return DatabaseClient()\n\n    class CacheClient:\n        pass\n\n    def get_cache_client(\n        config: Annotated[ConfigData, ResourceConfig(config_file=\"cache_config.json\")],\n    ) -> CacheClient:\n        return CacheClient()\n\n    class WorkflowWithMultipleConfigs(Workflow):\n        @step\n        async def step_with_both(\n            self,\n            ev: StartEvent,\n            db: Annotated[DatabaseClient, Resource(get_db_client)],\n            cache: Annotated[CacheClient, Resource(get_cache_client)],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithMultipleConfigs()\n    graph = get_workflow_representation(workflow=wf)\n\n    config_files = {node.config_file for node in _resource_config_nodes(graph)}\n    assert config_files == {db_path, cache_path}\n\n\ndef test_filter_by_node_type_with_resource_config() -> None:\n    \"\"\"Test that filter_by_node_type works with resource_config nodes.\"\"\"\n    graph = WorkflowGraph(\n        name=\"TestWorkflow\",\n        nodes=[\n            WorkflowStepNode(id=\"step1\", label=\"Step 1\"),\n            WorkflowResourceNode(id=\"resource_123\", label=\"Resource\"),\n            WorkflowResourceConfigNode(\n                id=\"resource_config_456\",\n                label=\"Config\",\n                config_file=\"config.json\",\n            ),\n        ],\n        edges=[\n            WorkflowGraphEdge(source=\"step1\", target=\"resource_123\", label=\"client\"),\n            WorkflowGraphEdge(\n                source=\"resource_123\", target=\"resource_config_456\", label=\"config\"\n            ),\n        ],\n    )\n\n    # Filter out resource_config nodes\n    filtered = graph.filter_by_node_type(\"resource_config\")\n\n    assert len(filtered.nodes) == 2\n    node_types = {n.node_type for n in filtered.nodes}\n    assert node_types == {\"step\", \"resource\"}\n\n    # Edge from resource to config should be removed\n    # (no remaining node to connect to)\n    assert len(filtered.edges) == 1\n    assert filtered.edges[0].source == \"step1\"\n    assert filtered.edges[0].target == \"resource_123\"\n\n\ndef test_resource_config_label_and_description(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"ResourceConfig label and description are preserved in graph nodes.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"categories\": [\"invoice\", \"resume\", \"contract\"]}\n    _write_config(tmp_path, \"classify.json\", config_data)\n\n    class WorkflowWithLabeledConfig(Workflow):\n        @step\n        async def classify_step(\n            self,\n            ev: StartEvent,\n            config: Annotated[\n                ConfigData,\n                ResourceConfig(\n                    config_file=\"classify.json\",\n                    label=\"Document Classifier\",\n                    description=\"Configuration for document type classification\",\n                ),\n            ],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithLabeledConfig()\n    graph = get_workflow_representation(workflow=wf)\n\n    config_nodes = _resource_config_nodes(graph)\n    assert len(config_nodes) == 1\n    config_node = config_nodes[0]\n\n    # Label should be used instead of type name\n    assert config_node.label == \"Document Classifier\"\n    assert config_node.description == \"Configuration for document type classification\"\n    # Type name should still be preserved\n    assert config_node.type_name == \"ConfigData\"\n\n\ndef test_resource_config_label_fallback(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"ResourceConfig without label falls back to type name.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    config_data = {\"value\": 123}\n    _write_config(tmp_path, \"config.json\", config_data)\n\n    class WorkflowWithUnlabeledConfig(Workflow):\n        @step\n        async def step(\n            self,\n            ev: StartEvent,\n            config: Annotated[ConfigData, ResourceConfig(config_file=\"config.json\")],\n        ) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = WorkflowWithUnlabeledConfig()\n    graph = get_workflow_representation(workflow=wf)\n\n    config_nodes = _resource_config_nodes(graph)\n    assert len(config_nodes) == 1\n    config_node = config_nodes[0]\n\n    # Label should fall back to type name when not specified\n    assert config_node.label == \"ConfigData\"\n    assert config_node.description is None\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_resources.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport json\nimport re\nfrom pathlib import Path\nfrom typing import Annotated\nfrom unittest import mock\n\nimport pytest\nfrom pydantic import BaseModel, Field\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.resource import (\n    Resource,\n    ResourceConfig,\n    ResourceManager,\n    _Resource,\n    _ResourceConfig,\n)\nfrom workflows.workflow import Workflow\n\n# Global counters used in resource workflow tests\ncc: int\ncc1: int\ncc2: int\n\n\n# Test fixtures for annotation shadowing tests\nclass _FactoryConfig(BaseModel):\n    \"\"\"Config class defined in test_resources module scope.\"\"\"\n\n    name: str\n\n\ndef _get_factory_with_config_path(config_path: str) -> _Resource:\n    \"\"\"Returns a Resource that creates a factory using module-scoped _FactoryConfig.\"\"\"\n\n    def factory(\n        config: Annotated[_FactoryConfig, ResourceConfig(config_file=config_path)],\n    ) -> dict:\n        return {\"name\": config.name}\n\n    return Resource(factory)\n\n\nclass SecondEvent(Event):\n    msg: str = Field(description=\"A message\")\n\n\nclass ThirdEvent(Event):\n    msg: str = Field(description=\"A message\")\n\n\nclass ChatMessage(BaseModel):\n    @classmethod\n    def from_str(cls, role, content):  # noqa: ANN001\n        return mock.MagicMock(content=content)\n\n\nclass Memory(mock.MagicMock):\n    @classmethod\n    def from_defaults(cls, *args, **kwargs):  # noqa: ANN002, ANN003\n        return mock.MagicMock()\n\n\nclass MessageStopEvent(StopEvent):\n    llm_response: str | None = Field(default=None)\n\n\nclass FileData(BaseModel):\n    file: str\n    permission_mode: str\n\n\nclass FileOperator:\n    def __init__(self, data: FileData) -> None:\n        self.file = data.file\n        self.permission_mode = data.permission_mode\n\n    def operate(self) -> str:\n        if self.permission_mode == \"r\":\n            with open(self.file, self.permission_mode) as f:\n                return f.read()\n        elif self.permission_mode == \"w\":\n            with open(self.file, self.permission_mode) as f:\n                f.write(\"hello world!\")\n                return \"hello world!\"\n        else:\n            raise ValueError(f\"Unsupported operation: {self.permission_mode}\")\n\n\nclass ChatMessages(BaseModel):\n    messages: list[str]\n\n\nclass Fs(BaseModel):\n    files: list[str]\n    dirs: list[str]\n\n\n@pytest.mark.asyncio\nasync def test_function_resource_init() -> None:\n    def get_string() -> str:\n        return \"string\"\n\n    retval = Resource(get_string)\n    assert isinstance(retval, _Resource)\n    assert \"get_string\" in retval.name\n    assert retval.cache\n    assert not retval._is_async\n\n    resource_manager = ResourceManager()\n    result = await retval.call(resource_manager)\n    assert result == \"string\"\n\n\ndef test_resource_config_init(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n\n    data = {\"messages\": [\"hello\"]}\n\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    retval = ResourceConfig(config_file=\"config.json\")\n    assert isinstance(retval, _ResourceConfig)\n    assert retval.path_selector is None\n    expected_path = str(tmp_path / \"config.json\")\n    assert retval.config_file == expected_path\n    assert retval.cls_factory is None\n    assert retval.name == \"config.json\"\n\n    # modify path selector, modify name\n    retval.path_selector = \"hello.world\"\n    assert retval.name == \"config.json.hello.world\"\n\n    retval.path_selector = None\n\n    with pytest.raises(\n        ValueError,\n        match=\"Class factory should be set to a BaseModel subclass before calling\",\n    ):\n        retval.call()\n\n    # define a cls_factory for the resource to be called\n    retval.cls_factory = ChatMessages\n\n    result = retval.call()\n    assert isinstance(result, ChatMessages)\n    assert result.messages == [\"hello\"]\n\n\ndef test_resource_config_path_selector(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    data = {\n        \"memory\": {\"messages\": [\"hello\"]},\n        \"core\": {\"fs\": {\"files\": [\"hello.py\"], \"dirs\": [\"hello/\"]}},\n    }\n\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    resource = ResourceConfig(config_file=\"config.json\", path_selector=\"memory\")\n    assert resource.name == \"config.json.memory\"\n    resource.cls_factory = ChatMessages\n    value = resource.call()\n    assert isinstance(value, ChatMessages)\n    assert value.messages == [\"hello\"]\n    resource.path_selector = \"core.fs\"\n    assert resource.name == \"config.json.core.fs\"\n    resource.cls_factory = Fs\n    value = resource.call()\n    assert isinstance(value, Fs)\n    assert value.files == [\"hello.py\"]\n    assert value.dirs == [\"hello/\"]\n\n\ndef test_resource_config_path_selector_error(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    data = {\n        \"core\": {\"fs\": {\"files\": [\"hello.py\"], \"dirs\": [\"hello/\"]}},\n    }\n\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    # path selector does not return a dict\n    resource = ResourceConfig(config_file=\"config.json\", path_selector=\"core.fs.files\")\n    resource.cls_factory = Fs\n    with pytest.raises(\n        ValueError,\n        match=r\"Expected dictionary for configuration from .+config\\.json at path core\\.fs\\.files, got: .*\",\n    ):\n        resource.call()\n\n    # path selector does not exist\n    resource.path_selector = \"core.filesystem\"\n    with pytest.raises(\n        ValueError,\n        match=r\"Expected dictionary for configuration from .+config\\.json at path core\\.filesystem, got: .*\",\n    ):\n        resource.call()\n\n    # error occurs before reaching the end of the path_selector\n    # (tests the not the full path_selector is shown, but only up to the item with the error)\n    resource.path_selector = \"core.filesystem.fs\"\n    with pytest.raises(\n        ValueError,\n        match=r\"Expected dictionary for configuration from .+config\\.json at path core\\.filesystem, got: .*\",\n    ):\n        resource.call()\n\n\n@pytest.mark.asyncio\nasync def test_resource() -> None:\n    m = Memory.from_defaults(\"user_id_123\", token_limit=60000)\n\n    def get_memory(*args, **kwargs) -> Memory:  # noqa: ANN002, ANN003\n        return m\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(self, ev: StartEvent) -> SecondEvent:\n            print(\"Start step is done\", flush=True)\n            return SecondEvent(msg=\"Hello\")\n\n        @step\n        def f1(\n            self, ev: SecondEvent, memory: Annotated[Memory, Resource(get_memory)]\n        ) -> StopEvent:\n            memory.put(ChatMessage.from_str(role=\"user\", content=ev.msg))\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n    m.put.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_resource_async() -> None:\n    m = Memory.from_defaults(\"user_id_123\", token_limit=60000)\n\n    async def get_memory(*args, **kwargs) -> Memory:  # noqa: ANN002, ANN003\n        return m\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(self, ev: StartEvent) -> SecondEvent:\n            print(\"Start step is done\", flush=True)\n            return SecondEvent(msg=\"Hello\")\n\n        @step\n        def f1(\n            self,\n            ev: SecondEvent,\n            history: Annotated[Memory, Resource(get_memory)],\n        ) -> StopEvent:\n            history.put(ChatMessage.from_str(role=\"user\", content=ev.msg))\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n    m.put.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_resource_config(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n\n    data = {\"file\": \"hello.py\", \"permission_mode\": \"r\"}\n\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    with open(\"hello.py\", \"w\") as f:\n        f.write(\"print('hello')\")\n\n    def get_file_operator(\n        config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n    ) -> FileOperator:\n        return FileOperator(data=config)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(self, ev: StartEvent) -> SecondEvent:\n            print(\"Start step is done\", flush=True)\n            return SecondEvent(msg=\"Hello\")\n\n        @step\n        def f1(\n            self,\n            ev: SecondEvent,\n            file_operator: Annotated[FileOperator, Resource(get_file_operator)],\n        ) -> StopEvent:\n            assert file_operator.file == \"hello.py\"\n            assert file_operator.permission_mode == \"r\"\n            assert file_operator.operate() == \"print('hello')\"\n            return StopEvent(result=None)\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n\n\n@pytest.mark.asyncio\nasync def test_caching_behavior() -> None:\n    class CounterThing:\n        counter = 0\n\n        def incr(self) -> None:\n            self.counter += 1\n\n    class StepEvent(Event):\n        pass\n\n    def provide_counter_thing() -> CounterThing:\n        return CounterThing()\n\n    class TestWorkflow(Workflow):\n        @step\n        async def test_step(\n            self,\n            ev: StartEvent,\n            counter_thing: Annotated[CounterThing, Resource(provide_counter_thing)],\n        ) -> StepEvent:\n            counter_thing.incr()\n            return StepEvent()\n\n        @step\n        async def test_step_2(\n            self,\n            ev: StepEvent,\n            counter_thing: Annotated[CounterThing, Resource(provide_counter_thing)],\n        ) -> StopEvent:\n            global cc\n            counter_thing.incr()\n            cc = counter_thing.counter\n            return StopEvent()\n\n    wf_1 = TestWorkflow(disable_validation=True)\n    await wf_1.run()\n    assert (\n        cc == 2\n    )  # this is expected to be 2, as it is a cached resource shared by test_step and test_step_2, which means at test_step counter_thing.counter goes from 0 to 1 and at test_step_2 goes from 1 to 2\n\n    wf_2 = TestWorkflow(disable_validation=True)\n    await wf_2.run()\n    assert (\n        cc == 2\n    )  # the cache is workflow-specific, so since wf_2 is different from wf_1, we expect no interference between the two\n\n\n@pytest.mark.asyncio\nasync def test_caching_behavior_resource_configs(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n\n    data = {\"file\": \"hello.py\", \"permission_mode\": \"r\"}\n    data_1 = {\"file\": \"bye.py\", \"permission_mode\": \"w\"}\n\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    def get_file_operator(\n        config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n    ) -> FileOperator:\n        return FileOperator(data=config)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            file: Annotated[FileOperator, Resource(get_file_operator)],\n        ) -> SecondEvent:\n            print(\"first step\")\n            assert file.file == \"hello.py\"\n            assert file.permission_mode == \"r\"\n            # change config.json: the underlying resource has been cached, so will be unaffected\n            with open(\"config.json\", \"w\") as f:\n                json.dump(data_1, f)\n            return SecondEvent(msg=\"Hello\")\n\n        @step\n        def f1(\n            self,\n            ev: SecondEvent,\n            file: Annotated[FileOperator, Resource(get_file_operator)],\n        ) -> StopEvent:\n            print(\"second step\")\n            # this resource has been cached,\n            # so even if config.json has changed,\n            # the resource remains the same\n            assert file.file == \"hello.py\"\n            assert file.permission_mode == \"r\"\n            return StopEvent()\n\n    wf = TestWorkflow()\n    await wf.run()\n\n\n@pytest.mark.asyncio\nasync def test_non_caching_behavior() -> None:\n    class CounterThing:\n        counter = 0\n\n        def incr(self) -> None:\n            self.counter += 1\n\n    class StepEvent(Event):\n        pass\n\n    def provide_counter_thing() -> CounterThing:\n        return CounterThing()\n\n    class TestWorkflow(Workflow):\n        @step\n        async def test_step(\n            self,\n            ev: StartEvent,\n            counter_thing: Annotated[CounterThing, Resource(provide_counter_thing)],\n        ) -> StepEvent:\n            global cc1\n            counter_thing.incr()\n            cc1 = counter_thing.counter\n            return StepEvent()\n\n        @step\n        async def test_step_2(\n            self,\n            ev: StepEvent,\n            counter_thing: Annotated[\n                CounterThing, Resource(provide_counter_thing, cache=False)\n            ],\n        ) -> StopEvent:\n            global cc2\n            counter_thing.incr()\n            cc2 = counter_thing.counter\n            return StopEvent()\n\n    wf_1 = TestWorkflow(disable_validation=True)\n    await wf_1.run()\n    assert cc1 == 1\n    assert cc2 == 1\n\n\n@pytest.mark.asyncio\nasync def test_resource_manager() -> None:\n    m = ResourceManager()\n    await m.set(\"test_resource\", 42)\n    assert m.get_all() == {\"test_resource\": 42}\n\n\n@pytest.mark.asyncio\nasync def test_recursive_resource_injection() -> None:\n    \"\"\"Test that a Resource can depend on another Resource.\"\"\"\n\n    class DBConnection:\n        def __init__(self, host: str):\n            self.host = host\n\n    class Repository:\n        def __init__(self, db: DBConnection):\n            self.db = db\n\n    def get_db_connection() -> DBConnection:\n        return DBConnection(host=\"localhost\")\n\n    def get_repository(\n        db: Annotated[DBConnection, Resource(get_db_connection)],\n    ) -> Repository:\n        return Repository(db)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(self, ev: StartEvent) -> SecondEvent:\n            return SecondEvent(msg=\"Hello\")\n\n        @step\n        def use_repo(\n            self,\n            ev: SecondEvent,\n            repo: Annotated[Repository, Resource(get_repository)],\n        ) -> StopEvent:\n            assert repo.db.host == \"localhost\"\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n\n\n@pytest.mark.asyncio\nasync def test_recursive_resource_caching() -> None:\n    \"\"\"Test that nested resources respect individual cache settings.\"\"\"\n    call_counts = {\"db\": 0, \"repo\": 0}\n\n    class DBConnection:\n        pass\n\n    class Repository:\n        def __init__(self, db: DBConnection):\n            self.db = db\n\n    def get_db_connection() -> DBConnection:\n        call_counts[\"db\"] += 1\n        return DBConnection()\n\n    def get_repository(\n        db: Annotated[DBConnection, Resource(get_db_connection)],\n    ) -> Repository:\n        call_counts[\"repo\"] += 1\n        return Repository(db)\n\n    class StepEvent(Event):\n        pass\n\n    class TestWorkflow(Workflow):\n        @step\n        def step1(\n            self,\n            ev: StartEvent,\n            repo: Annotated[Repository, Resource(get_repository)],\n        ) -> StepEvent:\n            return StepEvent()\n\n        @step\n        def step2(\n            self,\n            ev: StepEvent,\n            repo: Annotated[Repository, Resource(get_repository)],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n\n    # Both should be cached (called once each)\n    assert call_counts[\"db\"] == 1\n    assert call_counts[\"repo\"] == 1\n\n\n@pytest.mark.asyncio\nasync def test_circular_resource_dependency_detection() -> None:\n    \"\"\"Test that circular dependencies are detected at runtime.\"\"\"\n\n    class A:\n        pass\n\n    class B:\n        pass\n\n    # Create the cycle by modifying __annotations__ after creating the resources\n    # This allows us to create mutual dependencies\n\n    def cyclic_factory_a(b: Annotated[B, \"placeholder\"]) -> A:\n        return A()\n\n    def cyclic_factory_b(a: Annotated[A, \"placeholder\"]) -> B:\n        return B()\n\n    # Create resources\n    cyclic_res_a = Resource(cyclic_factory_a)\n    cyclic_res_b = Resource(cyclic_factory_b)\n\n    # Modify annotations to create the cycle:\n    # cyclic_res_a depends on cyclic_res_b, and cyclic_res_b depends on cyclic_res_a\n    cyclic_factory_a.__annotations__[\"b\"] = Annotated[B, cyclic_res_b]\n    cyclic_factory_b.__annotations__[\"a\"] = Annotated[A, cyclic_res_a]\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            a: Annotated[A, cyclic_res_a],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    expected_chain = (\n        f\"{cyclic_factory_a.__qualname__} -> \"\n        f\"{cyclic_factory_b.__qualname__} -> \"\n        f\"{cyclic_factory_a.__qualname__}\"\n    )\n    with pytest.raises(ValueError, match=re.escape(expected_chain)):\n        await wf.run()\n\n\n@pytest.mark.asyncio\nasync def test_non_cached_resource_single_resolution_cycle() -> None:\n    \"\"\"Non-cached resources should resolve once per dependency graph.\"\"\"\n    call_counts = {\"d\": 0}\n\n    class D:\n        pass\n\n    def get_d() -> D:\n        call_counts[\"d\"] += 1\n        return D()\n\n    def get_b(d: Annotated[D, Resource(get_d, cache=False)]) -> str:\n        return \"b\"\n\n    def get_c(d: Annotated[D, Resource(get_d, cache=False)]) -> str:\n        return \"c\"\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            b: Annotated[str, Resource(get_b)],\n            c: Annotated[str, Resource(get_c)],\n        ) -> StopEvent:\n            assert b == \"b\"\n            assert c == \"c\"\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n    assert call_counts[\"d\"] == 1\n\n\n@pytest.mark.asyncio\nasync def test_resource_config_in_step_signature(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that ResourceConfig can be used directly in step signatures.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    data = {\"file\": \"test.txt\", \"permission_mode\": \"r\"}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(self, ev: StartEvent) -> SecondEvent:\n            return SecondEvent(msg=\"Hello\")\n\n        @step\n        def use_config(\n            self,\n            ev: SecondEvent,\n            config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n        ) -> StopEvent:\n            assert config.file == \"test.txt\"\n            assert config.permission_mode == \"r\"\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n\n\n@pytest.mark.asyncio\nasync def test_resource_config_in_step_with_path_selector(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test ResourceConfig with path_selector in step signatures.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    data = {\n        \"database\": {\"file\": \"db.sqlite\", \"permission_mode\": \"rw\"},\n        \"cache\": {\"file\": \"cache.json\", \"permission_mode\": \"r\"},\n    }\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    class TestWorkflow(Workflow):\n        @step\n        def use_db_config(\n            self,\n            ev: StartEvent,\n            db_config: Annotated[\n                FileData,\n                ResourceConfig(config_file=\"config.json\", path_selector=\"database\"),\n            ],\n        ) -> StopEvent:\n            assert db_config.file == \"db.sqlite\"\n            assert db_config.permission_mode == \"rw\"\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    await wf.run()\n\n\n# Tests for resource validation during workflow.validate()\n\n\ndef test_validate_detects_circular_resource_dependency() -> None:\n    \"\"\"Test that validate() detects circular resource dependencies.\"\"\"\n\n    class A:\n        pass\n\n    class B:\n        pass\n\n    def cyclic_factory_a(b: Annotated[B, \"placeholder\"]) -> A:\n        return A()\n\n    def cyclic_factory_b(a: Annotated[A, \"placeholder\"]) -> B:\n        return B()\n\n    cyclic_res_a = Resource(cyclic_factory_a)\n    cyclic_res_b = Resource(cyclic_factory_b)\n\n    # Create circular dependency\n    cyclic_factory_a.__annotations__[\"b\"] = Annotated[B, cyclic_res_b]\n    cyclic_factory_b.__annotations__[\"a\"] = Annotated[A, cyclic_res_a]\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            a: Annotated[A, cyclic_res_a],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    # Circular deps are caught at resolution time by ResourceManager\n    with pytest.raises(Exception, match=r\"Circular resource dependency detected\"):\n        wf.validate(validate_resources=True)\n\n\ndef test_validate_resource_config_success(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that validate() succeeds with valid resource config.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    data = {\"file\": \"test.txt\", \"permission_mode\": \"r\"}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    # Should not raise\n    wf.validate(validate_resource_configs=True, validate_resources=False)\n\n\ndef test_validate_resource_config_invalid_data(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that validate() fails with invalid resource config data.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    # Missing required 'permission_mode' field\n    data = {\"file\": \"test.txt\"}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    with pytest.raises(\n        Exception,\n        match=r\"(?s)step 'start_step', parameter 'config'.*permission_mode\",\n    ):\n        wf.validate(validate_resource_configs=True, validate_resources=False)\n\n\ndef test_validate_resource_config_disabled(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that validate() skips resource config validation when disabled.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    # Invalid data but should be ignored since validation is disabled\n    data = {\"file\": \"test.txt\"}  # Missing permission_mode\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    # Should not raise because resource config validation is disabled\n    wf.validate(validate_resource_configs=False, validate_resources=False)\n\n\ndef test_validate_nested_resource_config(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that validate() validates nested resource configs.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    data = {\"file\": \"test.txt\", \"permission_mode\": \"r\"}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    def get_file_operator(\n        config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n    ) -> FileOperator:\n        return FileOperator(data=config)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            operator: Annotated[FileOperator, Resource(get_file_operator)],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    # Should validate the nested ResourceConfig\n    wf.validate(validate_resource_configs=True, validate_resources=False)\n\n\ndef test_validate_nested_resource_config_invalid(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that validate() fails with invalid nested resource config.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    # Invalid data - missing required field\n    data = {\"file\": \"test.txt\"}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    def get_file_operator(\n        config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n    ) -> FileOperator:\n        return FileOperator(data=config)\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            operator: Annotated[FileOperator, Resource(get_file_operator)],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    with pytest.raises(\n        Exception,\n        match=r\"(?s)step 'start_step', parameter 'operator'.*get_file_operator.*config\\.json.*permission_mode\",\n    ):\n        wf.validate(validate_resource_configs=True, validate_resources=False)\n\n\ndef test_validate_resources_enabled() -> None:\n    \"\"\"Test that validate() resolves resources when enabled.\"\"\"\n    call_count = {\"count\": 0}\n\n    def get_string() -> str:\n        call_count[\"count\"] += 1\n        return \"test\"\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            value: Annotated[str, Resource(get_string)],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    # Resource factory should be called during validation\n    wf.validate(validate_resource_configs=True, validate_resources=True)\n    assert call_count[\"count\"] == 1\n\n\ndef test_validate_resources_disabled_by_default() -> None:\n    \"\"\"Test that resource factories are not resolved by default.\"\"\"\n    call_count = {\"count\": 0}\n\n    def get_string() -> str:\n        call_count[\"count\"] += 1\n        return \"test\"\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            value: Annotated[str, Resource(get_string)],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    # Resource factory should NOT be called during validation (default)\n    wf.validate()  # Uses defaults: validate_resource_configs=True, validate_resources=False\n    assert call_count[\"count\"] == 0\n\n\ndef test_validate_resource_factory_failure() -> None:\n    \"\"\"Test that validate() reports resource factory failures.\"\"\"\n\n    def failing_factory() -> str:\n        raise RuntimeError(\"Factory failed!\")\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            value: Annotated[str, Resource(failing_factory)],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    with pytest.raises(\n        Exception, match=r\"step 'start_step', parameter 'value'.*Factory failed\"\n    ):\n        wf.validate(validate_resources=True)\n\n\ndef test_validate_annotation_shadowing_with_resource_factory(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that validation works with factory annotation shadowing.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    data = {\"name\": \"test_name\"}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    # Use the module-scoped helper that tests annotation shadowing\n    factory_resource = _get_factory_with_config_path(str(tmp_path / \"config.json\"))\n\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            result: Annotated[dict, factory_resource],\n        ) -> StopEvent:\n            return StopEvent()\n\n    wf = TestWorkflow(disable_validation=True)\n    # Should validate the nested ResourceConfig in the factory\n    wf.validate(validate_resource_configs=True)\n\n\ndef test_resource_config_deferred_file_check(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that ResourceConfig can be declared before the config file exists.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    # Declare ResourceConfig BEFORE the file exists - this should NOT raise\n    resource = ResourceConfig(config_file=\"config.json\")\n    assert resource.name == \"config.json\"\n\n    # Accessing config_file property should raise because file doesn't exist yet\n    with pytest.raises(FileNotFoundError, match=\"No such file: config.json\"):\n        _ = resource.config_file\n\n    # calling should also raise\n    resource.cls_factory = ChatMessages\n    with pytest.raises(FileNotFoundError, match=\"No such file: config.json\"):\n        resource.call()\n\n    # Now create the file\n    data = {\"messages\": [\"hello\"]}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    # Now it should work\n    result = resource.call()\n    assert isinstance(result, ChatMessages)\n    assert result.messages == [\"hello\"]\n\n\n@pytest.mark.asyncio\nasync def test_resource_config_deferred_in_workflow(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that workflow can be defined before config file exists.\"\"\"\n    monkeypatch.chdir(tmp_path)\n\n    # Define workflow with ResourceConfig pointing to non-existent file\n    # This should NOT raise at definition time\n    class TestWorkflow(Workflow):\n        @step\n        def start_step(\n            self,\n            ev: StartEvent,\n            config: Annotated[FileData, ResourceConfig(config_file=\"config.json\")],\n        ) -> StopEvent:\n            assert config.file == \"test.txt\"\n            return StopEvent()\n\n    # Workflow can be instantiated even though file doesn't exist\n    wf = TestWorkflow(disable_validation=True)\n\n    # Running should fail because file doesn't exist\n    with pytest.raises(FileNotFoundError, match=\"No such file: config.json\"):\n        await wf.run()\n\n    # Now create the file\n    data = {\"file\": \"test.txt\", \"permission_mode\": \"r\"}\n    with open(\"config.json\", \"w\") as f:\n        json.dump(data, f)\n\n    # Now it should work\n    await wf.run()\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_retry_policy.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport hashlib\nimport re\nfrom typing import Any, cast\n\nimport pytest\nfrom workflows import retry_policy as retry_policy_module\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.retry_policy import (\n    ConstantDelayRetryPolicy,\n    ExponentialBackoffRetryPolicy,\n    retry_all,\n    retry_always,\n    retry_any,\n    retry_if_exception,\n    retry_if_exception_cause_type,\n    retry_if_exception_message,\n    retry_if_exception_type,\n    retry_if_not_exception_message,\n    retry_if_not_exception_type,\n    retry_never,\n    retry_policy,\n    retry_unless_exception_type,\n    stop_after_attempt,\n    stop_after_delay,\n    stop_all,\n    stop_any,\n    stop_before_delay,\n    stop_never,\n    wait_chain,\n    wait_combine,\n    wait_exponential,\n    wait_exponential_jitter,\n    wait_fixed,\n    wait_full_jitter,\n    wait_incrementing,\n    wait_none,\n    wait_random,\n    wait_random_exponential,\n)\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.workflow import Workflow\n\n# ---------------------------------------------------------------------------\n# Retry conditions\n# ---------------------------------------------------------------------------\n\n\ndef test_retry_if_exception_type_matches() -> None:\n    cond = retry_if_exception_type(exception_types=(ValueError, TypeError))\n    assert cond(ValueError(\"bad\")) is True\n    assert cond(TypeError(\"bad\")) is True\n    assert cond(RuntimeError(\"bad\")) is False\n\n\ndef test_retry_if_exception_type_subclass() -> None:\n    cond = retry_if_exception_type(exception_types=OSError)\n    assert cond(ConnectionError(\"refused\")) is True\n\n\ndef test_retry_if_not_exception_type() -> None:\n    cond = retry_if_not_exception_type(exception_types=(ValueError, KeyError))\n    assert cond(ValueError(\"bad\")) is False\n    assert cond(KeyError(\"bad\")) is False\n    assert cond(RuntimeError(\"bad\")) is True\n\n\ndef test_retry_if_exception_uses_predicate() -> None:\n    cond = retry_if_exception(lambda error: \"retryable\" in str(error))\n    assert cond(RuntimeError(\"retryable failure\")) is True\n    assert cond(RuntimeError(\"permanent failure\")) is False\n\n\ndef test_retry_if_exception_message_exact_match() -> None:\n    cond = retry_if_exception_message(message=\"rate limit exceeded\")\n    assert cond(Exception(\"rate limit exceeded\")) is True\n    assert cond(Exception(\"rate limit exceeded immediately\")) is False\n    assert cond(Exception(\"invalid input\")) is False\n\n\ndef test_retry_if_exception_message_regex() -> None:\n    cond = retry_if_exception_message(match=r\"code=\\d{3}\")\n    assert cond(Exception(\"code=429\")) is True\n    assert cond(Exception(\"code=abc\")) is False\n\n\ndef test_retry_if_exception_message_accepts_compiled_pattern() -> None:\n    cond = retry_if_exception_message(match=re.compile(r\"retry-\\d+\"))\n    assert cond(Exception(\"retry-42\")) is True\n    assert cond(Exception(\"retry-later\")) is False\n\n\ndef test_retry_if_exception_message_requires_exactly_one_matcher() -> None:\n    with pytest.raises(TypeError, match=\"message' or 'match\"):\n        retry_if_exception_message()\n\n    with pytest.raises(TypeError, match=\"either 'message' or 'match'\"):\n        retry_if_exception_message(message=\"rate limited\", match=\"rate\")\n\n\ndef test_retry_if_not_exception_message() -> None:\n    cond = retry_if_not_exception_message(message=\"do not retry\")\n    assert cond(Exception(\"transient\")) is True\n    assert cond(Exception(\"do not retry\")) is False\n\n\ndef test_retry_if_exception_cause_type() -> None:\n    root = ValueError(\"root\")\n    middle = RuntimeError(\"middle\")\n    middle.__cause__ = root\n    top = Exception(\"top\")\n    top.__cause__ = middle\n    cond = retry_if_exception_cause_type(exception_types=ValueError)\n    assert cond(top) is True\n    assert cond(RuntimeError(\"no cause\")) is False\n\n\ndef test_retry_unless_exception_type() -> None:\n    cond = retry_unless_exception_type(exception_types=(ValueError, KeyError))\n    assert cond(RuntimeError(\"retry\")) is True\n    assert cond(ValueError(\"stop\")) is False\n\n\ndef test_retry_named_combinators() -> None:\n    combined_any = retry_any(\n        retry_if_exception_type(ValueError),\n        retry_if_exception_message(message=\"retry me\"),\n    )\n    combined_all = retry_all(\n        retry_if_exception_type(ValueError),\n        retry_if_exception_message(match=\"retry\"),\n    )\n\n    assert combined_any(ValueError(\"other\")) is True\n    assert combined_any(RuntimeError(\"retry me\")) is True\n    assert combined_any(RuntimeError(\"stop\")) is False\n    assert combined_all(ValueError(\"retry now\")) is True\n    assert combined_all(ValueError(\"stop\")) is False\n\n\ndef test_retry_operator_sugar_matches_named_combinators() -> None:\n    by_type = retry_if_exception_type(ValueError)\n    by_message = retry_if_exception_message(message=\"retry me\")\n\n    via_operator = by_type | by_message\n    via_named = retry_any(by_type, by_message)\n    assert via_operator(ValueError(\"other\")) == via_named(ValueError(\"other\"))\n    assert via_operator(RuntimeError(\"retry me\")) == via_named(RuntimeError(\"retry me\"))\n    assert via_operator(RuntimeError(\"stop\")) == via_named(RuntimeError(\"stop\"))\n\n    via_operator_all = by_type & retry_if_exception_message(match=\"retry\")\n    via_named_all = retry_all(by_type, retry_if_exception_message(match=\"retry\"))\n    assert via_operator_all(ValueError(\"retry\")) == via_named_all(ValueError(\"retry\"))\n    assert via_operator_all(ValueError(\"stop\")) == via_named_all(ValueError(\"stop\"))\n\n\ndef test_retry_operator_grouping_is_left_associative() -> None:\n    combined = (\n        retry_if_exception_type(ValueError)\n        | retry_if_exception_message(message=\"retry\")\n        | retry_if_exception_cause_type(TypeError)\n    )\n\n    caused = RuntimeError(\"outer\")\n    caused.__cause__ = TypeError(\"root\")\n\n    assert combined(ValueError(\"other\")) is True\n    assert combined(RuntimeError(\"retry\")) is True\n    assert combined(caused) is True\n    assert combined(RuntimeError(\"stop\")) is False\n\n\ndef test_retry_always_and_retry_never() -> None:\n    always = retry_always()\n    never = retry_never()\n    assert always(Exception(\"anything\")) is True\n    assert never(Exception(\"anything\")) is False\n    assert (always & retry_if_exception_type(Exception))(Exception(\"anything\")) is True\n    assert (never | retry_if_exception_type(ValueError))(ValueError(\"bad\")) is True\n\n\n# ---------------------------------------------------------------------------\n# Wait strategies\n# ---------------------------------------------------------------------------\n\n\ndef test_wait_fixed() -> None:\n    w = wait_fixed(wait=3.5)\n    assert w(0) == 3.5\n    assert w(5) == 3.5\n\n\ndef test_wait_none() -> None:\n    assert wait_none()(0) == 0\n\n\ndef test_wait_exponential() -> None:\n    w = wait_exponential(multiplier=1.0, exp_base=2.0, max=100.0, min=0.0)\n    assert w(0) == 1.0\n    assert w(1) == 2.0\n    assert w(2) == 4.0\n    assert w(3) == 8.0\n\n\ndef test_wait_exponential_floor() -> None:\n    w = wait_exponential(multiplier=0.5, exp_base=2.0, max=100.0, min=2.0)\n    assert w(0) == 2.0\n    assert w(1) == 2.0\n\n\ndef test_wait_exponential_cap() -> None:\n    w = wait_exponential(multiplier=1.0, exp_base=10.0, max=50.0, min=0.0)\n    assert w(2) == 50.0\n\n\ndef test_wait_incrementing() -> None:\n    w = wait_incrementing(start=1.0, increment=0.5, max=2.0)\n    assert w(0) == 1.0\n    assert w(1) == 1.5\n    assert w(2) == 2.0\n    assert w(10) == 2.0\n\n\ndef test_wait_incrementing_never_returns_negative_delay() -> None:\n    w = wait_incrementing(start=-5.0, increment=-1.0, max=10.0)\n    assert w(0) == 0.0\n    assert w(3) == 0.0\n\n\ndef test_wait_random_range() -> None:\n    w = wait_random(min=1.0, max=5.0)\n    for _ in range(20):\n        val = w(0)\n        assert 1.0 <= val <= 5.0\n\n\ndef test_wait_random_deterministic_with_seed() -> None:\n    w = wait_random(min=0.0, max=10.0)\n    assert w(0, seed=42) == w(0, seed=42)\n\n\ndef test_wait_exponential_jitter_base() -> None:\n    w = wait_exponential_jitter(initial=1.0, exp_base=2.0, max=100.0, jitter=0.0)\n    assert w(0) == 1.0\n    assert w(1) == 2.0\n    assert w(2) == 4.0\n\n\ndef test_wait_exponential_jitter_adds_jitter() -> None:\n    w = wait_exponential_jitter(initial=1.0, exp_base=2.0, max=100.0, jitter=1.0)\n    for attempt in range(5):\n        base = min(1.0 * 2.0**attempt, 100.0)\n        val = w(attempt, seed=attempt)\n        assert base <= val <= base + 1.0\n\n\ndef test_wait_exponential_jitter_deterministic() -> None:\n    w = wait_exponential_jitter(initial=1.0, exp_base=2.0, max=60.0, jitter=1.0)\n    assert w(3, seed=99) == w(3, seed=99)\n\n\ndef test_wait_exponential_jitter_respects_max_after_jitter() -> None:\n    w = wait_exponential_jitter(initial=10.0, exp_base=2.0, max=10.0, jitter=5.0)\n    for seed in range(10):\n        assert w(3, seed=seed) <= 10.0\n\n\ndef test_wait_random_exponential_range() -> None:\n    w = wait_random_exponential(multiplier=1.0, exp_base=2.0, max=100.0, min=0.0)\n    for attempt in range(6):\n        val = w(attempt, seed=attempt)\n        assert 0.0 <= val <= min(1.0 * 2.0**attempt, 100.0)\n\n\ndef test_wait_random_exponential_deterministic_with_seed() -> None:\n    w = wait_random_exponential(multiplier=0.5, exp_base=3.0, max=50.0, min=0.0)\n    assert w(4, seed=17) == w(4, seed=17)\n\n\ndef test_wait_random_exponential_respects_minimum() -> None:\n    w = wait_random_exponential(multiplier=0.5, exp_base=2.0, max=100.0, min=2.0)\n    assert w(0, seed=11) == 2.0\n    assert 2.0 <= w(4, seed=12) <= 8.0\n\n\ndef test_wait_chain_sequence() -> None:\n    w = wait_chain(wait_fixed(1), wait_fixed(2), wait_fixed(5))\n    assert w(0) == 1.0\n    assert w(1) == 2.0\n    assert w(2) == 5.0\n\n\ndef test_wait_chain_repeats_last() -> None:\n    w = wait_chain(wait_fixed(1), wait_fixed(10))\n    assert w(0) == 1.0\n    assert w(1) == 10.0\n    assert w(2) == 10.0\n    assert w(99) == 10.0\n\n\ndef test_wait_chain_requires_strategies() -> None:\n    with pytest.raises(ValueError, match=\"at least one\"):\n        wait_chain()\n\n\ndef test_wait_combine_adds_delays() -> None:\n    combined = wait_combine(\n        wait_fixed(1.5), wait_incrementing(start=1.0, increment=1.0)\n    )\n    assert combined(0) == 2.5\n    assert combined(2) == 4.5\n\n\ndef test_wait_operator_sugar_matches_wait_combine() -> None:\n    left = wait_fixed(1.0)\n    right = wait_random(min=0.0, max=1.0)\n    via_operator = left + right\n    via_named = wait_combine(left, right)\n    assert via_operator(3, seed=42) == via_named(3, seed=42)\n\n\ndef test_wait_sum_support() -> None:\n    combined = cast(Any, sum([wait_fixed(1.0), wait_fixed(2.0), wait_none()]))\n    assert combined(0) == 3.0\n\n\ndef test_wait_combine_preserves_seeded_determinism() -> None:\n    combined = wait_random(min=0.0, max=1.0) + wait_random_exponential(\n        multiplier=0.5,\n        exp_base=2.0,\n        max=10.0,\n        min=0.0,\n    )\n    assert combined(3, seed=123) == combined(3, seed=123)\n\n\ndef test_wait_full_jitter_alias() -> None:\n    alias = wait_full_jitter(multiplier=1.0, exp_base=2.0, max=20.0, min=0.0)\n    direct = wait_random_exponential(multiplier=1.0, exp_base=2.0, max=20.0, min=0.0)\n    assert alias(4, seed=99) == direct(4, seed=99)\n\n\n# ---------------------------------------------------------------------------\n# Stop conditions\n# ---------------------------------------------------------------------------\n\n\ndef test_stop_after_attempt() -> None:\n    s = stop_after_attempt(max_attempt_number=3)\n    assert s(2, 0.0) is False\n    assert s(3, 0.0) is True\n    assert s(4, 0.0) is True\n\n\ndef test_stop_after_delay() -> None:\n    s = stop_after_delay(10.0)\n    assert s(1, 9.9) is False\n    assert s(1, 10.0) is True\n    assert s(1, 15.0) is True\n\n\ndef test_stop_any_and_stop_all() -> None:\n    any_stop = stop_any(stop_after_attempt(3), stop_after_delay(10.0))\n    all_stop = stop_all(stop_after_attempt(3), stop_after_delay(10.0))\n\n    assert any_stop(3, 1.0) is True\n    assert any_stop(1, 10.0) is True\n    assert any_stop(1, 1.0) is False\n\n    assert all_stop(3, 10.0) is True\n    assert all_stop(3, 1.0) is False\n\n\ndef test_stop_operator_sugar_matches_named_combinators() -> None:\n    by_attempt = stop_after_attempt(3)\n    by_delay = stop_after_delay(10.0)\n    assert (by_attempt | by_delay)(3, 1.0) == stop_any(by_attempt, by_delay)(3, 1.0)\n    assert (by_attempt & by_delay)(3, 10.0) == stop_all(by_attempt, by_delay)(3, 10.0)\n\n\ndef test_stop_never() -> None:\n    assert stop_never()(999, 999.0) is False\n\n\ndef test_stop_before_delay_uses_upcoming_sleep_inside_retry_policy() -> None:\n    policy = retry_policy(\n        wait=wait_fixed(0.5),\n        stop=stop_before_delay(5.0),\n    )\n\n    assert policy.next(4.4, 1, Exception(\"retry\")) == 0.5\n    assert policy.next(4.5, 1, Exception(\"retry\")) is None\n\n\n# ---------------------------------------------------------------------------\n# RetryPolicy composition\n# ---------------------------------------------------------------------------\n\n\ndef test_retry_policy_defaults() -> None:\n    p = retry_policy()\n    err = Exception(\"fail\")\n    assert type(p).__name__ == \"_ComposableRetryPolicy\"\n    assert p.next(0.0, 0, err) == 5.0\n    assert p.next(0.0, 1, err) == 5.0\n    assert p.next(0.0, 2, err) == 5.0\n    assert p.next(0.0, 3, err) is None\n\n\ndef test_retry_policy_constructor() -> None:\n    p = retry_policy(\n        wait=wait_fixed(wait=1),\n        stop=stop_after_attempt(max_attempt_number=3),\n    )\n    err = Exception(\"fail\")\n    assert p.next(0.0, 0, err) == 1.0\n    assert p.next(0.0, 2, err) == 1.0\n    assert p.next(0.0, 3, err) is None\n\n\ndef test_retry_policy_with_retry_condition() -> None:\n    p = retry_policy(\n        retry=retry_if_exception_type(exception_types=ValueError),\n        stop=stop_after_attempt(max_attempt_number=5),\n    )\n    assert p.next(0.0, 0, ValueError(\"bad\")) == 5.0\n    assert p.next(0.0, 0, RuntimeError(\"bad\")) is None\n\n\ndef test_retry_policy_with_wait_strategy() -> None:\n    p = retry_policy(\n        wait=wait_exponential(multiplier=1.0, exp_base=2.0, max=100.0, min=0.0),\n        stop=stop_after_attempt(max_attempt_number=5),\n    )\n    assert p.next(0.0, 0, Exception()) == 1.0\n    assert p.next(0.0, 1, Exception()) == 2.0\n    assert p.next(0.0, 2, Exception()) == 4.0\n\n\ndef test_retry_policy_with_random_exponential_wait() -> None:\n    p = retry_policy(\n        wait=wait_random_exponential(multiplier=1.0, exp_base=2.0, max=100.0, min=0.0),\n        stop=stop_after_attempt(max_attempt_number=5),\n    )\n    a = p.next(0.0, 2, Exception(), seed=42)\n    b = p.next(0.0, 2, Exception(), seed=42)\n    assert a == b\n    assert a is not None\n    assert 0.0 <= a <= 4.0\n\n\ndef test_retry_policy_stop_after_delay() -> None:\n    p = retry_policy(\n        wait=wait_fixed(1),\n        stop=stop_after_delay(10),\n    )\n    assert p.next(9.0, 100, Exception()) == 1.0\n    assert p.next(10.0, 100, Exception()) is None\n\n\ndef test_retry_policy_stop_before_delay_stops_using_next_sleep() -> None:\n    p = retry_policy(\n        wait=wait_incrementing(start=1.0, increment=1.0, max=10.0),\n        stop=stop_before_delay(5.0),\n    )\n    assert p.next(2.0, 1, Exception(\"retry\")) == 2.0\n    assert p.next(2.0, 2, Exception(\"retry\")) is None\n\n\ndef test_retry_policy_seed_forwarded() -> None:\n    p = retry_policy(\n        wait=wait_random(min=0, max=10),\n        stop=stop_after_attempt(max_attempt_number=5),\n    )\n    a = p.next(0.0, 0, Exception(), seed=42)\n    b = p.next(0.0, 0, Exception(), seed=42)\n    assert a == b\n\n\ndef test_retry_policy_retry_none_retries_all() -> None:\n    p = retry_policy(retry=None, stop=stop_after_attempt(max_attempt_number=2))\n    assert p.next(0.0, 0, ValueError(\"a\")) == 5.0\n    assert p.next(0.0, 0, RuntimeError(\"b\")) == 5.0\n\n\ndef test_retry_policy_all_three_composed() -> None:\n    p = retry_policy(\n        retry=retry_if_exception_type(exception_types=ConnectionError),\n        wait=wait_exponential(multiplier=0.5, exp_base=3.0, max=50.0, min=0.0),\n        stop=stop_after_attempt(max_attempt_number=3),\n    )\n    err = ConnectionError(\"refused\")\n    assert p.next(0.0, 0, err) == 0.5\n    assert p.next(0.0, 1, err) == 1.5\n    assert p.next(0.0, 2, err) == 4.5\n    assert p.next(0.0, 3, err) is None\n    assert p.next(0.0, 0, ValueError(\"bad\")) is None\n\n\ndef test_retry_policy_with_operator_composition() -> None:\n    retry = retry_if_exception_type(ValueError) | retry_if_exception_message(\n        message=\"retry\"\n    )\n    wait = wait_fixed(1.0) + wait_none()\n    stop = stop_after_attempt(3) | stop_before_delay(10.0)\n    policy = retry_policy(retry=retry, wait=wait, stop=stop)\n\n    assert policy.next(0.0, 1, ValueError(\"other\")) == 1.0\n    assert policy.next(0.0, 1, RuntimeError(\"retry\")) == 1.0\n    assert policy.next(0.0, 1, RuntimeError(\"stop\")) is None\n\n\ndef test_composable_retry_policy_is_no_longer_public() -> None:\n    assert not hasattr(retry_policy_module, \"ComposableRetryPolicy\")\n\n\n# ---------------------------------------------------------------------------\n# Legacy policies — deprecation warnings\n# ---------------------------------------------------------------------------\n\n\ndef test_ConstantDelayRetryPolicy_emits_deprecation_warning() -> None:\n    with pytest.warns(\n        DeprecationWarning, match=\"ConstantDelayRetryPolicy is deprecated\"\n    ):\n        ConstantDelayRetryPolicy()\n\n\ndef test_ExponentialBackoffRetryPolicy_emits_deprecation_warning() -> None:\n    with pytest.warns(\n        DeprecationWarning, match=\"ExponentialBackoffRetryPolicy is deprecated\"\n    ):\n        ExponentialBackoffRetryPolicy()\n\n\n# ---------------------------------------------------------------------------\n# Legacy policies — factory function behavior\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_ConstantDelayRetryPolicy_next() -> None:\n    delay = 4.2\n    p = ConstantDelayRetryPolicy(maximum_attempts=5, delay=delay)\n    assert type(p).__name__ == \"_ComposableRetryPolicy\"\n    assert p.next(elapsed_time=0.0, attempts=4, error=Exception()) == delay\n    assert p.next(elapsed_time=0.0, attempts=5, error=Exception()) is None\n    assert p.next(elapsed_time=0.0, attempts=999, error=Exception()) is None\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_ExponentialBackoffRetryPolicy_next_basic() -> None:\n    p = ExponentialBackoffRetryPolicy(\n        initial_delay=1.0, multiplier=2.0, max_delay=100.0, jitter=False\n    )\n    assert type(p).__name__ == \"_ComposableRetryPolicy\"\n    err = Exception()\n    assert p.next(elapsed_time=0.0, attempts=0, error=err) == 1.0\n    assert p.next(elapsed_time=0.0, attempts=1, error=err) == 2.0\n    assert p.next(elapsed_time=0.0, attempts=2, error=err) == 4.0\n    assert p.next(elapsed_time=0.0, attempts=3, error=err) == 8.0\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_ExponentialBackoffRetryPolicy_max_delay_cap() -> None:\n    p = ExponentialBackoffRetryPolicy(\n        initial_delay=1.0, multiplier=10.0, max_delay=50.0, jitter=False\n    )\n    assert p.next(elapsed_time=0.0, attempts=2, error=Exception()) == 50.0\n    assert p.next(elapsed_time=0.0, attempts=5, error=Exception()) is None\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_ExponentialBackoffRetryPolicy_gives_up() -> None:\n    p = ExponentialBackoffRetryPolicy(maximum_attempts=3, jitter=False)\n    err = Exception()\n    assert p.next(elapsed_time=0.0, attempts=2, error=err) is not None\n    assert p.next(elapsed_time=0.0, attempts=3, error=err) is None\n    assert p.next(elapsed_time=0.0, attempts=999, error=err) is None\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_ExponentialBackoffRetryPolicy_jitter() -> None:\n    p = ExponentialBackoffRetryPolicy(\n        initial_delay=1.0, multiplier=2.0, max_delay=100.0, jitter=True\n    )\n    err = Exception()\n    for attempt in range(5):\n        computed = min(1.0 * 2.0**attempt, 100.0)\n        delay = p.next(elapsed_time=0.0, attempts=attempt, error=err, seed=attempt)\n        assert delay is not None\n        assert 0 <= delay <= computed\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_ExponentialBackoffRetryPolicy_jitter_deterministic() -> None:\n    \"\"\"Same seed must produce the same delay on every call (DBOS replay determinism).\"\"\"\n    p = ExponentialBackoffRetryPolicy(\n        initial_delay=1.0, multiplier=2.0, max_delay=100.0, jitter=True\n    )\n    err = Exception()\n    for attempt in range(5):\n        seed = (\n            int(\n                hashlib.sha256(f\"run-abc:my_step:{attempt + 1}\".encode()).hexdigest(),\n                16,\n            )\n            & 0xFFFF_FFFF\n        )\n        first = p.next(elapsed_time=0.0, attempts=attempt, error=err, seed=seed)\n        second = p.next(elapsed_time=0.0, attempts=attempt, error=err, seed=seed)\n        assert first == second\n\n\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_ExponentialBackoffRetryPolicy_no_jitter() -> None:\n    p = ExponentialBackoffRetryPolicy(\n        initial_delay=0.5, multiplier=3.0, max_delay=100.0, jitter=False\n    )\n    err = Exception()\n    assert p.next(elapsed_time=0.0, attempts=0, error=err) == 0.5\n    assert p.next(elapsed_time=0.0, attempts=1, error=err) == 1.5\n    assert p.next(elapsed_time=0.0, attempts=2, error=err) == 4.5\n    assert p.next(elapsed_time=0.0, attempts=3, error=err) == 13.5\n\n\n# ---------------------------------------------------------------------------\n# E2E — retry policies still work through the full workflow\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\nasync def test_retry_e2e() -> None:\n    class CountEvent(Event):\n        \"\"\"Empty event to signal a step to increment a counter in the Context.\"\"\"\n\n    class DummyWorkflow(Workflow):\n        @step(retry_policy=ConstantDelayRetryPolicy(delay=0.2, maximum_attempts=4))\n        async def flaky_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            count = await ctx.store.get(\"counter\", default=0)\n            ctx.send_event(CountEvent())\n            if count < 3:\n                raise ValueError(\"Something bad happened!\")\n            return StopEvent(result=\"All good!\")\n\n        @step\n        async def counter(self, ctx: Context, ev: CountEvent) -> None:\n            count = await ctx.store.get(\"counter\", default=0)\n            await ctx.store.set(\"counter\", count + 1)\n\n    res = await WorkflowTestRunner(DummyWorkflow(disable_validation=True)).run()\n    assert res.result == \"All good!\"\n\n\n@pytest.mark.asyncio\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\nasync def test_retry_e2e_exponential() -> None:\n    class CountEvent(Event):\n        \"\"\"Event to increment a counter in the Context.\"\"\"\n\n    class DummyWorkflow(Workflow):\n        @step(\n            retry_policy=ExponentialBackoffRetryPolicy(\n                initial_delay=0.05, multiplier=2.0, maximum_attempts=4, jitter=False\n            )\n        )\n        async def flaky_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            count = await ctx.store.get(\"counter\", default=0)\n            ctx.send_event(CountEvent())\n            if count < 3:\n                raise ValueError(\"Something bad happened!\")\n            return StopEvent(result=\"All good!\")\n\n        @step\n        async def counter(self, ctx: Context, ev: CountEvent) -> None:\n            count = await ctx.store.get(\"counter\", default=0)\n            await ctx.store.set(\"counter\", count + 1)\n\n    res = await WorkflowTestRunner(DummyWorkflow(disable_validation=True)).run()\n    assert res.result == \"All good!\"\n\n\n# ---------------------------------------------------------------------------\n# E2E — RetryPolicy through the full workflow\n# ---------------------------------------------------------------------------\n\n\n@pytest.mark.asyncio\nasync def test_retry_e2e_retry_policy() -> None:\n    class CountEvent(Event):\n        pass\n\n    class DummyWorkflow(Workflow):\n        @step(\n            retry_policy=retry_policy(\n                retry=retry_if_exception_type(exception_types=ValueError),\n                wait=wait_fixed(wait=0.1),\n                stop=stop_after_attempt(max_attempt_number=4),\n            )\n        )\n        async def flaky_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            count = await ctx.store.get(\"counter\", default=0)\n            ctx.send_event(CountEvent())\n            if count < 3:\n                raise ValueError(\"transient failure\")\n            return StopEvent(result=\"recovered\")\n\n        @step\n        async def counter(self, ctx: Context, ev: CountEvent) -> None:\n            count = await ctx.store.get(\"counter\", default=0)\n            await ctx.store.set(\"counter\", count + 1)\n\n    res = await WorkflowTestRunner(DummyWorkflow(disable_validation=True)).run()\n    assert res.result == \"recovered\"\n\n\n@pytest.mark.asyncio\nasync def test_retry_e2e_retry_policy_non_retryable() -> None:\n    class DummyWorkflow(Workflow):\n        @step(\n            retry_policy=retry_policy(\n                retry=retry_if_exception_type(exception_types=ConnectionError),\n                wait=wait_fixed(wait=0.1),\n                stop=stop_after_attempt(max_attempt_number=5),\n            )\n        )\n        async def always_fails(self, ev: StartEvent) -> StopEvent:\n            raise ValueError(\"permanent failure\")\n\n    with pytest.raises(ValueError, match=\"permanent failure\"):\n        await WorkflowTestRunner(DummyWorkflow(disable_validation=True)).run()\n\n\n@pytest.mark.asyncio\nasync def test_retry_e2e_composed_policy() -> None:\n    class DummyWorkflow(Workflow):\n        attempts = 0\n\n        @step(\n            retry_policy=retry_policy(\n                retry=retry_if_exception_type(ValueError)\n                | retry_if_exception_message(message=\"retry me\"),\n                wait=wait_fixed(0.01) + wait_none(),\n                stop=stop_after_attempt(4) | stop_never(),\n            )\n        )\n        async def flaky_step(self, ev: StartEvent) -> StopEvent:\n            self.attempts += 1\n            if self.attempts < 4:\n                raise ValueError(\"retry me\")\n            return StopEvent(result=\"recovered\")\n\n    res = await WorkflowTestRunner(DummyWorkflow(disable_validation=True)).run()\n    assert res.result == \"recovered\"\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_retry_tenacity_conformance.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport inspect\nimport types as builtin_types\nimport typing\nfrom collections.abc import Callable\nfrom typing import Protocol, Union, cast\n\nimport pytest\nfrom tenacity import retry_all as tenacity_retry_all\nfrom tenacity import retry_any as tenacity_retry_any\nfrom tenacity import retry_if_exception as tenacity_retry_if_exception\nfrom tenacity import (\n    retry_if_exception_cause_type as tenacity_retry_if_exception_cause_type,\n)\nfrom tenacity import (\n    retry_if_exception_message as tenacity_retry_if_exception_message,\n)\nfrom tenacity import retry_if_exception_type as tenacity_retry_if_exception_type\nfrom tenacity import (\n    retry_if_not_exception_message as tenacity_retry_if_not_exception_message,\n)\nfrom tenacity import (\n    retry_if_not_exception_type as tenacity_retry_if_not_exception_type,\n)\nfrom tenacity import (\n    retry_unless_exception_type as tenacity_retry_unless_exception_type,\n)\nfrom tenacity import stop_after_attempt as tenacity_stop_after_attempt\nfrom tenacity import stop_after_delay as tenacity_stop_after_delay\nfrom tenacity import stop_all as tenacity_stop_all\nfrom tenacity import stop_any as tenacity_stop_any\nfrom tenacity import stop_before_delay as tenacity_stop_before_delay\nfrom tenacity import wait_chain as tenacity_wait_chain\nfrom tenacity import wait_combine as tenacity_wait_combine\nfrom tenacity import wait_exponential as tenacity_wait_exponential\nfrom tenacity import (\n    wait_exponential_jitter as tenacity_wait_exponential_jitter,\n)\nfrom tenacity import wait_fixed as tenacity_wait_fixed\nfrom tenacity import wait_incrementing as tenacity_wait_incrementing\nfrom tenacity import wait_none as tenacity_wait_none\nfrom tenacity import wait_random as tenacity_wait_random\nfrom tenacity.wait import wait_random_exponential as tenacity_wait_random_exponential\nfrom workflows.retry_policy import (\n    retry_all,\n    retry_always,\n    retry_any,\n    retry_if_exception,\n    retry_if_exception_cause_type,\n    retry_if_exception_message,\n    retry_if_exception_type,\n    retry_if_not_exception_message,\n    retry_if_not_exception_type,\n    retry_never,\n    retry_unless_exception_type,\n    stop_after_attempt,\n    stop_after_delay,\n    stop_all,\n    stop_any,\n    stop_before_delay,\n    stop_never,\n    wait_chain,\n    wait_combine,\n    wait_exponential,\n    wait_exponential_jitter,\n    wait_fixed,\n    wait_full_jitter,\n    wait_incrementing,\n    wait_none,\n    wait_random,\n    wait_random_exponential,\n)\n\n\nclass NamedCallable(Protocol):\n    __name__: str\n\n    def __call__(self, *args: object, **kwargs: object) -> object: ...\n\n\nCONFORMANCE_CASES: list[tuple[NamedCallable, NamedCallable | None, bool]] = cast(\n    list[tuple[NamedCallable, NamedCallable | None, bool]],\n    [\n        (wait_fixed, tenacity_wait_fixed, True),\n        (wait_none, tenacity_wait_none, True),\n        (wait_exponential, tenacity_wait_exponential, True),\n        (wait_incrementing, tenacity_wait_incrementing, True),\n        (wait_random, tenacity_wait_random, True),\n        (wait_exponential_jitter, tenacity_wait_exponential_jitter, True),\n        (wait_random_exponential, tenacity_wait_random_exponential, True),\n        (wait_chain, tenacity_wait_chain, True),\n        (wait_combine, tenacity_wait_combine, True),\n        (stop_after_attempt, tenacity_stop_after_attempt, True),\n        (stop_after_delay, tenacity_stop_after_delay, True),\n        (stop_any, tenacity_stop_any, True),\n        (stop_all, tenacity_stop_all, True),\n        (stop_before_delay, tenacity_stop_before_delay, True),\n        (stop_never, None, True),\n        (retry_if_exception, tenacity_retry_if_exception, True),\n        (retry_if_exception_type, tenacity_retry_if_exception_type, True),\n        (retry_if_not_exception_type, tenacity_retry_if_not_exception_type, True),\n        (retry_if_exception_message, tenacity_retry_if_exception_message, True),\n        (retry_if_not_exception_message, tenacity_retry_if_not_exception_message, True),\n        (retry_if_exception_cause_type, tenacity_retry_if_exception_cause_type, True),\n        (retry_any, tenacity_retry_any, True),\n        (retry_all, tenacity_retry_all, True),\n        (retry_always, None, True),\n        (retry_never, None, True),\n        (retry_unless_exception_type, tenacity_retry_unless_exception_type, True),\n    ],\n)\n\n\ndef _parameter_types(\n    callable_obj: Callable[..., object],\n) -> dict[str, type | object]:\n    \"\"\"Return {name: resolved_annotation} for all non-self parameters.\"\"\"\n    names = {\n        name for name in inspect.signature(callable_obj).parameters if name != \"self\"\n    }\n    target = callable_obj.__init__ if isinstance(callable_obj, type) else callable_obj  # type: ignore[misc]\n    try:\n        hints = typing.get_type_hints(target)\n    except Exception:\n        hints = {}\n    hints.pop(\"return\", None)\n    return {name: hints.get(name, inspect.Parameter.empty) for name in names}\n\n\ndef _normalize(tp: object) -> object:\n    \"\"\"Normalize type representations so ``X | Y`` equals ``Union[X, Y]``\n    and ``type[X]`` equals ``typing.Type[X]``, etc.\"\"\"\n    # Callable parameter lists are represented as plain lists\n    if isinstance(tp, list):\n        return tuple(_normalize(a) for a in tp)\n    origin = typing.get_origin(tp)\n    if origin is Union or isinstance(tp, builtin_types.UnionType):\n        return frozenset(_normalize(a) for a in typing.get_args(tp))\n    if origin is not None:\n        return (origin, tuple(_normalize(a) for a in typing.get_args(tp)))\n    return tp\n\n\ndef _types_match(ours: object, theirs: object) -> bool:\n    \"\"\"Check our annotation matches tenacity's.\n\n    Normalizes union representations so ``X | Y`` equals ``Union[X, Y]``.\n    Accepts Protocol-vs-concrete-base-class mismatches for compositor *args\n    (we use Protocols, tenacity uses inheritance).\n    \"\"\"\n    if ours == theirs:\n        return True\n    if _normalize(ours) == _normalize(theirs):\n        return True\n    # Our Protocol types vs tenacity's base classes are an intentional\n    # design choice (structural typing vs inheritance), not type drift.\n    return (\n        isinstance(ours, type)\n        and isinstance(theirs, type)\n        and getattr(ours, \"_is_protocol\", False)\n    )\n\n\n@pytest.mark.parametrize(\n    \"ours, theirs, strict\",\n    CONFORMANCE_CASES,\n    ids=[c[0].__name__ for c in CONFORMANCE_CASES],\n)\ndef test_retry_policy_signatures_align_with_tenacity(\n    ours: NamedCallable,\n    theirs: NamedCallable | None,\n    strict: bool,\n) -> None:\n    name = ours.__name__\n    ours_params = _parameter_types(ours)\n    theirs_params = {} if theirs is None else _parameter_types(theirs)\n\n    ours_names = set(ours_params)\n    theirs_names = set(theirs_params)\n\n    if strict:\n        assert ours_names == theirs_names, (\n            f\"{name}: expected exact parameter match with tenacity; \"\n            f\"missing={sorted(theirs_names - ours_names)}, \"\n            f\"unexpected={sorted(ours_names - theirs_names)}\"\n        )\n    else:\n        assert ours_names <= theirs_names, (\n            f\"{name}: expected our parameter names to be a subset of tenacity; \"\n            f\"unexpected={sorted(ours_names - theirs_names)}\"\n        )\n\n    for param in sorted(ours_names & theirs_names):\n        assert _types_match(ours_params[param], theirs_params[param]), (\n            f\"{name}.{param}: type mismatch — \"\n            f\"ours={ours_params[param]!r}, theirs={theirs_params[param]!r}\"\n        )\n\n\ndef test_wait_full_jitter_alias_matches_wait_random_exponential_signature() -> None:\n    assert _parameter_types(wait_full_jitter) == _parameter_types(\n        wait_random_exponential\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_runtime_integration.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Integration tests for runtime lifecycle with registering() context manager.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport pytest\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.plugins import BasicRuntime, basic_runtime, get_current_runtime\nfrom workflows.testing import WorkflowTestRunner\n\n\nclass SimpleWorkflow(Workflow):\n    \"\"\"Simple test workflow.\"\"\"\n\n    @step\n    async def start(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\nclass CountingWorkflow(Workflow):\n    \"\"\"Workflow that counts invocations.\"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self.run_count = 0\n\n    @step\n    async def start(self, ev: StartEvent) -> StopEvent:\n        self.run_count += 1\n        return StopEvent(result=self.run_count)\n\n\nclass StatefulWorkflow(Workflow):\n    \"\"\"Workflow that preserves state across runs.\"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self.value = 0\n\n    @step\n    async def start(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        if hasattr(ev, \"value\"):\n            self.value = getattr(ev, \"value\")\n        return StopEvent(result=self.value)\n\n\n# Basic Runtime Tests\n\n\nasync def test_basic_runtime_no_explicit_launch() -> None:\n    \"\"\"BasicRuntime works without explicit launch().\"\"\"\n    wf = SimpleWorkflow()\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == \"done\"\n\n\nasync def test_basic_runtime_with_explicit_launch() -> None:\n    \"\"\"launch()/destroy() are no-ops but don't error for BasicRuntime.\"\"\"\n    runtime = BasicRuntime()\n    await runtime.launch()\n\n    wf = SimpleWorkflow(runtime=runtime)\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == \"done\"\n\n    await runtime.destroy()\n\n\nasync def test_basic_runtime_launch_idempotent() -> None:\n    \"\"\"Multiple launch() calls are allowed.\"\"\"\n    runtime = BasicRuntime()\n    await runtime.launch()\n    await runtime.launch()\n\n    wf = SimpleWorkflow(runtime=runtime)\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == \"done\"\n\n\n# Registering Context Manager Tests\n\n\nasync def test_registering_multiple_workflows() -> None:\n    \"\"\"Multiple workflows register in same block.\"\"\"\n    runtime = BasicRuntime()\n\n    with runtime.registering():\n        wf1 = SimpleWorkflow()\n        wf2 = CountingWorkflow()\n\n    assert wf1.runtime is runtime\n    assert wf2.runtime is runtime\n\n    await runtime.launch()\n\n    result1 = await WorkflowTestRunner(wf1).run()\n    result2 = await WorkflowTestRunner(wf2).run()\n\n    assert result1.result == \"done\"\n    assert result2.result == 1\n\n\nasync def test_registering_preserves_workflow_state() -> None:\n    \"\"\"Workflow state preserved through launch().\"\"\"\n    runtime = BasicRuntime()\n\n    with runtime.registering():\n        wf = CountingWorkflow()\n\n    await runtime.launch()\n\n    # Run multiple times, state should persist\n    result1 = await WorkflowTestRunner(wf).run()\n    result2 = await WorkflowTestRunner(wf).run()\n\n    assert result1.result == 1\n    assert result2.result == 2\n\n\nasync def test_registering_with_exception_still_resets_context() -> None:\n    \"\"\"Context reset even on exception.\"\"\"\n    runtime = BasicRuntime()\n\n    with pytest.raises(ValueError):\n        with runtime.registering():\n            raise ValueError(\"test\")\n\n    # Context should be reset\n    assert get_current_runtime() is basic_runtime\n\n\nasync def test_mixed_explicit_and_implicit_registration() -> None:\n    \"\"\"Explicit runtime= overrides context.\"\"\"\n    context_runtime = BasicRuntime()\n    explicit_runtime = BasicRuntime()\n\n    with context_runtime.registering():\n        wf_implicit = SimpleWorkflow()\n        wf_explicit = SimpleWorkflow(runtime=explicit_runtime)\n\n    assert wf_implicit.runtime is context_runtime\n    assert wf_explicit.runtime is explicit_runtime\n\n    await context_runtime.launch()\n    await explicit_runtime.launch()\n\n    result1 = await WorkflowTestRunner(wf_implicit).run()\n    result2 = await WorkflowTestRunner(wf_explicit).run()\n\n    assert result1.result == \"done\"\n    assert result2.result == \"done\"\n\n\n# Workflow Execution Tests\n\n\nasync def test_workflow_can_run_multiple_times() -> None:\n    \"\"\"Same workflow runs multiple times after launch().\"\"\"\n    runtime = BasicRuntime()\n\n    with runtime.registering():\n        wf = CountingWorkflow()\n\n    await runtime.launch()\n\n    result1 = await WorkflowTestRunner(wf).run()\n    result2 = await WorkflowTestRunner(wf).run()\n    result3 = await WorkflowTestRunner(wf).run()\n\n    assert result1.result == 1\n    assert result2.result == 2\n    assert result3.result == 3\n\n\nasync def test_destroy_allows_reuse() -> None:\n    \"\"\"Runtime can create new workflows after destroy().\"\"\"\n    runtime = BasicRuntime()\n\n    with runtime.registering():\n        wf1 = SimpleWorkflow()\n\n    await runtime.launch()\n    result1 = await WorkflowTestRunner(wf1).run()\n\n    await runtime.destroy()\n\n    # Can register new workflows after destroy\n    with runtime.registering():\n        wf2 = SimpleWorkflow()\n\n    await runtime.launch()\n    result2 = await WorkflowTestRunner(wf2).run()\n\n    assert result1.result == \"done\"\n    assert result2.result == \"done\"\n\n\n# Edge Cases\n\n\nasync def test_workflow_without_any_runtime_context() -> None:\n    \"\"\"Falls back to basic_runtime.\"\"\"\n    wf = SimpleWorkflow()\n    assert wf.runtime is basic_runtime\n\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == \"done\"\n\n\nasync def test_empty_registering_block() -> None:\n    \"\"\"Empty block is valid no-op.\"\"\"\n    runtime = BasicRuntime()\n\n    with runtime.registering():\n        pass\n\n    # Context should be reset\n    assert get_current_runtime() is basic_runtime\n\n\nasync def test_registering_yields_runtime() -> None:\n    \"\"\"Context manager yields the runtime.\"\"\"\n    runtime = BasicRuntime()\n\n    with runtime.registering() as r:\n        assert r is runtime\n\n    # Context should be reset after exit\n    assert get_current_runtime() is basic_runtime\n\n\nasync def test_nested_registering_preserves_workflows() -> None:\n    \"\"\"Nested registering blocks correctly assign workflows.\"\"\"\n    outer_runtime = BasicRuntime()\n    inner_runtime = BasicRuntime()\n\n    with outer_runtime.registering():\n        wf_outer = SimpleWorkflow()\n\n        with inner_runtime.registering():\n            wf_inner = SimpleWorkflow()\n\n        wf_outer_again = SimpleWorkflow()\n\n    assert wf_outer.runtime is outer_runtime\n    assert wf_inner.runtime is inner_runtime\n    assert wf_outer_again.runtime is outer_runtime\n\n    await outer_runtime.launch()\n    await inner_runtime.launch()\n\n    r1 = await WorkflowTestRunner(wf_outer).run()\n    r2 = await WorkflowTestRunner(wf_inner).run()\n    r3 = await WorkflowTestRunner(wf_outer_again).run()\n\n    assert r1.result == \"done\"\n    assert r2.result == \"done\"\n    assert r3.result == \"done\"\n\n\nasync def test_workflow_runs_without_registering() -> None:\n    \"\"\"Workflows created outside registering() use basic_runtime.\"\"\"\n    wf = SimpleWorkflow()\n    assert wf.runtime is basic_runtime\n\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == \"done\"\n\n\nasync def test_multiple_concurrent_workflows() -> None:\n    \"\"\"Multiple workflows can run concurrently.\"\"\"\n    import asyncio\n\n    runtime = BasicRuntime()\n\n    with runtime.registering():\n        wf1 = SimpleWorkflow()\n        wf2 = SimpleWorkflow()\n        wf3 = SimpleWorkflow()\n\n    await runtime.launch()\n\n    # Run all concurrently\n    results = await asyncio.gather(\n        WorkflowTestRunner(wf1).run(),\n        WorkflowTestRunner(wf2).run(),\n        WorkflowTestRunner(wf3).run(),\n    )\n\n    assert results[0].result == \"done\"\n    assert results[1].result == \"done\"\n    assert results[2].result == \"done\"\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_spans.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport inspect\nfrom typing import Any, Generator\n\nimport pytest\nfrom llama_index_instrumentation import get_dispatcher\nfrom llama_index_instrumentation.base import BaseEvent\nfrom llama_index_instrumentation.event_handlers import BaseEventHandler\nfrom llama_index_instrumentation.span import BaseSpan\nfrom llama_index_instrumentation.span_handlers import BaseSpanHandler\nfrom pydantic import PrivateAttr\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.runtime.types.step_function import SpanCancelledEvent\nfrom workflows.workflow import Workflow\n\n\nclass SpanTracker(BaseSpanHandler[BaseSpan]):\n    \"\"\"Track span lifecycle events for testing.\"\"\"\n\n    _exited_ids: list[str] = PrivateAttr(default_factory=list)\n    _dropped_ids: list[tuple[str, BaseException | None]] = PrivateAttr(\n        default_factory=list\n    )\n\n    @classmethod\n    def class_name(cls) -> str:\n        return \"SpanTracker\"\n\n    def new_span(\n        self,\n        id_: str,\n        bound_args: inspect.BoundArguments,\n        instance: Any | None = None,\n        parent_span_id: str | None = None,\n        tags: dict[str, Any] | None = None,\n        **kwargs: Any,\n    ) -> BaseSpan | None:\n        return BaseSpan(id_=id_, parent_id=parent_span_id, tags=tags or {})\n\n    def prepare_to_exit_span(\n        self,\n        id_: str,\n        bound_args: inspect.BoundArguments,\n        instance: Any | None = None,\n        result: Any | None = None,\n        **kwargs: Any,\n    ) -> BaseSpan | None:\n        span = self.open_spans.get(id_)\n        self._exited_ids.append(id_)\n        return span\n\n    def prepare_to_drop_span(\n        self,\n        id_: str,\n        bound_args: inspect.BoundArguments,\n        instance: Any | None = None,\n        err: BaseException | None = None,\n        **kwargs: Any,\n    ) -> BaseSpan | None:\n        span = self.open_spans.get(id_)\n        self._dropped_ids.append((id_, err))\n        return span\n\n\nclass EventTracker(BaseEventHandler):\n    \"\"\"Track dispatched events for testing.\"\"\"\n\n    _events: list[BaseEvent] = PrivateAttr(default_factory=list)\n\n    @classmethod\n    def class_name(cls) -> str:\n        return \"EventTracker\"\n\n    def handle(self, event: BaseEvent, **kwargs: Any) -> None:\n        self._events.append(event)\n\n\n@pytest.fixture\ndef span_tracker() -> Generator[SpanTracker, None, None]:\n    tracker = SpanTracker()\n    root = get_dispatcher()\n    root.span_handlers.append(tracker)\n    yield tracker\n    root.span_handlers.remove(tracker)\n\n\n@pytest.fixture\ndef event_tracker() -> Generator[EventTracker, None, None]:\n    tracker = EventTracker()\n    root = get_dispatcher()\n    root.event_handlers.append(tracker)\n    yield tracker\n    root.event_handlers.remove(tracker)\n\n\nclass WaitEvent(Event):\n    value: str\n\n\nasync def test_wait_for_event_does_not_produce_dropped_spans(\n    span_tracker: SpanTracker,\n) -> None:\n    \"\"\"A step using wait_for_event should exit cleanly, not drop with an error.\"\"\"\n\n    class WaitWorkflow(Workflow):\n        @step\n        async def wait_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            result = await ctx.wait_for_event(WaitEvent)\n            return StopEvent(result=result.value)\n\n    wf = WaitWorkflow()\n    handler = wf.run()\n    assert handler.ctx is not None\n\n    handler.ctx.send_event(WaitEvent(value=\"hello\"))\n\n    result = await handler\n    assert result == \"hello\"\n\n    step_drops = [\n        (id_, err) for id_, err in span_tracker._dropped_ids if \"wait_step\" in id_\n    ]\n    step_exits = [id_ for id_ in span_tracker._exited_ids if \"wait_step\" in id_]\n\n    assert step_drops == [], (\n        f\"Expected no dropped spans for wait_step, got: {step_drops}\"\n    )\n    # Exited twice: once for the WaitingForEvent invocation, once for the replay\n    assert len(step_exits) >= 2, (\n        f\"Expected at least 2 span exits for wait_step, got {len(step_exits)}: {step_exits}\"\n    )\n\n\nasync def test_cancel_run_produces_exited_spans_not_dropped(\n    span_tracker: SpanTracker,\n    event_tracker: EventTracker,\n) -> None:\n    \"\"\"Cancelling a workflow should exit spans cleanly (OK) and emit SpanCancelledEvents.\"\"\"\n\n    class SleepWorkflow(Workflow):\n        @step\n        async def sleep_step(self, ev: StartEvent) -> StopEvent:\n            await asyncio.sleep(3600)\n            return StopEvent(result=\"unreachable\")\n\n    wf = SleepWorkflow()\n    handler = wf.run()\n\n    await asyncio.sleep(0.05)\n    await handler.cancel_run()\n    try:\n        await handler\n    except Exception:\n        pass\n\n    step_drops = [\n        (id_, err) for id_, err in span_tracker._dropped_ids if \"sleep_step\" in id_\n    ]\n    step_exits = [id_ for id_ in span_tracker._exited_ids if \"sleep_step\" in id_]\n\n    assert step_drops == [], (\n        f\"Expected no dropped spans for sleep_step, got: {step_drops}\"\n    )\n    assert len(step_exits) == 1, (\n        f\"Expected 1 span exit for sleep_step, got {len(step_exits)}: {step_exits}\"\n    )\n\n    # run_workflow span should also be exited, not dropped\n    run_drops = [\n        (id_, err)\n        for id_, err in span_tracker._dropped_ids\n        if \"SleepWorkflow.run\" in id_\n    ]\n    run_exits = [id_ for id_ in span_tracker._exited_ids if \"SleepWorkflow.run\" in id_]\n    assert run_drops == [], (\n        f\"Expected no dropped spans for run_workflow, got: {run_drops}\"\n    )\n    assert len(run_exits) == 1, (\n        f\"Expected 1 span exit for run_workflow, got {len(run_exits)}: {run_exits}\"\n    )\n\n    # SpanCancelledEvents should have been emitted\n    cancel_events = [\n        e for e in event_tracker._events if isinstance(e, SpanCancelledEvent)\n    ]\n    assert len(cancel_events) >= 1, (\n        f\"Expected at least 1 SpanCancelledEvent, got {len(cancel_events)}\"\n    )\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_state_manager.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\n\"\"\"Minimal unit tests for InMemoryStateStore.\n\nFull state store protocol tests are in the integration test package\n(llama-index-integration-tests/tests/test_state_store_matrix.py),\nwhich tests InMemoryStateStore alongside SqlStateStore.\n\nThese tests provide fast feedback during development of the base package.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport pytest\nfrom workflows.context.state_store import DictState, InMemoryStateStore\n\n\n@pytest.mark.asyncio\nasync def test_in_memory_state_store_smoke() -> None:\n    \"\"\"Smoke test for basic InMemoryStateStore functionality.\"\"\"\n    store: InMemoryStateStore[DictState] = InMemoryStateStore(DictState())\n\n    # Basic get/set\n    await store.set(\"name\", \"test\")\n    assert await store.get(\"name\") == \"test\"\n\n    # Nested path\n    await store.set(\"nested\", {\"key\": \"value\"})\n    assert await store.get(\"nested.key\") == \"value\"\n\n    # Default on missing\n    assert await store.get(\"missing\", default=None) is None\n\n    # Clear\n    await store.clear()\n    assert await store.get(\"name\", default=None) is None\n\n\n@pytest.mark.asyncio\nasync def test_in_memory_edit_state() -> None:\n    \"\"\"Test edit_state context manager.\"\"\"\n    store: InMemoryStateStore[DictState] = InMemoryStateStore(DictState())\n\n    async with store.edit_state() as state:\n        state[\"counter\"] = 1\n\n    assert await store.get(\"counter\") == 1\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_streaming.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nimport asyncio\nimport json\nfrom typing import AsyncGenerator\n\nimport pytest\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.errors import WorkflowRuntimeError\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.workflow import Workflow\n\n\nclass StreamingWorkflow(Workflow):\n    @step\n    async def chat(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        async def stream_messages() -> AsyncGenerator[str, None]:\n            resp = \"Paul Graham is a British-American computer scientist, entrepreneur, vc, and writer.\"\n            for word in resp.split():\n                yield word\n\n        async for w in stream_messages():\n            ctx.write_event_to_stream(Event(msg=w))\n\n        return StopEvent(result=None)\n\n\n@pytest.mark.asyncio\nasync def test_multiple_sequential_streams() -> None:\n    test_runner = WorkflowTestRunner(StreamingWorkflow())\n    # stream 1\n    await test_runner.run(StartEvent())\n    # stream 2 -- should not raise an error\n    await test_runner.run(StartEvent())\n\n\n@pytest.mark.asyncio\nasync def test_consume_only_once() -> None:\n    wf = StreamingWorkflow()\n    handler = wf.run()\n\n    async for _ in handler.stream_events():\n        pass\n\n    with pytest.raises(\n        WorkflowRuntimeError,\n        match=\"All the streamed events have already been consumed.\",\n    ):\n        async for _ in handler.stream_events():\n            pass\n\n    await handler\n\n\n@pytest.mark.asyncio\nasync def test_multiple_ongoing_streams() -> None:\n    wf = StreamingWorkflow()\n    stream_1 = wf.run()\n    stream_2 = wf.run()\n\n    async for ev in stream_1.stream_events():\n        if not isinstance(ev, StopEvent):\n            assert \"msg\" in ev\n\n    async for ev in stream_2.stream_events():\n        if not isinstance(ev, StopEvent):\n            assert \"msg\" in ev\n\n    await asyncio.gather(stream_1, stream_2)\n\n\n@pytest.mark.asyncio\nasync def test_resume_streams() -> None:\n    class CounterWorkflow(Workflow):\n        @step\n        async def count(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            ctx.write_event_to_stream(Event(msg=\"hello!\"))\n\n            cur_count = await ctx.store.get(\"cur_count\", default=0)\n            await ctx.store.set(\"cur_count\", cur_count + 1)\n            return StopEvent(result=\"done\")\n\n    wf = CounterWorkflow()\n    result = await WorkflowTestRunner(wf).run()\n    ctx1 = result.ctx\n    assert ctx1\n\n    result2 = await WorkflowTestRunner(wf).run(ctx=ctx1)\n\n    ctx2 = result2.ctx\n\n    assert ctx2\n    ctx_dict = ctx2.to_dict()\n    # State is serialized as JSON strings under state_data._data for DictState\n    assert json.loads(ctx_dict[\"state\"][\"state_data\"][\"_data\"][\"cur_count\"]) == 2\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_testing_utils.py",
    "content": "import pytest\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StepStateChanged,\n    StopEvent,\n)\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.testing.runner import WorkflowTestResult\n\n\nclass SecondEvent(Event):\n    greeting: str\n\n\nclass SimpleWf(Workflow):\n    @step\n    async def step_one(self, ev: StartEvent, ctx: Context) -> SecondEvent:\n        async with ctx.store.edit_state() as state:\n            state.test = \"this is a test\"\n        ctx.write_event_to_stream(SecondEvent(greeting=\"hello\"))\n        return SecondEvent(greeting=\"hello\")\n\n    @step\n    async def step_two(self, ev: SecondEvent, ctx: Context) -> StopEvent:\n        async with ctx.store.edit_state() as state:\n            state.hello = \"hello\"\n        return StopEvent(result=\"done\")\n\n\n@pytest.mark.asyncio\nasync def test_testing_utils() -> None:\n    wf = SimpleWf()\n    runner = WorkflowTestRunner(wf)\n    wf_test_run = await runner.run(\n        start_event=StartEvent(message=\"hi\"),  # type: ignore\n    )\n    assert isinstance(wf_test_run, WorkflowTestResult)\n    assert len(wf_test_run.collected) == sum(\n        [wf_test_run.event_types[k] for k in wf_test_run.event_types]\n    )\n    assert wf_test_run.event_types.get(SecondEvent, 0) == 1\n    assert wf_test_run.event_types.get(StopEvent, 0) == 1\n    assert wf_test_run.event_types.get(StepStateChanged, 0) == len(\n        wf_test_run.collected\n    ) - (\n        wf_test_run.event_types.get(SecondEvent, 0)\n        + wf_test_run.event_types.get(StopEvent, 0)\n    )\n    assert str(wf_test_run.result) == \"done\"\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_utils.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nimport inspect\nfrom typing import Any, get_type_hints\n\nimport pytest\nfrom workflows.context import Context\nfrom workflows.decorators import step\nfrom workflows.errors import WorkflowValidationError\nfrom workflows.events import StartEvent, StopEvent\nfrom workflows.utils import (\n    _get_param_types,\n    _get_return_types,\n    get_steps_from_class,\n    get_steps_from_instance,\n    inspect_signature,\n    is_free_function,\n    validate_step_signature,\n)\n\nfrom .conftest import (  # type: ignore[import]\n    AnotherTestEvent,\n    OneTestEvent,\n)\n\n\ndef test_validate_step_signature_of_method() -> None:\n    def f(self, ev: OneTestEvent) -> OneTestEvent:  # noqa: ANN001\n        return OneTestEvent()\n\n    validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_of_free_function() -> None:\n    def f(ev: OneTestEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_union() -> None:\n    def f(ev: OneTestEvent | AnotherTestEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_of_free_function_with_context() -> None:\n    def f(ctx: Context, ev: OneTestEvent) -> OneTestEvent:\n        return OneTestEvent()\n\n    validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_union_invalid() -> None:\n    def f(ev: OneTestEvent | str) -> None:\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step signature must have at least one parameter annotated as type Event\",\n    ):\n        validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_no_params() -> None:\n    def f() -> None:\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError, match=\"Step signature must have at least one parameter\"\n    ):\n        validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_no_annotations() -> None:\n    def f(self, ev) -> None:  # noqa: ANN001\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step signature must have at least one parameter annotated as type Event\",\n    ):\n        validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_wrong_annotations() -> None:\n    def f(self, ev: str) -> None:  # noqa: ANN001\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step signature must have at least one parameter annotated as type Event\",\n    ):\n        validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_no_return_annotations() -> None:\n    def f(self, ev: OneTestEvent):  # noqa: ANN001\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Return types of workflows step functions must be annotated with their type\",\n    ):\n        validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_no_events() -> None:\n    def f(self, ctx: Context) -> None:  # noqa: ANN001\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step signature must have at least one parameter annotated as type Event\",\n    ):\n        validate_step_signature(inspect_signature(f))\n\n\ndef test_validate_step_signature_too_many_params() -> None:\n    def f1(self, ev: OneTestEvent, foo: OneTestEvent) -> None:  # noqa: ANN001\n        pass\n\n    def f2(ev: OneTestEvent, foo: OneTestEvent) -> None:  # noqa: ANN001\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step signature must contain exactly one parameter of type Event but found 2.\",\n    ):\n        validate_step_signature(inspect_signature(f1))\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step signature must contain exactly one parameter of type Event but found 2.\",\n    ):\n        validate_step_signature(inspect_signature(f2))\n\n\ndef test_get_steps_from() -> None:\n    class Test:\n        @step\n        def start(self, start: StartEvent) -> OneTestEvent:\n            return OneTestEvent()\n\n        @step\n        def my_method(self, event: OneTestEvent) -> StopEvent:\n            return StopEvent()\n\n        def not_a_step(self) -> None:\n            pass\n\n    steps = get_steps_from_class(Test)\n    assert len(steps)\n    assert \"my_method\" in steps\n\n    steps = get_steps_from_instance(Test())\n    assert len(steps)\n    assert \"my_method\" in steps\n\n\ndef test_get_param_types() -> None:\n    def f(foo: str) -> None:\n        pass\n\n    sig = inspect.signature(f)\n    type_hints = get_type_hints(f)\n    res = _get_param_types(sig.parameters[\"foo\"], type_hints)\n    assert len(res) == 1\n    assert res[0] is str\n\n\ndef test_get_param_types_no_annotations() -> None:\n    def f(foo) -> None:  # noqa: ANN001\n        pass\n\n    sig = inspect.signature(f)\n    type_hints = get_type_hints(f)\n    res = _get_param_types(sig.parameters[\"foo\"], type_hints)\n    assert len(res) == 1\n    assert res[0] is Any\n\n\ndef test_get_param_types_union() -> None:\n    def f(foo: str | int) -> None:\n        pass\n\n    sig = inspect.signature(f)\n    type_hints = get_type_hints(f)\n    res = _get_param_types(sig.parameters[\"foo\"], type_hints)\n    assert len(res) == 2\n    assert res == [str, int]\n\n\ndef test_get_return_types() -> None:\n    def f(foo: int) -> str:\n        return \"\"\n\n    assert _get_return_types(f) == [str]\n\n\ndef test_get_return_types_union() -> None:\n    def f(foo: int) -> str | int:\n        return \"\"\n\n    assert _get_return_types(f) == [str, int]\n\n\ndef test_get_return_types_optional() -> None:\n    def f(foo: int) -> str | None:\n        return \"\"\n\n    assert _get_return_types(f) == [str]\n\n\ndef test_get_return_types_list() -> None:\n    def f(foo: int) -> list[str]:\n        return [\"\"]\n\n    assert _get_return_types(f) == [list[str]]\n\n\ndef test_is_free_function() -> None:\n    assert is_free_function(\"my_function\") is True\n    assert is_free_function(\"MyClass.my_method\") is False\n    assert is_free_function(\"some_function.<locals>.my_function\") is True\n    assert is_free_function(\"some_function.<locals>.MyClass.my_function\") is False\n    with pytest.raises(ValueError):\n        is_free_function(\"\")\n\n\ndef test_inspect_signature_raises_if_not_callable() -> None:\n    with pytest.raises(TypeError, match=\"Expected a callable object, got str\"):\n        inspect_signature(\"foo\")  # type: ignore\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_verbose_decorator.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for VerboseDecorator and _VerboseInternalRunAdapter.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nimport pytest\nfrom workflows import Workflow, step\nfrom workflows.events import Event, StartEvent, StepState, StepStateChanged, StopEvent\nfrom workflows.runtime.types.plugin import InternalRunAdapter, WaitResult, WorkflowTick\nfrom workflows.runtime.types.results import StepWorkerResult\nfrom workflows.runtime.types.ticks import (\n    TickAddEvent,\n    TickCancelRun,\n    TickIdleRelease,\n    TickPublishEvent,\n    TickStepResult,\n    TickTimeout,\n    TickWaiterTimeout,\n)\nfrom workflows.runtime.verbose import _VerboseInternalRunAdapter\nfrom workflows.testing import WorkflowTestRunner\n\n\nclass FakeInternalRunAdapter(InternalRunAdapter):\n    \"\"\"Minimal fake adapter that records events written to the stream.\"\"\"\n\n    def __init__(self) -> None:\n        self.written_events: list[Event] = []\n\n    @property\n    def run_id(self) -> str:\n        return \"fake-run-id\"\n\n    async def write_to_event_stream(self, event: Event) -> None:\n        self.written_events.append(event)\n\n    async def get_now(self) -> float:\n        raise NotImplementedError\n\n    async def send_event(self, tick: WorkflowTick) -> None:\n        raise NotImplementedError\n\n    async def wait_receive(\n        self,\n        timeout_seconds: float | None = None,\n    ) -> WaitResult:\n        raise NotImplementedError\n\n    async def sleep(self, seconds: float) -> None:\n        raise NotImplementedError\n\n\ndef _make_step_state_changed(\n    name: str = \"my_step\",\n    step_state: StepState = StepState.RUNNING,\n    worker_id: str = \"0\",\n    input_event_name: str = \"StartEvent\",\n    output_event_name: str | None = None,\n) -> StepStateChanged:\n    return StepStateChanged(\n        name=name,\n        step_state=step_state,\n        worker_id=worker_id,\n        input_event_name=input_event_name,\n        output_event_name=output_event_name,\n    )\n\n\n@pytest.fixture\ndef verbose_adapter() -> tuple[FakeInternalRunAdapter, _VerboseInternalRunAdapter]:\n    fake = FakeInternalRunAdapter()\n    adapter = _VerboseInternalRunAdapter(fake, output=print)\n    return fake, adapter\n\n\n# -- write_to_event_stream tests (step state changes) --\n\n\n@pytest.mark.parametrize(\n    \"event,expected\",\n    [\n        pytest.param(\n            _make_step_state_changed(\n                name=\"my_step\", step_state=StepState.RUNNING, worker_id=\"0\"\n            ),\n            \"[my_step:0] started from StartEvent\",\n            id=\"running\",\n        ),\n        pytest.param(\n            _make_step_state_changed(\n                name=\"my_step\",\n                step_state=StepState.NOT_RUNNING,\n                output_event_name=\"MyEvent\",\n                worker_id=\"2\",\n            ),\n            \"[my_step:2] complete with MyEvent\",\n            id=\"complete-with-event\",\n        ),\n        pytest.param(\n            _make_step_state_changed(\n                name=\"my_step\",\n                step_state=StepState.NOT_RUNNING,\n                output_event_name=None,\n                worker_id=\"1\",\n            ),\n            \"[my_step:1] complete with no result\",\n            id=\"complete-no-result\",\n        ),\n        pytest.param(\n            _make_step_state_changed(\n                name=\"my_step\",\n                step_state=StepState.PREPARING,\n                worker_id=\"<enqueued>\",\n            ),\n            \"[my_step] enqueued (waiting for capacity)\",\n            id=\"preparing\",\n        ),\n    ],\n)\nasync def test_verbose_step_state(\n    verbose_adapter: tuple[FakeInternalRunAdapter, _VerboseInternalRunAdapter],\n    capsys: pytest.CaptureFixture[str],\n    event: StepStateChanged,\n    expected: str,\n) -> None:\n    _, adapter = verbose_adapter\n    await adapter.write_to_event_stream(event)\n    assert expected in capsys.readouterr().out\n\n\nasync def test_verbose_auto_detects_logger_when_info_enabled(\n    caplog: pytest.LogCaptureFixture,\n) -> None:\n    logger = logging.getLogger(\"workflows.verbose\")\n    old_level = logger.level\n    try:\n        logger.setLevel(logging.INFO)\n        from workflows.runtime.verbose import _resolve_output\n\n        output = _resolve_output()\n        fake = FakeInternalRunAdapter()\n        adapter = _VerboseInternalRunAdapter(fake, output=output)\n\n        event = _make_step_state_changed(name=\"my_step\", step_state=StepState.RUNNING)\n        with caplog.at_level(logging.INFO, logger=\"workflows.verbose\"):\n            await adapter.write_to_event_stream(event)\n\n        assert \"[my_step:0] started from StartEvent\" in caplog.text\n    finally:\n        logger.setLevel(old_level)\n\n\nasync def test_verbose_falls_back_to_print_by_default() -> None:\n    logger = logging.getLogger(\"workflows.verbose\")\n    old_level = logger.level\n    try:\n        logger.setLevel(logging.NOTSET)\n        from workflows.runtime.verbose import _resolve_output\n\n        output = _resolve_output()\n        assert output is print\n    finally:\n        logger.setLevel(old_level)\n\n\nasync def test_verbose_forwards_events(\n    verbose_adapter: tuple[FakeInternalRunAdapter, _VerboseInternalRunAdapter],\n) -> None:\n    fake, adapter = verbose_adapter\n    event = _make_step_state_changed(name=\"my_step\", step_state=StepState.RUNNING)\n    await adapter.write_to_event_stream(event)\n\n    assert len(fake.written_events) == 1\n    assert fake.written_events[0] is event\n\n\n# -- on_tick tests (tick-level logging) --\n\n\n@pytest.mark.parametrize(\n    \"tick,expected\",\n    [\n        pytest.param(\n            TickAddEvent(event=StartEvent()),\n            \"[tick] add: StartEvent()\",\n            id=\"add-event\",\n        ),\n        pytest.param(\n            TickAddEvent(event=StartEvent(), step_name=\"retrieve\"),\n            \"[tick] add: StartEvent() -> retrieve\",\n            id=\"add-event-targeted\",\n        ),\n        pytest.param(\n            TickPublishEvent(event=StopEvent(result=\"done\")),\n            \"[tick] publish: StopEvent(result='done')\",\n            id=\"publish-event\",\n        ),\n        pytest.param(\n            TickTimeout(timeout=30.0),\n            \"[tick] timeout: 30.0s\",\n            id=\"timeout\",\n        ),\n        pytest.param(\n            TickWaiterTimeout(step_name=\"my_step\", waiter_id=\"w-123\"),\n            \"[tick] waiter timeout: step my_step waiter w-123\",\n            id=\"waiter-timeout\",\n        ),\n        pytest.param(\n            TickCancelRun(),\n            \"[tick] cancelled\",\n            id=\"cancel\",\n        ),\n        pytest.param(\n            TickIdleRelease(),\n            \"[tick] idle release\",\n            id=\"idle-release\",\n        ),\n    ],\n)\nasync def test_verbose_on_tick(\n    verbose_adapter: tuple[FakeInternalRunAdapter, _VerboseInternalRunAdapter],\n    capsys: pytest.CaptureFixture[str],\n    tick: WorkflowTick,\n    expected: str,\n) -> None:\n    _, adapter = verbose_adapter\n    await adapter.on_tick(tick)\n    assert expected in capsys.readouterr().out\n\n\nasync def test_verbose_tick_step_result_logs_stop_event(\n    verbose_adapter: tuple[FakeInternalRunAdapter, _VerboseInternalRunAdapter],\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    \"\"\"TickStepResult with a StopEvent logs a [result] line.\"\"\"\n    _, adapter = verbose_adapter\n    tick = TickStepResult(\n        step_name=\"my_step\",\n        worker_id=0,\n        event=StartEvent(),\n        result=[StepWorkerResult(result=StopEvent(result=\"done\"))],\n    )\n    await adapter.on_tick(tick)\n\n    captured = capsys.readouterr()\n    assert \"[result] StopEvent(result='done')\" in captured.out\n\n\nasync def test_verbose_tick_step_result_silent_for_non_stop(\n    verbose_adapter: tuple[FakeInternalRunAdapter, _VerboseInternalRunAdapter],\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    \"\"\"TickStepResult without a StopEvent produces no on_tick output.\"\"\"\n    _, adapter = verbose_adapter\n    tick = TickStepResult(\n        step_name=\"my_step\",\n        worker_id=0,\n        event=StartEvent(),\n        result=[StepWorkerResult(result=StartEvent())],\n    )\n    await adapter.on_tick(tick)\n\n    assert capsys.readouterr().out == \"\"\n\n\n# -- Integration test --\n\n\nclass TwoStepWorkflow(Workflow):\n    @step\n    async def first(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\nasync def test_workflow_verbose_integration(\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    wf = TwoStepWorkflow(verbose=True)\n    result = await WorkflowTestRunner(wf).run()\n    assert result.result == \"done\"\n\n    captured = capsys.readouterr()\n    assert \"[first:0] started from StartEvent\" in captured.out\n    assert \"[first:0] complete with StopEvent\" in captured.out\n    assert \"[result] StopEvent(result='done')\" in captured.out\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_workflow.py",
    "content": "# ty: ignore[invalid-argument-type, invalid-assignment, unknown-argument]\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\nimport gc\nimport json\nimport logging\nimport pickle\nimport threading\nimport weakref\nfrom typing import Any, Callable, Optional, cast\nfrom unittest import mock\n\nimport pytest\nfrom llama_index_instrumentation.dispatcher import (\n    active_instrument_tags,\n    instrument_tags,\n)\nfrom pydantic import PrivateAttr\nfrom workflows.context import Context, PickleSerializer\nfrom workflows.decorators import catch_error, step\nfrom workflows.errors import (\n    WorkflowConfigurationError,\n    WorkflowRuntimeError,\n    WorkflowValidationError,\n)\nfrom workflows.events import (\n    Event,\n    HumanResponseEvent,\n    InputRequiredEvent,\n    StartEvent,\n    StepFailedEvent,\n    StopEvent,\n)\nfrom workflows.handler import WorkflowHandler\nfrom workflows.runtime.types.ticks import TickAddEvent\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.workflow import Workflow\n\nfrom .conftest import (  # type: ignore[import]\n    DummyWorkflow,\n    OneTestEvent,\n)\n\n\nclass EventWithName(Event):\n    name: str\n\n\nclass MyStart(StartEvent):\n    query: str\n\n\nclass MyStop(StopEvent):\n    outcome: str\n\n\nclass ResumeStartEvent(StartEvent):\n    topic: str\n\n\ndef test_fn() -> None:\n    print(\"test_fn\")\n\n\n@pytest.mark.asyncio\nasync def test_workflow_initialization(workflow: Workflow) -> None:\n    assert workflow._timeout == 45\n    assert not workflow._disable_validation\n    assert not workflow._verbose\n\n\n@pytest.mark.asyncio\nasync def test_workflow_validation_unproduced_events() -> None:\n    class InvalidWorkflow(Workflow):\n        @step\n        async def invalid_step(self, ev: StartEvent) -> None:\n            pass\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=\"At least one Event of type StopEvent must be returned by any step.\",\n    ):\n        InvalidWorkflow()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_validation_unconsumed_events() -> None:\n    class InvalidWorkflow(Workflow):\n        @step\n        async def invalid_step(self, ev: StartEvent) -> OneTestEvent:\n            return OneTestEvent()\n\n        @step\n        async def a_step(self, ev: StartEvent) -> StopEvent:\n            return StopEvent()\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"The following events are produced but never consumed: OneTestEvent\",\n    ):\n        await WorkflowTestRunner(InvalidWorkflow()).run()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_validation_start_event_not_consumed() -> None:\n    class InvalidWorkflow(Workflow):\n        @step\n        async def a_step(self, ev: OneTestEvent) -> StopEvent:\n            return StopEvent()\n\n        @step\n        async def another_step(self, ev: OneTestEvent) -> OneTestEvent:\n            return OneTestEvent()\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=\"At least one Event of type StartEvent must be received by any step.\",\n    ):\n        InvalidWorkflow()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_step_send_event_to_None() -> None:\n    class StepSendEventToNoneWorkflow(Workflow):\n        @step\n        async def step1(self, ctx: Context, ev: StartEvent) -> OneTestEvent:\n            ctx.send_event(OneTestEvent(), step=None)\n            return  # type:ignore\n\n        @step\n        async def step2(self, ev: OneTestEvent) -> StopEvent:\n            return StopEvent(result=\"step2\")\n\n    workflow = StepSendEventToNoneWorkflow(verbose=True)\n    result = await WorkflowTestRunner(workflow).run()\n    from workflows.context.external_context import ExternalContext\n\n    assert isinstance(result.ctx._face, ExternalContext)\n    replay = result.ctx._face._tick_log\n    assert TickAddEvent(event=OneTestEvent()) in replay\n\n\n@pytest.mark.asyncio\nasync def test_workflow_step_returning_bogus() -> None:\n    class TestWorkflow(Workflow):\n        @step\n        async def step1(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            return \"foo\"  # type:ignore\n\n    with pytest.raises(\n        WorkflowRuntimeError,\n        match=\"Step function step1 returned str instead of an Event instance.\",\n    ):\n        await WorkflowTestRunner(TestWorkflow()).run()\n\n\ndef test_add_step() -> None:\n    class TestWorkflow(Workflow):\n        @step\n        def foo_step(self, ev: StartEvent) -> None:\n            pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"A step foo_step is already part of this workflow, please choose another name.\",\n    ):\n\n        @step(workflow=TestWorkflow)\n        def foo_step(ev: StartEvent) -> None:\n            pass\n\n\ndef test_add_step_not_a_step() -> None:\n    class TestWorkflow(Workflow):\n        @step\n        def a_ste(self, ev: StartEvent) -> None:\n            pass\n\n    def another_step(ev: StartEvent) -> None:\n        pass\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step function another_step is missing the `@step` decorator.\",\n    ):\n        TestWorkflow.add_step(another_step)  # type: ignore[reportArgumentType]\n\n\ndef test_workflow_disable_validation() -> None:\n    class DummyWorkflow(Workflow):\n        @step\n        async def step(self, ev: StartEvent) -> StopEvent:\n            raise ValueError(\"The step raised an error!\")\n\n    w = DummyWorkflow(disable_validation=True)\n    w._get_steps = mock.MagicMock()  # type: ignore[assignment]\n    mock_get_steps = cast(mock.MagicMock, w._get_steps)\n    w._validate()\n    mock_get_steps.assert_not_called()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_pickle() -> None:\n    class DummyWorkflow(Workflow):\n        @step\n        async def step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            cur_step = await ctx.store.get(\"step\", default=0)\n            await ctx.store.set(\"step\", cur_step + 1)\n            await ctx.store.set(\"test_fn\", test_fn)\n            return StopEvent(result=\"Done\")\n\n    wf = DummyWorkflow(timeout=1)\n    r = await WorkflowTestRunner(wf).run()\n    ctx = r.ctx\n    assert ctx\n\n    # by default, we can't pickle the LLM/embedding object\n    with pytest.raises(ValueError):\n        ctx.to_dict()\n\n    # if we allow pickle, then we can pickle the LLM/embedding object\n    state_dict = ctx.to_dict(serializer=PickleSerializer())\n    # Verify step count via serialized dict (integers are JSON serialized)\n    assert json.loads(state_dict[\"state\"][\"state_data\"][\"_data\"][\"step\"]) == 1\n\n    # Restore and run again to verify deserialization works\n    new_ctx = Context.from_dict(wf, state_dict, serializer=PickleSerializer())\n    r2 = await WorkflowTestRunner(wf).run(ctx=new_ctx)\n    ctx2 = r2.ctx\n    assert ctx2\n\n    # check that the step count is incremented after second run\n    state_dict2 = ctx2.to_dict(serializer=PickleSerializer())\n    assert json.loads(state_dict2[\"state\"][\"state_data\"][\"_data\"][\"step\"]) == 2\n\n\n@pytest.mark.asyncio\nasync def test_workflow_context_to_dict() -> None:\n    ctx: Context | None = None\n    new_ctx: Context | None = None\n    signal_continue = asyncio.Event()\n    signal_ready = asyncio.Event()\n    run_count = 0\n\n    class StallableWorkflow(Workflow):\n        @step\n        async def step1(self, ev: StartEvent) -> EventWithName:\n            nonlocal run_count\n            run_count += 1\n            if run_count > 1:\n                raise ValueError(\"Start ran more than once\")\n            return EventWithName(name=\"test\")\n\n        @step\n        async def step2(self, ev: EventWithName) -> StopEvent:\n            signal_ready.set()\n            await signal_continue.wait()\n            return StopEvent(result=\"Done\")\n\n    from workflows.context.external_context import ExternalContext\n\n    workflow = StallableWorkflow()\n    try:\n        handler = workflow.run()\n        ctx = handler.ctx\n        await signal_ready.wait()\n        # get the context dict\n        data = ctx.to_dict()\n\n        await handler.cancel_run()\n\n        new_ctx = Context.from_dict(workflow, data)\n        handler2 = workflow.run(ctx=new_ctx)\n        signal_continue.set()\n        await handler2\n    finally:\n        if ctx is not None and isinstance(ctx._face, ExternalContext):\n            await ctx._face.shutdown()\n        if new_ctx is not None and isinstance(new_ctx._face, ExternalContext):\n            await new_ctx._face.shutdown()\n\n\nclass HumanInTheLoopWorkflow(Workflow):\n    @step\n    async def step1(self, ctx: Context, ev: StartEvent) -> InputRequiredEvent:\n        cur_runs = await ctx.store.get(\"step1_runs\", default=0)\n        await ctx.store.set(\"step1_runs\", cur_runs + 1)\n        return InputRequiredEvent(prefix=\"Enter a number: \")  # type: ignore[reportCallIssue]\n\n    @step\n    async def step2(self, ctx: Context, ev: HumanResponseEvent) -> StopEvent:\n        cur_runs = await ctx.store.get(\"step2_runs\", default=0)\n        await ctx.store.set(\"step2_runs\", cur_runs + 1)\n        return StopEvent(result=ev.response)\n\n\n@pytest.mark.asyncio\nasync def test_human_in_the_loop_with_resume() -> None:\n    # workflow should work with streaming\n    workflow = HumanInTheLoopWorkflow()\n\n    handler: WorkflowHandler = workflow.run()\n    assert handler.ctx\n\n    ctx_dict = None\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            ctx_dict = handler.ctx.to_dict()\n            await handler.cancel_run()\n            await asyncio.sleep(0.01)\n            break\n\n    assert handler.exception()\n    new_handler = workflow.run(ctx=Context.from_dict(workflow, ctx_dict))  # type: ignore[reportArgumentType]\n    new_handler.ctx.send_event(HumanResponseEvent(response=\"42\"))  # type: ignore[reportCallIssue]\n\n    final_result = await new_handler\n    assert final_result == \"42\"\n\n    # ensure the workflow ran each step once\n    assert new_handler.ctx is not None\n    ctx_dict = new_handler.ctx.to_dict()\n    assert json.loads(ctx_dict[\"state\"][\"state_data\"][\"_data\"][\"step1_runs\"]) == 1  # type: ignore[index]\n    assert json.loads(ctx_dict[\"state\"][\"state_data\"][\"_data\"][\"step2_runs\"]) == 1  # type: ignore[index]\n\n\n@pytest.mark.asyncio\nasync def test_human_in_the_loop_resume_custom_start_event_inactive_ctx() -> None:\n    class CustomHumanWorkflow(Workflow):\n        @step\n        async def ask(self, ctx: Context, ev: ResumeStartEvent) -> InputRequiredEvent:\n            runs = await ctx.store.get(\"ask_runs\", default=0)\n            await ctx.store.set(\"ask_runs\", runs + 1)\n            return InputRequiredEvent(prefix=ev.topic)  # type: ignore[arg-type]\n\n        @step\n        async def complete(self, ctx: Context, ev: HumanResponseEvent) -> StopEvent:\n            runs = await ctx.store.get(\"complete_runs\", default=0)\n            await ctx.store.set(\"complete_runs\", runs + 1)\n            return StopEvent(result=ev.response)\n\n    workflow = CustomHumanWorkflow()\n    handler: WorkflowHandler = workflow.run(topic=\"pizza\")\n    assert handler.ctx\n\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            break\n\n    await handler.cancel_run()\n    ctx_dict = handler.ctx.to_dict()\n    assert ctx_dict[\"is_running\"]\n\n    resumed_ctx = Context.from_dict(workflow, ctx_dict)\n    resumed_handler = workflow.run(ctx=resumed_ctx)\n    resumed_handler.ctx.send_event(HumanResponseEvent(response=\"42\"))  # type: ignore[arg-type]\n\n    events = []\n    async for event in resumed_handler.stream_events():\n        events.append(event)\n\n    assert events == [StopEvent(result=\"42\")]\n\n    final_result = await resumed_handler\n    assert final_result == \"42\"\n\n    ctx_dict = resumed_handler.ctx.to_dict()  # type: ignore[union-attr]\n    assert json.loads(ctx_dict[\"state\"][\"state_data\"][\"_data\"][\"ask_runs\"]) == 1\n    assert json.loads(ctx_dict[\"state\"][\"state_data\"][\"_data\"][\"complete_runs\"]) == 1\n\n\nclass DummyWorkflowForConcurrentRunsTest(Workflow):\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self._lock = asyncio.Lock()\n        self.num_active_runs = 0\n        self.num_active_runs_history: list[int] = []\n\n    @step\n    async def step_one(self, ev: StartEvent) -> StopEvent:\n        run_num = ev.get(\"run_num\")\n        async with self._lock:\n            self.num_active_runs += 1\n            self.num_active_runs_history.append(self.num_active_runs)\n        await asyncio.sleep(0.01)\n        async with self._lock:\n            self.num_active_runs -= 1\n        return StopEvent(result=f\"Run {run_num}: Done\")\n\n    async def get_active_runs(self) -> list[int]:\n        return self.num_active_runs_history\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\n    (\n        \"workflow_factory\",\n        \"validate_max_concurrent_runs\",\n    ),\n    [\n        (\n            lambda: DummyWorkflowForConcurrentRunsTest(num_concurrent_runs=1),\n            lambda actual_max_concurrent_runs: actual_max_concurrent_runs == 1,\n        ),\n        # This workflow is not protected, and so NumConcurrentRunsException is raised\n        (\n            lambda: DummyWorkflowForConcurrentRunsTest(),\n            lambda actual_max_concurrent_runs: actual_max_concurrent_runs > 1,\n        ),\n    ],\n)\nasync def test_workflow_run_num_concurrent(\n    workflow_factory: Callable[[], DummyWorkflowForConcurrentRunsTest],\n    validate_max_concurrent_runs: Callable[[int], bool],\n) -> None:\n    workflow = workflow_factory()\n    results = await asyncio.gather(*[workflow.run(run_num=ix) for ix in range(1, 5)])\n    max_concurrent_runs = max(workflow.num_active_runs_history)\n    assert validate_max_concurrent_runs(max_concurrent_runs)\n    assert results == [f\"Run {ix}: Done\" for ix in range(1, 5)]\n\n\nclass RandomEvent(Event):\n    pass\n\n\nclass InvalidStopWorkflow(Workflow):\n    @step\n    async def a_step(self, ev: MyStart) -> RandomEvent:\n        return RandomEvent()\n\n\nclass InvalidStartWorkflow(Workflow):\n    @step\n    async def a_step(self, ev: RandomEvent) -> StopEvent:\n        return StopEvent()\n\n\ndef test_wrong_event_types() -> None:\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=\"At least one Event of type StopEvent must be returned by any step.\",\n    ):\n        InvalidStopWorkflow()\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=\"At least one Event of type StartEvent must be received by any step.\",\n    ):\n        InvalidStartWorkflow()\n\n\nclass LockEvent(Event):\n    key: Any\n\n\nclass LockResponseEvent(Event):\n    key: Any\n\n\nclass NonSerializableRequirement:\n    def __init__(self, be_serializable: bool = False) -> None:\n        if be_serializable:\n            self.lock = None\n        else:\n            self.lock = threading.Lock()\n\n    def __eq__(self, other: Any) -> bool:\n        return isinstance(other, NonSerializableRequirement)\n\n\nclass NonSerializableRequirementsWorkflow(Workflow):\n    @step\n    async def wait_step(self, ctx: Context, ev: StartEvent) -> StopEvent:\n        # Create a waiter with a non-picklable requirement value\n        await ctx.wait_for_event(\n            LockResponseEvent,\n            waiter_event=InputRequiredEvent(),\n            waiter_id=\"lock_wait\",\n            requirements={\"key\": NonSerializableRequirement()},\n            timeout=5,\n        )\n        return StopEvent(result=\"Done\")\n\n\n@pytest.mark.asyncio\nasync def test_human_in_the_loop_waiter_with_nonserializable_requirements_pickle_resume() -> (\n    None\n):\n    with pytest.raises(TypeError, match=\"cannot pickle '_thread.lock'\"):\n        pickle.dumps(NonSerializableRequirement())\n\n    workflow = NonSerializableRequirementsWorkflow()\n\n    handler: WorkflowHandler = workflow.run()\n    assert handler.ctx\n\n    ctx_dict: dict[str, Any] | None = None\n\n    async for event in handler.stream_events():\n        if isinstance(event, InputRequiredEvent):\n            ctx_dict = handler.ctx.to_dict(serializer=PickleSerializer())\n            await handler.cancel_run()\n            break\n\n    # Restore and resume\n    new_ctx = Context.from_dict(workflow, ctx_dict or {}, serializer=PickleSerializer())\n    new_handler = workflow.run(ctx=new_ctx)\n    new_handler.ctx.send_event(\n        LockResponseEvent(key=NonSerializableRequirement(be_serializable=True))\n    )\n    final_result = await new_handler\n    assert final_result == \"Done\"\n\n\ndef test__get_start_event_instance(caplog: Any) -> None:\n    class CustomEvent(StartEvent):\n        field: str\n\n    e = CustomEvent(field=\"test\")\n    d = DummyWorkflow()\n    d._start_event_class = CustomEvent\n\n    # Invoke run() passing a legit start event but with additional kwargs\n    with caplog.at_level(logging.WARN):\n        assert d._get_start_event_instance(e, this_will_be_ignored=True) == e\n        assert (\n            \"Keyword arguments are not supported when 'run()' is invoked with the 'start_event' parameter.\"\n            in caplog.text\n        )\n\n    # Old style kwargs passed to the designed StartEvent\n    assert type(d._get_start_event_instance(None, field=\"test\")) is CustomEvent\n\n    # Old style but wrong kwargs passed to the designed StartEvent\n    err = \"Failed creating a start event of type 'CustomEvent' with the keyword arguments: {'wrong_field': 'test'}\"\n    with pytest.raises(WorkflowRuntimeError, match=err):\n        d._get_start_event_instance(None, wrong_field=\"test\")\n\n\ndef test_run_with_invalid_start_event_raises() -> None:\n    \"\"\"Passing wrong type to start_event argument should raise ValueError.\"\"\"\n\n    class SimpleWorkflow(Workflow):\n        @step\n        async def only(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = SimpleWorkflow()\n\n    # Pass a non-StartEvent object\n    with pytest.raises(ValueError, match=\"must be an instance of 'StartEvent'\"):\n        wf.run(start_event=\"not a StartEvent\")  # type: ignore[arg-type]\n\n    # Also test with other invalid types\n    with pytest.raises(ValueError, match=\"must be an instance of 'StartEvent'\"):\n        wf.run(start_event=42)  # type: ignore[arg-type]\n\n    with pytest.raises(ValueError, match=\"must be an instance of 'StartEvent'\"):\n        wf.run(start_event={\"topic\": \"test\"})  # type: ignore[arg-type]\n\n\ndef test__ensure_start_event_class_multiple_types() -> None:\n    class DummyWorkflow(Workflow):\n        @step\n        def one(self, ev: MyStart) -> None:\n            pass\n\n        @step\n        def two(self, ev: StartEvent) -> StopEvent:\n            return StopEvent()\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=\"Only one type of StartEvent is allowed per workflow, found 2\",\n    ):\n        DummyWorkflow()\n\n\ndef test__ensure_stop_event_class_multiple_types() -> None:\n    class DummyWorkflow(Workflow):\n        @step\n        def one(self, ev: MyStart) -> MyStop:\n            return MyStop(outcome=\"nope\")\n\n        @step\n        def two(self, ev: MyStart) -> StopEvent:\n            return StopEvent()\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=\"Only one type of StopEvent is allowed per workflow, found 2\",\n    ):\n        DummyWorkflow()\n\n\n@pytest.mark.asyncio\nasync def test_workflow_validation_steps_cannot_accept_stop_event() -> None:\n    # Test single step that incorrectly accepts StopEvent\n    class InvalidWorkflowSingleStep(Workflow):\n        @step\n        async def start_step(self, ev: StartEvent) -> StopEvent:\n            return StopEvent()\n\n        @step\n        async def bad_step(self, ev: StopEvent) -> StopEvent:\n            return StopEvent()\n\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Step 'bad_step' cannot accept StopEvent. StopEvent signals the end of the workflow. Use a different Event type instead.\",\n    ):\n        await WorkflowTestRunner(InvalidWorkflowSingleStep()).run()\n\n\nclass _OrphanEvent(Event):\n    pass\n\n\ndef test_workflow_validation_consumed_but_never_produced() -> None:\n    class Flow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n        @step\n        async def b(self, ev: _OrphanEvent) -> StopEvent:\n            return StopEvent(result=\"never\")\n\n    with pytest.raises(WorkflowValidationError, match=\"consumed but never produced\"):\n        Flow().validate()\n\n\nclass _CatchErrorMarker(Event):\n    pass\n\n\ndef test_multiple_wildcard_catch_error_handlers_invalid() -> None:\n    class MultiHandlerFlow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n        @catch_error\n        async def h1(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"one\")\n\n        @catch_error\n        async def h2(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"two\")\n\n    with pytest.raises(WorkflowValidationError, match=\"wildcard @catch_error handler\"):\n        MultiHandlerFlow().validate()\n\n\ndef test_catch_error_unknown_step_invalid() -> None:\n    class BadFlow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n        @catch_error(for_steps=[\"nonexistent\"])\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"caught\")\n\n    with pytest.raises(WorkflowValidationError, match=\"unknown step\"):\n        BadFlow().validate()\n\n\ndef test_catch_error_duplicate_scope_invalid() -> None:\n    class BadFlow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def h1(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"one\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def h2(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"two\")\n\n    with pytest.raises(WorkflowValidationError, match=\"claimed by two @catch_error\"):\n        BadFlow().validate()\n\n\ndef test_catch_error_max_recoveries_zero_invalid() -> None:\n    with pytest.raises(WorkflowValidationError, match=\"max_recoveries\"):\n\n        @catch_error(max_recoveries=0)\n        async def bad(self: Any, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent()\n\n\ndef test_catch_error_handler_cannot_cover_another_handler() -> None:\n    class BadFlow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n        @catch_error(for_steps=[\"h2\"])\n        async def h1(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"one\")\n\n        @catch_error(for_steps=[\"a\"])\n        async def h2(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"two\")\n\n    with pytest.raises(\n        WorkflowValidationError, match=\"cannot cover another handler step\"\n    ):\n        BadFlow().validate()\n\n\ndef test_catch_error_max_recoveries_invalid_on_step_config() -> None:\n    class Flow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n        @catch_error\n        async def handler(self, ctx: Context, ev: StepFailedEvent) -> StopEvent:\n            return StopEvent(result=\"caught\")\n\n    wf = Flow()\n    # Simulate post-decoration corruption of the step config.\n    wf._get_steps()[\"handler\"]._step_config.catch_error_max_recoveries = 0\n    with pytest.raises(WorkflowValidationError, match=\"max_recoveries\"):\n        wf.validate()\n\n\ndef test_unknown_skip_graph_check_name_invalid() -> None:\n    class Flow(Workflow):\n        @step\n        async def a(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"ok\")\n\n    with pytest.raises(WorkflowValidationError, match=\"Unknown graph check names\"):\n        Flow(skip_graph_checks={\"not_a_real_check\"})  # type: ignore[arg-type]\n\n\ndef test_get_workflow_events() -> None:\n    class DummyWorkflow(Workflow):\n        @step\n        def one(self, ev: MyStart) -> MyStop:\n            return MyStop(outcome=\"nope\")\n\n    events = DummyWorkflow().events\n    assert len(events) == 2\n    event_names = [e.__name__ for e in events]\n    assert \"MyStop\" in event_names\n    assert \"MyStart\" in event_names\n\n\n@pytest.mark.asyncio\nasync def test_workflow_instances_garbage_collected_after_completion() -> None:\n    # test for memory leaks\n    class TinyWorkflow(Workflow):\n        @step\n        async def only(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    refs: list[weakref.ReferenceType[Workflow]] = []\n\n    for _ in range(10):\n        wf = TinyWorkflow()\n        wf_ref: weakref.ReferenceType[Workflow] = cast(\n            weakref.ReferenceType[Workflow], weakref.ref(wf)\n        )\n        refs.append(wf_ref)\n        await WorkflowTestRunner(wf).run()\n        # Drop strong reference before next iteration\n        del wf\n\n    # Force GC to clear weakly-referenced registry entries\n    for _ in range(3):\n        gc.collect()\n        await asyncio.sleep(0)\n\n    # All weakrefs should be cleared\n    assert all([r() is None for r in refs])\n\n\n@pytest.mark.asyncio\nasync def test_workflow_not_pinned_by_timer_handle_context() -> None:\n    # Regression: TimerHandle._context snapshots the ContextVar holding\n    # RunContext -> Workflow, pinning the workflow until the handle runs\n    # (or forever if the handle re-registers itself).\n    handles: list[asyncio.TimerHandle] = []\n\n    class TinyWorkflow(Workflow):\n        @step\n        async def only(self, ev: StartEvent) -> StopEvent:\n            handles.append(asyncio.get_running_loop().call_later(3600, lambda: None))\n            return StopEvent(result=\"done\")\n\n    refs: list[weakref.ReferenceType[Workflow]] = []\n    try:\n        for _ in range(5):\n            wf = TinyWorkflow()\n            refs.append(cast(weakref.ReferenceType[Workflow], weakref.ref(wf)))\n            await WorkflowTestRunner(wf).run()\n            del wf\n\n        for _ in range(3):\n            gc.collect()\n\n        assert all(r() is None for r in refs), (\n            f\"{sum(r() is not None for r in refs)} workflows pinned by TimerHandle context\"\n        )\n    finally:\n        for h in handles:\n            h.cancel()\n\n\ndef test_workflow_error_no_steps_configured_message() -> None:\n    class Dummy(Workflow):\n        @step\n        def ok(self, ev: StartEvent) -> StopEvent:\n            return StopEvent()\n\n    wf = Dummy()\n\n    # simulate a workflow with no steps at validation time\n    wf._get_steps = lambda: {}  # type: ignore[method-assign]\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=r\"Workflow 'Dummy' has no configured steps\",\n    ):\n        wf._validate()\n\n\ndef test_missing_start_event_error_includes_class_name() -> None:\n    class Dummy(Workflow):\n        @step\n        def ok(self, ev: StartEvent) -> StopEvent:\n            return StopEvent()\n\n        @step\n        def bad(self, ev: Event) -> StopEvent:\n            return StopEvent()\n\n    wf = Dummy()\n    # validate only the 'bad' step to simulate missing StartEvent config\n    only_bad = {\"bad\": wf._get_steps()[\"bad\"]}\n    wf._get_steps = lambda: only_bad  # type: ignore[method-assign]\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=r\"Workflow 'Dummy' has no @step that accepts StartEvent\",\n    ):\n        wf._validate()\n\n\ndef test_missing_stop_event_error_includes_class_name() -> None:\n    class Dummy(Workflow):\n        @step\n        def ok(self, ev: StartEvent) -> StopEvent:\n            return StopEvent()\n\n        @step\n        def bad(self, ev: StartEvent) -> None:\n            return None  # type: ignore[return-value]\n\n    wf = Dummy()\n    # validate only the 'bad' step to simulate missing StopEvent config\n    only_bad = {\"bad\": wf._get_steps()[\"bad\"]}\n    wf._get_steps = lambda: only_bad  # type: ignore[method-assign]\n\n    with pytest.raises(\n        WorkflowConfigurationError,\n        match=r\"Workflow 'Dummy' has no @step that returns StopEvent\",\n    ):\n        wf._validate()\n\n\nclass SomeEvent(StartEvent):\n    _not_serializable: threading.Lock | None = PrivateAttr(default=None)\n\n\n@pytest.mark.asyncio\nasync def test_workflow_non_picklable_event() -> None:\n    step_started = asyncio.Event()\n    step_continued = asyncio.Event()\n\n    class DummyWorkflow(Workflow):\n        @step\n        async def step(self, ev: SomeEvent) -> StopEvent:\n            step_started.set()\n            await step_continued.wait()\n            return StopEvent()\n\n    start_event = SomeEvent()\n    start_event._not_serializable = threading.Lock()\n    wf = DummyWorkflow()\n    handler = wf.run(start_event=start_event)\n    await step_started.wait()\n    handler.ctx.to_dict()\n    step_continued.set()\n    await handler\n\n\n@pytest.mark.asyncio\nasync def test_inner_step_can_access_run_id_and_others_from_instrument_tags() -> None:\n    # container to mutate rather than deal with nonlocal\n    run_id: dict[str, str | None] = {\"run_id\": None, \"foo\": None}\n\n    class TagReadingWorkflow(Workflow):\n        @step\n        async def read_tags(self, ctx: Context, ev: StartEvent) -> StopEvent:\n            tags = active_instrument_tags.get()\n            run_id[\"run_id\"] = tags.get(\"llamaindex.run_id\")\n            run_id[\"foo\"] = tags.get(\"foo\")\n            return StopEvent()\n\n    wf = TagReadingWorkflow()\n    with instrument_tags({\"foo\": \"bar\"}):\n        handler = wf.run()\n    await handler\n\n    assert handler.run_id is not None\n    assert run_id[\"run_id\"] is not None\n    assert run_id[\"run_id\"] == handler.run_id\n    assert run_id[\"foo\"] is not None\n    assert run_id[\"foo\"] == \"bar\"\n\n\nclass Par(Event):\n    id: int\n\n\nclass ParDone(Event):\n    id: int\n\n\n@pytest.mark.asyncio\nasync def test_workflow_parallel_resume() -> None:\n    \"\"\"Test that workflows with parallel workers can be serialized and resumed.\n\n    This test verifies that:\n    1. Events sent via ctx.send_event() are properly captured during serialization\n    2. In-progress workers are properly restored on resume\n    3. The workflow can complete after multiple serialize/resume cycles\n    \"\"\"\n    resume_event = asyncio.Event()\n    all_workers_started = asyncio.Event()\n    worker_count = 0\n\n    class ParallelResumeWorkflow(Workflow):\n        @step\n        async def step1(self, ev: StartEvent, ctx: Context) -> Optional[Par]:  # noqa - python 3.9 struggles here with | None\n            for i in range(4):\n                ctx.send_event(Par(id=i))\n            return None\n\n        @step(num_workers=4)\n        async def par(self, ev: Par) -> ParDone:\n            nonlocal worker_count\n            # Track when workers start waiting (or complete for id=0)\n            worker_count += 1\n            if worker_count >= 4:\n                all_workers_started.set()\n            if ev.id == 0:\n                return ParDone(id=ev.id)\n            # Others wait for resume signal\n            await resume_event.wait()\n            return ParDone(id=ev.id)\n\n        @step\n        async def step3(self, ev: ParDone, ctx: Context) -> Optional[StopEvent]:  # noqa - python 3.9 struggles here with | None\n            if ctx.collect_events(ev, [ParDone] * 4) is None:\n                return None\n            return StopEvent(result=\"Done\")\n\n    wf = ParallelResumeWorkflow(timeout=10)\n\n    # First run: wait until all par workers have started, then serialize\n    handler = wf.run()\n    await all_workers_started.wait()\n    # Small delay to ensure all workers are in BrokerState\n    await asyncio.sleep(0.01)\n    serialized_ctx = handler.ctx.to_dict()\n    try:\n        handler.cancel()\n        await handler.cancel_run()\n    except Exception:\n        pass\n\n    # Second run: resume and complete\n    worker_count = 0\n    all_workers_started.clear()\n    new_handler = wf.run(ctx=Context.from_dict(wf, serialized_ctx))\n    resume_event.set()\n    result = await new_handler\n    assert result == \"Done\"\n\n\nclass OtherEvent(Event):\n    \"\"\"Event written to stream by concurrent step.\"\"\"\n\n    pass\n\n\n@pytest.mark.asyncio\nasync def test_stop_event_cancels_concurrent_step_stream_write() -> None:\n    \"\"\"Test that StopEvent cancels concurrent step before it writes to the event stream.\n\n    This test exposes a regression introduced in 2.14.0:\n    - In 2.13.1: A single asyncio.sleep(0) yield after stop_step returns is\n      sufficient for cancellation to propagate, blocking other_step's write.\n    - In 2.14.0: The yield is not enough - other_step's write still goes through.\n\n    The architectural change from asyncio.Queue to tick_buffer + wait_for_next_task\n    changed the timing characteristics, requiring longer delays for cancellation\n    to take effect.\n\n    To reproduce:\n    1. stop_step returns StopEvent\n    2. Single yield point (asyncio.sleep(0))\n    3. other_step writes to stream\n\n    In 2.13.1: write is blocked (PASS)\n    In 2.14.0: write goes through (FAIL)\n    \"\"\"\n    stop_started = asyncio.Event()\n    other_started = asyncio.Event()\n    stop_proceed = asyncio.Event()\n    write_proceed = asyncio.Event()\n    return_proceed = asyncio.Event()\n\n    class ConcurrentStreamWriteWorkflow(Workflow):\n        @step\n        async def stop_step(self, ev: StartEvent) -> StopEvent:\n            stop_started.set()\n            await stop_proceed.wait()\n            return StopEvent(result=\"done\")\n\n        @step\n        async def other_step(self, ctx: Context, ev: StartEvent) -> None:\n            other_started.set()\n            await write_proceed.wait()\n            ctx.write_event_to_stream(OtherEvent())\n            await return_proceed.wait()\n\n    wf = ConcurrentStreamWriteWorkflow(timeout=5)\n    handler = wf.run()\n\n    # Wait for both steps to start\n    await asyncio.wait_for(stop_started.wait(), timeout=2)\n    await asyncio.wait_for(other_started.wait(), timeout=2)\n\n    # Sequence: stop returns, yield, then write attempts\n    stop_proceed.set()\n    await asyncio.sleep(0)  # Single yield - enough in 2.13.1, not in 2.14.0\n    write_proceed.set()\n    return_proceed.set()\n\n    # Collect events from stream\n    events: list[Event] = []\n    async for event in handler.stream_events():\n        events.append(event)\n        if isinstance(event, StopEvent):\n            break\n\n    await handler\n\n    # In 2.13.1: other_step is cancelled before write, only StopEvent in stream\n    # In 2.14.0: write goes through, OtherEvent appears before StopEvent\n    other_events = [e for e in events if isinstance(e, OtherEvent)]\n    assert len(other_events) == 0, (\n        f\"OtherEvent should not appear in stream - other_step should have been \"\n        f\"cancelled after stop_step returned. Got events: {events}\"\n    )\n\n\n# --- Graph validation integration tests ---\n# Detailed validate_graph() unit tests are in test_graph_validation.py.\n\n\ndef test_graph_validation_unreachable_step_raises() -> None:\n    \"\"\"Integration: unreachable step raises WorkflowValidationError via wf.validate().\"\"\"\n\n    class ProcessedEvent(Event):\n        pass\n\n    class IslandEvent(Event):\n        pass\n\n    class UnreachableStepWorkflow(Workflow):\n        @step\n        async def entry(self, ev: StartEvent) -> ProcessedEvent:\n            return ProcessedEvent()\n\n        @step\n        async def finish(self, ev: ProcessedEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n        @step\n        async def island(self, ev: IslandEvent) -> IslandEvent:\n            return IslandEvent()\n\n    wf = UnreachableStepWorkflow()\n    with pytest.raises(\n        WorkflowValidationError,\n        match=\"Unreachable steps\",\n    ):\n        wf.validate()\n\n\ndef test_graph_validation_accumulates_multiple_errors() -> None:\n    \"\"\"Integration: a single WorkflowValidationError lists all graph problems.\"\"\"\n\n    class _CycleA(Event):\n        pass\n\n    class _CycleB(Event):\n        pass\n\n    class _IslandEvent(Event):\n        pass\n\n    class MultiErrorWorkflow(Workflow):\n        @step\n        async def cycle_entry(self, ev: StartEvent) -> _CycleA | StopEvent:\n            return _CycleA()\n\n        @step\n        async def loop1(self, ev: _CycleA) -> _CycleB:\n            return _CycleB()\n\n        @step\n        async def loop2(self, ev: _CycleB) -> _CycleA:\n            return _CycleA()\n\n        @step\n        async def island(self, ev: _IslandEvent) -> _IslandEvent:\n            return _IslandEvent()\n\n    wf = MultiErrorWorkflow()\n    with pytest.raises(WorkflowValidationError, match=\"Graph validation failed\") as exc:\n        wf.validate()\n    msg = str(exc.value)\n    assert \"[reachability]\" in msg\n    assert \"[dead_end]\" in msg\n\n\n@pytest.mark.asyncio\nasync def test_validation_cached_after_first_run() -> None:\n    \"\"\"Validation result is cached; second run() does not re-run full validation.\"\"\"\n    wf = DummyWorkflow()\n    assert wf._validation_result is None\n    await WorkflowTestRunner(wf).run()\n    assert wf._validation_result is not None\n    first_result = wf._validation_result\n    await WorkflowTestRunner(wf).run()\n    assert wf._validation_result is first_result\n\n\n@pytest.mark.asyncio\nasync def test_validation_cache_invalidated_on_add_step() -> None:\n    \"\"\"Validation cache is invalidated when add_step() registers a new step.\"\"\"\n\n    class AddStepCacheWorkflow(Workflow):\n        @step\n        async def entry(self, ev: StartEvent) -> StopEvent:\n            return StopEvent(result=\"done\")\n\n    wf = AddStepCacheWorkflow()\n    await WorkflowTestRunner(wf).run()\n    assert wf._validation_result is not None\n    old_version = wf._validated_version\n\n    @step(workflow=AddStepCacheWorkflow)\n    async def extra_step(ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"extra\")\n\n    assert AddStepCacheWorkflow._step_functions_version > old_version\n\n    # Next run() must re-validate (cache is stale due to version bump)\n    await WorkflowTestRunner(wf).run()\n    assert wf._validated_version == AddStepCacheWorkflow._step_functions_version\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_workflow_internal_events.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport asyncio\n\nimport pytest\nfrom pydantic import BaseModel\nfrom workflows import Context, Workflow, step\nfrom workflows.events import (\n    Event,\n    StartEvent,\n    StepState,\n    StepStateChanged,\n    StopEvent,\n)\nfrom workflows.testing import WorkflowTestRunner\n\n\nclass SomeEvent(Event):\n    data: str\n\n\nclass ExampleWorkflow(Workflow):\n    @step\n    async def first_step(self, ev: StartEvent, ctx: Context) -> SomeEvent:\n        return SomeEvent(data=ev.message)\n\n    @step\n    async def second_step(self, ev: SomeEvent, ctx: Context) -> StopEvent:\n        return StopEvent(result=ev.data)\n\n\nclass WfState(BaseModel):\n    test: str = \"\"\n\n\nclass ExampleWorkflowState(Workflow):\n    @step\n    async def first_step(self, ev: StartEvent, ctx: Context[WfState]) -> SomeEvent:\n        async with ctx.store.edit_state() as state:\n            state.test = \"Test\"\n        return SomeEvent(data=ev.message)\n\n    @step\n    async def second_step(self, ev: SomeEvent, ctx: Context[WfState]) -> StopEvent:\n        return StopEvent(result=ev.data)\n\n\nclass ExampleWorkflowDictState(Workflow):\n    @step\n    async def first_step(self, ev: StartEvent, ctx: Context) -> SomeEvent:\n        async with ctx.store.edit_state() as state:\n            state.test = \"Test\"\n        return SomeEvent(data=ev.message)\n\n    @step\n    async def second_step(self, ev: SomeEvent, ctx: Context) -> StopEvent:\n        async with ctx.store.edit_state() as state:\n            del state._data[\"test\"]\n        return StopEvent(result=ev.data)\n\n\nclass ExampleWorkflowMultiWorkers(Workflow):\n    def __init__(self) -> None:\n        super().__init__()\n        self._running_event_ids: set[str] = set()\n        self._all_workers_running = asyncio.Event()\n\n    @step\n    async def first_step(self, ev: StartEvent, ctx: Context) -> SomeEvent | None:\n        for i in range(10):\n            ctx.send_event(SomeEvent(data=str(i)))\n        return None\n\n    @step(num_workers=10)\n    async def second_step(self, ev: SomeEvent, ctx: Context) -> StopEvent | None:\n        self._running_event_ids.add(ev.data)\n        if len(self._running_event_ids) == 10:\n            self._all_workers_running.set()\n\n        await self._all_workers_running.wait()\n        if ev.data == \"9\":\n            return StopEvent(result=ev.data)\n        return None\n\n\n@pytest.fixture()\ndef wf() -> ExampleWorkflow:\n    return ExampleWorkflow()\n\n\n@pytest.fixture()\ndef wf_state() -> ExampleWorkflowState:\n    return ExampleWorkflowState()\n\n\n@pytest.fixture()\ndef wf_workers() -> ExampleWorkflowMultiWorkers:\n    return ExampleWorkflowMultiWorkers()\n\n\n@pytest.fixture()\ndef wf_dict_state() -> ExampleWorkflowDictState:\n    return ExampleWorkflowDictState()\n\n\n@pytest.mark.asyncio\nasync def test_internal_events(wf: ExampleWorkflow) -> None:\n    test_runner = WorkflowTestRunner(wf)\n    result = await test_runner.run(\n        start_event=StartEvent(message=\"hello\"),  # type: ignore\n        exclude_events=[StopEvent],\n    )\n    assert len(result.collected) > 0\n    assert all(isinstance(ev, StepStateChanged) for ev in result.collected)\n\n\n@pytest.mark.asyncio\nasync def test_internal_events_sequence(wf_state: ExampleWorkflowState) -> None:\n    test_runner = WorkflowTestRunner(wf_state)\n    result = await test_runner.run(\n        start_event=StartEvent(message=\"hello\"),  # type: ignore\n        exclude_events=[StopEvent],\n    )\n    assert all(isinstance(ev, StepStateChanged) for ev in result.collected)\n    filtered_events = [\n        {\"name\": x.name, \"step_state\": x.step_state} for x in result.collected\n    ]\n    assert filtered_events == [\n        dict(name=\"first_step\", step_state=StepState.RUNNING),\n        dict(name=\"first_step\", step_state=StepState.NOT_RUNNING),\n        dict(name=\"second_step\", step_state=StepState.RUNNING),\n        dict(name=\"second_step\", step_state=StepState.NOT_RUNNING),\n    ]\n\n\n@pytest.mark.asyncio\nasync def test_internal_events_multiple_workers(\n    wf_workers: ExampleWorkflowMultiWorkers,\n) -> None:\n    test_runner = WorkflowTestRunner(wf_workers)\n    result = await test_runner.run(\n        start_event=StartEvent(message=\"hello\"),  # type: ignore\n        exclude_events=[StopEvent],\n    )\n    collected = [ev for ev in result.collected if isinstance(ev, StepStateChanged)]\n    run_ids = [\n        str(r.worker_id) + r.name\n        for r in collected\n        if r.step_state == StepState.RUNNING\n    ]\n    assert len(run_ids) == 11\n    assert (\n        len(set(run_ids)) == 11\n    )  # this check proves that we can differentiate among the same steps when they are run by different workers\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_workflow_naming.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for Workflow.workflow_name property.\"\"\"\n\nfrom workflows import Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\n\nclass SimpleWorkflow(Workflow):\n    @step\n    async def start(self, ev: StartEvent) -> StopEvent:\n        return StopEvent(result=\"done\")\n\n\ndef test_explicit_name() -> None:\n    \"\"\"Workflow with explicit name uses that name.\"\"\"\n    wf = SimpleWorkflow(workflow_name=\"my-custom-name\")\n    assert wf.workflow_name == \"my-custom-name\"\n\n\ndef test_default_name_is_module_qualified() -> None:\n    \"\"\"Workflow without name uses module.qualname.\"\"\"\n    wf = SimpleWorkflow()\n    # Should be tests.test_workflow_naming.SimpleWorkflow or similar\n    assert wf.workflow_name.endswith(\"SimpleWorkflow\")\n    assert \".\" in wf.workflow_name  # Has module prefix\n\n\ndef test_nested_class_qualname() -> None:\n    \"\"\"Nested class workflow has correct qualname.\"\"\"\n\n    class Outer:\n        class InnerWorkflow(Workflow):\n            @step\n            async def start(self, ev: StartEvent) -> StopEvent:\n                return StopEvent(result=\"done\")\n\n    wf = Outer.InnerWorkflow()\n    assert \"Outer.InnerWorkflow\" in wf.workflow_name\n\n\ndef test_function_scoped_workflow_has_locals_in_name() -> None:\n    \"\"\"Function-scoped workflow includes <locals> in name.\"\"\"\n\n    def create_workflow() -> Workflow:\n        class LocalWorkflow(Workflow):\n            @step\n            async def start(self, ev: StartEvent) -> StopEvent:\n                return StopEvent(result=\"done\")\n\n        return LocalWorkflow()\n\n    wf = create_workflow()\n    assert \"<locals>\" in wf.workflow_name\n    assert \"LocalWorkflow\" in wf.workflow_name\n\n\ndef test_explicit_name_overrides_default() -> None:\n    \"\"\"Explicit name takes precedence over computed name.\"\"\"\n\n    def create_workflow() -> Workflow:\n        class LocalWorkflow(Workflow):\n            @step\n            async def start(self, ev: StartEvent) -> StopEvent:\n                return StopEvent(result=\"done\")\n\n        return LocalWorkflow(workflow_name=\"explicit-name\")\n\n    wf = create_workflow()\n    assert wf.workflow_name == \"explicit-name\"\n    assert \"<locals>\" not in wf.workflow_name\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_workflow_postponed_annotations.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport pytest\nfrom workflows.decorators import step\nfrom workflows.events import (\n    StartEvent,\n    StopEvent,\n)\nfrom workflows.testing import WorkflowTestRunner\nfrom workflows.workflow import Workflow\n\nfrom .conftest import OneTestEvent  # type: ignore[import]\n\n\nclass PostponedAnnotationsWorkflow(Workflow):\n    @step\n    async def step1(self, ev: StartEvent) -> OneTestEvent:\n        return OneTestEvent(test_param=\"postponed\")\n\n    @step\n    async def step2(self, ev: OneTestEvent) -> StopEvent:\n        return StopEvent(result=f\"Handled {ev.test_param}\")\n\n\n@pytest.mark.asyncio\nasync def test_workflow_postponed_annotations() -> None:\n    r = await WorkflowTestRunner(PostponedAnnotationsWorkflow()).run()\n    assert r.result == \"Handled postponed\"\n\n\n@pytest.mark.asyncio\nasync def test_workflow_forward_reference() -> None:\n    class ForwardRefWorkflow(Workflow):\n        @step\n        async def step1(self, ev: StartEvent) -> OneTestEvent:\n            return OneTestEvent(test_param=\"forward\")\n\n        @step\n        async def step2(self, ev: OneTestEvent) -> StopEvent:\n            return StopEvent(result=f\"Handled {ev.test_param}\")\n\n    r = await WorkflowTestRunner(ForwardRefWorkflow()).run()\n    assert r.result == \"Handled forward\"\n"
  },
  {
    "path": "packages/llama-index-workflows/tests/test_workflow_typed_state.py",
    "content": "import asyncio\nimport json\n\nimport pytest\nfrom pydantic import BaseModel, Field\nfrom workflows import Context, Workflow\nfrom workflows.decorators import step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom workflows.testing import WorkflowTestRunner\n\n\nclass MyState(BaseModel):\n    name: str = Field(default=\"Jane\")\n    age: int = Field(default=25)\n\n\nclass MyWorkflow(Workflow):\n    @step\n    async def step(self, ctx: Context[MyState], ev: StartEvent) -> StopEvent:\n        # Modify state attributes\n        await ctx.store.set(\"name\", \"John\")\n        await ctx.store.set(\"age\", 30)\n\n        # Get and update entire state\n        state = await ctx.store.get_state()\n        state.age += 1\n        await ctx.store.set_state(state)\n\n        return StopEvent()\n\n\n@pytest.mark.asyncio\nasync def test_typed_state() -> None:\n    test_runner = WorkflowTestRunner(MyWorkflow())\n\n    result = await test_runner.run()\n\n    # Check final state via to_dict()\n    ctx = result.ctx\n    assert ctx is not None\n    ctx_dict = ctx.to_dict()\n    # For typed Pydantic models, state_data is a JSON string with {\"__is_pydantic\": True, \"value\": {...}}\n    state_data = json.loads(ctx_dict[\"state\"][\"state_data\"])\n    assert state_data[\"__is_pydantic\"] is True\n    assert state_data[\"value\"][\"name\"] == \"John\"\n    assert state_data[\"value\"][\"age\"] == 31\n\n\nclass SomeState(BaseModel):\n    val: int = Field(default=0)\n\n\nclass WorkerEvent(Event):\n    pass\n\n\nclass ResultEvent(Event):\n    pass\n\n\nclass GatherEvent(Event):\n    pass\n\n\nclass ParallelWorkflow(Workflow):\n    @step\n    async def init(\n        self, ctx: Context[SomeState], ev: StartEvent\n    ) -> WorkerEvent | GatherEvent:\n        for _ in range(10):\n            ctx.send_event(WorkerEvent())\n\n        return GatherEvent()\n\n    @step\n    async def worker(self, ctx: Context[SomeState], ev: WorkerEvent) -> ResultEvent:\n        async with ctx.store.edit_state() as state:\n            state.val += 1\n            await asyncio.sleep(0.01)\n            if state.val % 2 == 0:\n                state.val -= 1\n\n        return ResultEvent()\n\n    @step\n    async def gather(\n        self, ctx: Context[SomeState], ev: GatherEvent | ResultEvent\n    ) -> StopEvent | None:\n        results = ctx.collect_events(ev, [ResultEvent] * 10)\n        if not results:\n            return None\n\n        state = await ctx.store.get_state()\n        return StopEvent(result=state.val)\n\n\n@pytest.mark.asyncio\nasync def test_typed_state_with_context_manager() -> None:\n    test_runner = WorkflowTestRunner(ParallelWorkflow())\n\n    result = await test_runner.run()\n\n    # Should only be 1 since the context manager locks the state\n    assert result.result == 1\n"
  },
  {
    "path": "packages/llamactl/AGENTS.md",
    "content": "<!--\nSPDX-License-Identifier: MIT\nCopyright (c) 2026 LlamaIndex Inc.\n-->\n\n# llamactl\n\n`ProjectClient` and `ControlPlaneClient` own `httpx.AsyncClient` pools. Do not\nreuse one client instance across separate `asyncio.run(...)` calls; construct a\nfresh client or keep all awaited client work in one event loop.\n"
  },
  {
    "path": "packages/llamactl/CHANGELOG.md",
    "content": "# llamactl\n\n## 0.10.2\n\n### Patch Changes\n\n- 06ff626: Fix `projects use <id>` and `projects get <id>` failing with \"not found\" for projects outside the default org\n- abc176e: Only auto-push push-mode updates from repos with the deployment remote configured.\n\n## 0.10.1\n\n### Patch Changes\n\n- 532a6fa: Fix inline auth recovery while saving deployments.\n\n## 0.10.0\n\n### Minor Changes\n\n- 1452c18: Restructure auth-related commands into top-level resource groups\n- b70ace8: Add `llamactl deployments apply -f` and `delete -f` for declarative deployment management\n- 474b9ee: Replace simple-term-menu with blessed, add type-to-filter picker, handle non-interactive sessions gracefully\n- 0b9d6c2: Replace deployment create and edit forms with editor-backed YAML flows\n\n### Patch Changes\n\n- c3fac21: Validate `appserver_version` as a public PEP 440 version\n- c3fac21: Add `--annotate-on-error` to `llamactl deployments apply -f`\n- 865baba: Polish llamactl error handling, status output, and deployment update flags\n- fa2136f: Add `llamactl deployments template` and `deployments get -o template` to be used as templates to support `llamactl deployments apply`\n- 87ef930: Allow llamactl to authenticate with LlamaCloud environment variables\n- Updated dependencies [c3fac21]\n  - llama-agents-core@0.10.2\n  - llama-agents-appserver@0.11.4\n\n## 0.9.1\n\n### Patch Changes\n\n- 0b2098b: Fix `deployments update` crashing with `Event loop is closed` after a transient failure on the internal git push. The command now runs `get_deployment` and `update_deployment` in a single event loop instead of reusing the same `ProjectClient` across two `asyncio.run` calls.\n\n## 0.9.0\n\n### Minor Changes\n\n- 491e2d2: `auth list`, `auth env list`, and `auth organizations` now support `-o text|json|yaml|wide`. Plain-text tables replace the Rich-styled tables; JSON/YAML output round-trips. `auth list` no longer leaks `api_key` or OIDC tokens — `auth_type` reports `token`/`oidc`/`none`.\n- ec88970: `deployments get` now shows one deployment with a name and lists all of them without; adds `-o text|json|yaml|wide`, `--project`, and a new `deployments logs` command.\n- 9cee98d: `deployments history` now supports `-o text|json|yaml|wide` and `--project <id>`. Text output uses 7-char short SHAs and Z-suffixed UTC timestamps; JSON keeps full SHAs. `deployments rollback --git-sha` now offers shell completion from the deployment's history.\n\n### Patch Changes\n\n- 0c6afcd: Editing a push-mode (Local repo) deployment now pushes local code before calling update, so switching branches or saving new commits works on the first try and the server resolves git_ref to the actual latest SHA.\n- 91516a5: `llamactl auth login` now prints a friendly hint pointing to `llamactl auth token` when the server has no OIDC browser-login configured, instead of dumping a raw 400 from the discovery endpoint.\n- Updated dependencies [463c79d]\n  - llama-agents-core@0.10.1\n  - llama-agents-appserver@0.11.3\n\n## 0.8.0\n\n### Minor Changes\n\n- 2280e04: Rename deployment field `llama_deploy_version` to `appserver_version`. The old name remains as a deprecated input/output alias so existing clients and servers keep working.\n\n### Patch Changes\n\n- Updated dependencies [2280e04]\n  - llama-agents-core@0.10.0\n  - llama-agents-appserver@0.11.2\n\n## 0.7.3\n\n### Patch Changes\n\n- e75a15d: Revert previous changes, `llamactl serve` now re-exports frontend API keys with public prefixes once again since this is necessary for local dev auth to work.\n\n## 0.7.2\n\n### Patch Changes\n\n- Updated dependencies [916b157]\n  - llama-agents-appserver@0.11.1\n\n## 0.7.1\n\n### Patch Changes\n\n- facbac4: `PUBLIC_*` env var overlay for UI builds: `PUBLIC_X` overrides `X` in the build env so backend and frontend can use different URLs for the same service. Removes dead `VITE_`/`NEXT_PUBLIC_` injection from `llamactl serve`. Helm network policy gains `extraEgressRules`, DNS selector overrides, and `blockPrivateRanges` toggle.\n- Updated dependencies [facbac4]\n  - llama-agents-appserver@0.11.0\n\n## 0.7.0\n\n### Minor Changes\n\n- e8b8f47: feat: add support for organizations\n- e08c17c: Add shell tab-completion support with `llamactl completion generate` and `llamactl completion install`\n\n### Patch Changes\n\n- Updated dependencies [e8b8f47]\n  - llama-agents-core@0.9.0\n  - llama-agents-appserver@0.10.5\n\n## 0.6.9\n\n### Patch Changes\n\n- 7ad3049: Reduce full clones from github for config, repo validation, and sha discovery. Reduce dependencies on system git, preferring dulwich\n- Updated dependencies [7ad3049]\n  - llama-agents-appserver@0.10.4\n  - llama-agents-core@0.8.5\n\n## 0.6.8\n\n### Patch Changes\n\n- Updated dependencies [286c91a]\n  - llama-agents-appserver@0.10.3\n\n## 0.6.7\n\n### Patch Changes\n\n- 740ee9e: Add a grace window to build artifact GC (configurable via `BUILD_ARTIFACT_GC_GRACE_SECONDS`, default 75m) and parallelize its delete loop with bounded concurrency. `llamactl auth`'s non-idempotent key-creation POST now only retries on connect-phase errors (`ConnectError`, `ConnectTimeout`, `PoolTimeout`) so initial-connectivity blips are absorbed without risking duplicate keys from a read-timeout retry.\n\n## 0.6.6\n\n### Patch Changes\n\n- Updated dependencies [f27d98f]\n  - llama-agents-core@0.8.4\n  - llama-agents-appserver@0.10.2\n\n## 0.6.5\n\n### Patch Changes\n\n- Updated dependencies [3f12660]\n  - llama-agents-core@0.8.3\n  - llama-agents-appserver@0.10.1\n\n## 0.6.4\n\n### Patch Changes\n\n- Updated dependencies [3e2e7b8]\n  - llama-agents-appserver@0.10.0\n\n## 0.6.3\n\n### Patch Changes\n\n- 46f2675: security patches\n- Updated dependencies [46f2675]\n  - llama-agents-core@0.8.2\n  - llama-agents-appserver@0.9.1\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [58e7942]\n  - llama-agents-appserver@0.9.0\n  - llama-agents-core@0.8.1\n\n## 0.6.1\n\n### Patch Changes\n\n- 68b1ec5: Use sqlite in agentcore, add local mode\n\n## 0.6.0\n\n### Minor Changes\n\n- e2f3abd: Rename deployment name to display_name, add optional explicit id on create\n\n### Patch Changes\n\n- Updated dependencies [e2f3abd]\n  - llama-agents-core@0.8.0\n  - llama-agents-appserver@0.8.1\n\n## 0.5.3\n\n### Patch Changes\n\n- llama-agents-appserver@0.8.0\n\n## 0.5.2\n\n### Patch Changes\n\n- llama-agents-appserver@0.7.2\n\n## 0.5.1\n\n### Patch Changes\n\n- Updated dependencies [7bb9a90]\n  - llama-agents-core@0.7.0\n  - llama-agents-appserver@0.7.1\n\n## 0.5.0\n\n### Minor Changes\n\n- 9641415: Add dulwich-based git serving for internal repos. Users can push code via `llamactl push` and build pods clone via the build API. Bare repos are stored as tarballs in S3.\n\n### Patch Changes\n\n- llama-agents-appserver@0.7.0\n\n## 0.4.26\n\n### Patch Changes\n\n- llama-agents-appserver@0.6.5\n\n## 0.4.25\n\n### Patch Changes\n\n- llama-agents-appserver@0.6.4\n\n## 0.4.24\n\n### Patch Changes\n\n- a15f1b4: Rename `llama_index_docs` MCP server identifier to `llama-index-docs` in scaffold config files\n- Updated dependencies [4127101]\n- Updated dependencies [1594315]\n  - llama-agents-appserver@0.6.3\n\n## 0.4.23\n\n### Patch Changes\n\n- 508b5da: Fix deployment update, fix github user auth\n- Updated dependencies [508b5da]\n  - llama-agents-core@0.6.2\n  - llama-agents-appserver@0.6.2\n\n## 0.4.22\n\n### Patch Changes\n\n- 32283aa: Replace async doc fetching with MCP server config generation\n- Updated dependencies [1b86f90]\n  - llama-agents-core@0.6.1\n  - llama-agents-appserver@0.6.1\n\n## 0.4.21\n\n### Patch Changes\n\n- Updated dependencies [4ab011f]\n  - llama-agents-core@0.6.0\n  - llama-agents-appserver@0.6.0\n\n## 0.4.20\n\n### Patch Changes\n\n- Updated dependencies [eee29c1]\n  - llama-deploy-appserver@0.5.3\n\n## 0.4.19\n\n### Patch Changes\n\n- Updated dependencies [e11ad55]\n  - llama-deploy-appserver@0.5.2\n\n## 0.4.18\n\n### Patch Changes\n\n- llama-deploy-appserver@0.5.1\n\n## 0.4.17\n\n### Patch Changes\n\n- 5588b7e: Bump to be compatible with latest appserver\n\n## 0.4.16\n\n### Patch Changes\n\n- Updated dependencies [ac74af4]\n- Updated dependencies [4ba0d9d]\n  - llama-deploy-appserver@0.5.0\n  - llama-deploy-core@0.5.0\n"
  },
  {
    "path": "packages/llamactl/README.md",
    "content": "# llamactl\n\n`llamactl` is the CLI for developing LlamaAgents apps locally and managing their LlamaCloud deployments.\n\nFor the full guide, see the [LlamaAgents `llamactl` docs](https://developers.llamaindex.ai/python/llamaagents/llamactl/getting-started/).\n\n## Installation\n\nInstall globally with `uv`:\n\n```bash\nuv tool install -U llamactl\n```\n\nOr pin it to a project:\n\n```bash\nuv add --dev llamactl\n```\n\n## Quick Start\n\nCreate or select an auth profile:\n\n```bash\nllamactl auth login\n```\n\nIf browser login is not available, use an API key:\n\n```bash\nllamactl auth token --api-key \"$LLAMA_CLOUD_API_KEY\" --project \"$LLAMA_AGENTS_PROJECT_ID\"\n```\n\nScaffold and run an app:\n\n```bash\nllamactl init\ncd my-app\nllamactl serve\n```\n\nCreate a cloud deployment:\n\n```bash\nllamactl deployments create\n```\n\nInspect it and stream logs:\n\n```bash\nllamactl deployments get\nllamactl deployments get NAME\nllamactl deployments logs NAME --follow\n```\n\nFor declarative deployments:\n\n```bash\nllamactl deployments template > deployment.yaml\nllamactl deployments apply -f deployment.yaml\n```\n\n## Command Groups\n\n- `llamactl auth`: log in, create API-key profiles, and switch profiles.\n- `llamactl environments`: list, add, inspect, and switch LlamaCloud API environments.\n- `llamactl projects`: list and select projects.\n- `llamactl organizations`: list organizations.\n- `llamactl deployments`: create, apply, edit, update, inspect, delete, roll back, and stream deployment logs.\n- `llamactl init`: create a new LlamaAgents project from a starter template.\n- `llamactl serve`: run the local app server and optional frontend dev server.\n- `llamactl pkg`: generate container build files for self-hosted deployments.\n- `llamactl completion`: generate or install shell completions.\n- `llamactl agentcore`: run or export AgentCore apps.\n\n## Configuration\n\n`llamactl auth login` and `llamactl auth token` create local auth profiles. A profile stores the active API environment, project, and credential used by deployment commands.\n\nFor CI and other non-interactive environments, set env vars instead of using a profile:\n\n```bash\nexport LLAMA_CLOUD_API_KEY=\"llx-...\"\nexport LLAMA_AGENTS_PROJECT_ID=\"project-id\"\n```\n\n`LLAMA_CLOUD_BASE_URL` can point the CLI at a non-default environment. When both `LLAMA_CLOUD_API_KEY` and `LLAMA_AGENTS_PROJECT_ID` are set, env var auth takes precedence over the stored profile for cloud commands. Many commands also accept `--project` to override the active project for that invocation.\n\n## Shell Completion\n\nInstall completions for your current shell:\n\n```bash\nllamactl completion install\n```\n\nOr print a completion script:\n\n```bash\nllamactl completion generate zsh\n```\n\n## Requirements\n\n- Python 3.12+\n- `uv` for project dependency management\n- `git` for cloud deployments\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/llamactl/package.json",
    "content": "{\n  \"name\": \"llamactl\",\n  \"version\": \"0.10.2\",\n  \"private\": false,\n  \"dependencies\": {\n    \"llama-agents-core\": \"workspace:*\",\n    \"llama-agents-appserver\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/llamactl/pyproject.toml",
    "content": "[build-system]\nrequires = [\"uv_build>=0.7.20,<0.8.0\"]\nbuild-backend = \"uv_build\"\n\n[dependency-groups]\ndev = [\n  \"pytest>=8.3.4\",\n  \"pytest-asyncio>=0.25.3\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"respx>=0.22.0\",\n  \"ty>=0.0.15\",\n  \"ruff>=0.12.9\"\n]\n\n[project]\nname = \"llamactl\"\nversion = \"0.10.2\"\ndescription = \"A command-line interface for managing LlamaDeploy projects and deployments\"\nreadme = \"README.md\"\nlicense = {text = \"MIT\"}\nauthors = [\n  {name = \"Adrian Lyjak\", email = \"adrianlyjak@gmail.com\"}\n]\nrequires-python = \">=3.10, <4\"\ndependencies = [\n  \"llama-agents-core[client]>=0.5.0\",\n  \"llama-agents-appserver>=0.5.0\",\n  \"llama-agents-agentcore>=0.5.0\",\n  \"rich>=13.0.0\",\n  \"blessed>=1.20; sys_platform != 'win32'\",\n  \"click>=8.2.1\",\n  \"python-dotenv>=1.0.0\",\n  \"tenacity>=9.1.2\",\n  \"aiohttp>=3.12.14\",\n  \"copier>=9.10.2\",\n  \"pyjwt[crypto]>=2.10.1\",\n  \"pydantic-settings>=2.10.1\",\n  \"typing-extensions>=4.15.0\",\n  \"typing-extensions>=4.15.0 ; python_full_version < '3.11'\"\n]\n\n[project.scripts]\nllamactl = \"llama_agents.cli:main\"\n\n[tool.uv.build-backend]\nmodule-name = [\"llama_agents.cli\", \"llama_deploy.cli\"]\nnamespace = true\n\n[tool.uv.sources]\nllama-agents-appserver = {workspace = true}\nllama-agents-core = {workspace = true}\nllama-agents-agentcore = {workspace = true}\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/__init__.py",
    "content": "import warnings\n\nimport llama_agents.cli.commands.agentcore  # noqa: F401\nimport llama_agents.cli.commands.auth  # noqa: F401\nimport llama_agents.cli.commands.completion  # noqa: F401\nimport llama_agents.cli.commands.config  # noqa: F401\nimport llama_agents.cli.commands.deployment  # noqa: F401\nimport llama_agents.cli.commands.dev  # noqa: F401\nimport llama_agents.cli.commands.environments  # noqa: F401\nimport llama_agents.cli.commands.init  # noqa: F401\nimport llama_agents.cli.commands.organizations  # noqa: F401\nimport llama_agents.cli.commands.pkg  # noqa: F401\nimport llama_agents.cli.commands.projects  # noqa: F401\nimport llama_agents.cli.commands.serve  # noqa: F401\n\nfrom .app import app\n\n# Disable warnings in llamactl CLI, and specifically silence the Pydantic\n# UnsupportedFieldAttributeWarning about `validate_default` on Field().\nwarnings.simplefilter(\"ignore\")\nwarnings.filterwarnings(\n    \"ignore\",\n    message=r\"The 'validate_default' attribute .* has no effect.*\",\n)\n\n\n# Main entry point function (called by the script)\ndef main() -> None:\n    app()\n\n\n__all__ = [\"app\"]\n\n\nif __name__ == \"__main__\":\n    app()\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/app.py",
    "content": "from importlib.metadata import PackageNotFoundError\nfrom importlib.metadata import version as pkg_version\nfrom typing import Any\n\nimport click\nfrom llama_agents.cli.commands.aliased_group import AliasedGroup\nfrom llama_agents.cli.options import global_options\n\n\ndef print_version(ctx: click.Context, param: click.Parameter, value: Any) -> None:\n    \"\"\"Print the version of llama_deploy\"\"\"\n\n    from llama_agents.cli.config.env_service import service\n\n    if not value or ctx.resilient_parsing:\n        return None\n    try:\n        ver = pkg_version(\"llamactl\")\n        click.echo(f\"client version: {ver}\")\n\n        # If there is an active profile, attempt to query server version\n        auth_service = service.current_auth_service()\n        if auth_service:\n            try:\n                data = auth_service.fetch_server_version()\n                server_ver = data.version\n                click.echo(f\"server version: {server_ver or 'unknown'}\")\n            except Exception as e:\n                click.echo(f\"server version: unavailable - {e}\")\n    except PackageNotFoundError:\n        raise click.ClickException(\"Package 'llamactl' not found\")\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n    ctx.exit()\n\n\n# Main CLI application\n@click.group(help=\"Create, develop, and deploy LlamaDeploy apps.\", cls=AliasedGroup)\n@click.option(\n    \"--version\",\n    is_flag=True,\n    callback=print_version,\n    expose_value=False,\n    is_eager=True,\n    help=\"Print client and server versions of LlamaDeploy\",\n)\n@global_options\ndef app() -> None:\n    pass\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/apply_yaml.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"YAML parsing for ``llamactl deployments apply -f``.\n\nPure parsing + validation — no network, no client calls.  Takes YAML text,\nresolves ``${VAR}`` environment variables, strips ``********`` mask sentinels,\nand validates against :class:`DeploymentDisplay`.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nfrom dataclasses import dataclass\nfrom typing import Any\n\nimport yaml\nfrom llama_agents.cli.display import DeploymentDisplay, DeploymentSpec\nfrom pydantic import ValidationError\nfrom yaml.nodes import MappingNode\n\n_ENV_VAR_RE = re.compile(r\"\\$\\{([^}]+)\\}\")\n_ANNOTATION_RE = re.compile(r\"^\\s*## ERROR:\")\n\n# Spec-level field names that appear both in the apply YAML and on the wire\n# (DeploymentCreate / DeploymentUpdate).  Used by the YAML line-index and by\n# the wire-to-YAML path mapper.  Keep this in sync with DeploymentSpec.\nSPEC_FIELDS = {\n    \"repo_url\",\n    \"deployment_file_path\",\n    \"git_ref\",\n    \"appserver_version\",\n    \"suspended\",\n    \"personal_access_token\",\n}\n\n\n@dataclass(frozen=True)\nclass FieldError:\n    path: tuple[str | int, ...]\n    message: str\n\n\n@dataclass(frozen=True)\nclass UnresolvedEnvVar:\n    name: str\n    path: tuple[str | int, ...]\n\n\nclass ApplyYamlError(Exception):\n    \"\"\"Base error for YAML apply parsing/validation failures.\"\"\"\n\n    def __init__(\n        self,\n        message: str,\n        *,\n        errors: list[FieldError] | None = None,\n        original_error: Exception | None = None,\n    ) -> None:\n        self.errors = errors or [FieldError((), message)]\n        self.original_error = original_error\n        super().__init__(message)\n\n\nclass UnresolvedEnvVarsError(ApplyYamlError):\n    \"\"\"Raised when ``${VAR}`` references cannot be resolved.\"\"\"\n\n    def __init__(self, unresolved: list[UnresolvedEnvVar]) -> None:\n        self.unresolved = sorted({item.name for item in unresolved})\n        message = f\"unresolved environment variables: {', '.join(self.unresolved)}\"\n        errors = [\n            FieldError(\n                path=path,\n                message=f\"unresolved environment variables: {', '.join(names)}\",\n            )\n            for path, names in _group_unresolved_by_path(unresolved).items()\n        ]\n        super().__init__(message, errors=errors)\n\n\n_ENV_STRING_SPEC_FIELDS = (\n    \"repo_url\",\n    \"deployment_file_path\",\n    \"git_ref\",\n    \"appserver_version\",\n    \"personal_access_token\",\n)\n\n\ndef _group_unresolved_by_path(\n    unresolved: list[UnresolvedEnvVar],\n) -> dict[tuple[str | int, ...], list[str]]:\n    grouped: dict[tuple[str | int, ...], set[str]] = {}\n    for item in unresolved:\n        grouped.setdefault(item.path, set()).add(item.name)\n    return {path: sorted(names) for path, names in grouped.items()}\n\n\ndef _resolve_string(\n    text: str,\n    unresolved: list[UnresolvedEnvVar],\n    path: tuple[str | int, ...],\n) -> str:\n    def _replacer(match: re.Match[str]) -> str:\n        var = match.group(1)\n        env_val = os.environ.get(var)\n        if env_val is None:\n            unresolved.append(UnresolvedEnvVar(name=var, path=path))\n            return match.group(0)  # leave ${VAR} as-is\n        return env_val\n\n    return _ENV_VAR_RE.sub(_replacer, text)\n\n\ndef _resolve_spec_env_vars(spec: DeploymentSpec) -> DeploymentSpec:\n    unresolved: list[UnresolvedEnvVar] = []\n    updates: dict[str, Any] = {}\n\n    for field in _ENV_STRING_SPEC_FIELDS:\n        value = getattr(spec, field)\n        if isinstance(value, str):\n            updates[field] = _resolve_string(value, unresolved, (\"spec\", field))\n\n    if spec.secrets is not None:\n        updates[\"secrets\"] = {\n            name: _resolve_string(value, unresolved, (\"spec\", \"secrets\", name))\n            if isinstance(value, str)\n            else value\n            for name, value in spec.secrets.items()\n        }\n\n    if unresolved:\n        raise UnresolvedEnvVarsError(unresolved)\n    return spec.model_copy(update=updates)\n\n\ndef _load_yaml_mapping(text: str) -> dict[str, Any]:\n    try:\n        raw = yaml.safe_load(text)\n    except yaml.YAMLError as exc:\n        raise ApplyYamlError(f\"invalid YAML: {exc}\", original_error=exc) from exc\n\n    if not isinstance(raw, dict):\n        raise ApplyYamlError(\n            f\"expected a YAML mapping at the top level, got {type(raw).__name__}\"\n        )\n    return raw\n\n\ndef _strip_existing_annotations(text: str) -> str:\n    lines = text.splitlines(keepends=True)\n    return \"\".join(line for line in lines if not _ANNOTATION_RE.match(line))\n\n\ndef _path_label(path: tuple[str | int, ...]) -> str:\n    return \".\".join(str(part) for part in path)\n\n\ndef _annotation_text(\n    error: FieldError, *, include_path: bool = False, indent: str = \"\"\n) -> str:\n    message = error.message\n    if include_path and error.path:\n        message = f\"{_path_label(error.path)}: {message}\"\n    lines = message.splitlines() or [\"\"]\n    rendered = [f\"{indent}## ERROR: {lines[0]}\\n\"]\n    rendered.extend(f\"{indent}## ERROR: {line}\\n\" for line in lines[1:])\n    return \"\".join(rendered)\n\n\ndef _key_insert_line(lines: list[str], key_line: int) -> int:\n    indent = len(lines[key_line]) - len(lines[key_line].lstrip(\" \"))\n    current = key_line\n    while current > 0:\n        previous = lines[current - 1]\n        previous_indent = len(previous) - len(previous.lstrip(\" \"))\n        if previous_indent != indent or not previous.lstrip(\" \").startswith(\"## \"):\n            break\n        current -= 1\n    return current\n\n\ndef _index_mapping_node(\n    node: MappingNode,\n    *,\n    path: tuple[str | int, ...] = (),\n    index: dict[tuple[str | int, ...], int],\n) -> None:\n    for key_node, value_node in node.value:\n        if not hasattr(key_node, \"value\"):\n            continue\n        key = key_node.value\n        child_path = (*path, key)\n\n        if child_path in {(\"name\",), (\"generate_name\",)}:\n            index[child_path] = key_node.start_mark.line\n        elif len(child_path) == 2 and child_path[0] == \"spec\":\n            if key in SPEC_FIELDS or key == \"secrets\":\n                index[child_path] = key_node.start_mark.line\n        elif len(child_path) == 3 and child_path[:2] == (\"spec\", \"secrets\"):\n            index[child_path] = key_node.start_mark.line\n\n        if isinstance(value_node, MappingNode):\n            _index_mapping_node(value_node, path=child_path, index=index)\n\n\ndef _index_apply_paths(text: str) -> dict[tuple[str | int, ...], int] | None:\n    try:\n        root = yaml.compose(text)\n    except yaml.YAMLError:\n        return None\n    if not isinstance(root, MappingNode):\n        return None\n\n    index: dict[tuple[str | int, ...], int] = {}\n    _index_mapping_node(root, index=index)\n    return index\n\n\ndef annotate_yaml_with_errors(text: str, errors: list[FieldError]) -> str:\n    stripped = _strip_existing_annotations(text)\n    if not errors:\n        return stripped\n\n    lines = stripped.splitlines(keepends=True)\n    index = _index_apply_paths(stripped)\n    if index is None:\n        return (\n            \"\".join(_annotation_text(error, include_path=True) for error in errors)\n            + stripped\n        )\n\n    file_errors: list[FieldError] = []\n    grouped: dict[int, list[FieldError]] = {}\n    for error in errors:\n        key_line = index.get(error.path)\n        if key_line is None:\n            file_errors.append(error)\n            continue\n        grouped.setdefault(_key_insert_line(lines, key_line), []).append(error)\n\n    output: list[str] = []\n    output.extend(_annotation_text(error, include_path=True) for error in file_errors)\n    for line_no, line in enumerate(lines):\n        for error in grouped.get(line_no, []):\n            indent = len(line) - len(line.lstrip(\" \"))\n            output.append(_annotation_text(error, indent=\" \" * indent))\n        output.append(line)\n    return \"\".join(output)\n\n\n# ---------------------------------------------------------------------------\n# Main parse entry point\n# ---------------------------------------------------------------------------\n\n\ndef parse_apply_yaml(text: str) -> DeploymentDisplay:\n    \"\"\"Parse YAML apply input into a validated :class:`DeploymentDisplay`.\n\n    1. ``yaml.safe_load`` → dict.\n    2. Drop ``status`` (round-trip artifact from ``get -o yaml``).\n    3. Validate against :class:`DeploymentDisplay` (pydantic handles\n       ``extra=\"forbid\"`` rejection for typos / excluded fields).\n    4. Resolve ``${VAR}`` env vars in typed string fields under ``spec``.\n    5. Strip ``********`` mask sentinels from ``spec.secrets`` and\n       ``spec.personal_access_token``.\n    6. Wrap :class:`~pydantic.ValidationError` into :class:`ApplyYamlError`.\n    \"\"\"\n    raw = _load_yaml_mapping(text)\n\n    # Drop read-only status block.\n    raw.pop(\"status\", None)\n\n    try:\n        display = DeploymentDisplay.model_validate(raw)\n    except ValidationError as exc:\n        errors = [\n            FieldError(\n                path=tuple(\n                    part for part in error[\"loc\"] if isinstance(part, (str, int))\n                ),\n                message=str(error[\"msg\"]),\n            )\n            for error in exc.errors()\n        ]\n        raise ApplyYamlError(str(exc), errors=errors, original_error=exc) from exc\n\n    display = display.model_copy(update={\"spec\": _resolve_spec_env_vars(display.spec)})\n    return display.without_mask_sentinels()\n\n\n# ---------------------------------------------------------------------------\n# Lightweight delete helper\n# ---------------------------------------------------------------------------\n\n\ndef parse_delete_yaml_name(text: str) -> str:\n    \"\"\"Extract the ``name`` field from YAML for a delete operation.\n\n    No env resolution, no model validation — just pull the top-level\n    ``name`` string.\n    \"\"\"\n    raw = _load_yaml_mapping(text)\n\n    name = raw.get(\"name\")\n    if name is None:\n        raise ApplyYamlError(\"missing required field: name\")\n    if not isinstance(name, str):\n        raise ApplyYamlError(f\"name must be a string, got {type(name).__name__}\")\n    return name\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/auth/client.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom types import TracebackType\nfrom typing import (\n    Any,\n    AsyncContextManager,\n    AsyncGenerator,\n    Awaitable,\n    Callable,\n)\n\nimport httpx\nimport jwt\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey\nfrom jwt.algorithms import RSAAlgorithm\nfrom llama_agents.cli.config.schema import DeviceOIDC\nfrom llama_agents.core.client.ssl_util import get_httpx_verify_param\nfrom pydantic import BaseModel\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing_extensions import Self\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass OIDCNotEnabledError(Exception):\n    \"\"\"Raised when the server does not have OIDC browser-login configured.\"\"\"\n\n\nclass OidcDiscoveryResponse(BaseModel):\n    discovery_url: str\n    client_ids: dict[str, str] | None = None\n\n\nclass OidcProviderConfiguration(BaseModel):\n    device_authorization_endpoint: str | None = None\n    token_endpoint: str | None = None\n    scopes_supported: list[str] | None = None\n    jwks_uri: str | None = None\n\n\nclass JsonWebKey(BaseModel):\n    kty: str\n    kid: str | None = None\n    use: str | None = None\n    alg: str | None = None\n    n: str | None = None\n    e: str | None = None\n    x5c: list[str] | None = None\n    x5t: str | None = None\n    x5t_s256: str | None = None\n\n\nclass JsonWebKeySet(BaseModel):\n    keys: list[JsonWebKey]\n\n\nclass AuthMeResponse(BaseModel):\n    id: str\n    email: str | None = None\n    last_login_provider: str | None = None\n    name: str | None = None\n    first_name: str | None = None\n    last_name: str | None = None\n    claims: dict[str, Any] | None = None\n    restrict: Any | None = None\n    created_at: str | None = None\n\n\nclass ClientContextManager(AsyncContextManager):\n    def __init__(self, base_url: str | None, auth: httpx.Auth | None = None) -> None:\n        self.base_url = base_url.rstrip(\"/\") if base_url else None\n        verify = get_httpx_verify_param()\n        if self.base_url:\n            self.client = httpx.AsyncClient(\n                base_url=self.base_url, auth=auth, verify=verify\n            )\n        else:\n            self.client = httpx.AsyncClient(auth=auth, verify=verify)\n\n    async def close(self) -> None:\n        try:\n            await self.client.aclose()\n        except Exception:\n            pass\n\n    async def __aenter__(self) -> Self:\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> None:\n        await self.close()\n\n\ndef _extract_detail(resp: httpx.Response) -> str | None:\n    \"\"\"Best-effort extract of a FastAPI-style ``detail`` string from a response.\"\"\"\n    try:\n        payload = resp.json()\n    except Exception:\n        return None\n    if isinstance(payload, dict):\n        detail = payload.get(\"detail\")\n        if isinstance(detail, str):\n            return detail\n    return None\n\n\nclass PlatformAuthDiscoveryClient(ClientContextManager):\n    \"\"\"Client for ad hoc auth endpoints under /api/v1/auth.\"\"\"\n\n    def __init__(self, base_url: str) -> None:\n        super().__init__(base_url)\n\n    async def oidc_discovery(self) -> OidcDiscoveryResponse:\n        resp = await self.client.get(\"/api/v1/auth/oidc/discovery\", timeout=10.0)\n        if resp.status_code in (400, 404, 501):\n            # Server reachable but OIDC discovery isn't configured. Surface a\n            # typed error so callers can suggest the API key flow instead of\n            # leaking a raw HTTP status.\n            detail = _extract_detail(resp)\n            raise OIDCNotEnabledError(\n                detail or \"Server does not have OIDC browser login enabled\"\n            )\n        resp.raise_for_status()\n        return OidcDiscoveryResponse.model_validate(resp.json())\n\n\nclass APIToken(BaseModel):\n    token: str\n    id: str\n\n\nclass PlatformAuthClient(ClientContextManager):\n    \"\"\"Client for user introspection under /api/v1/auth/me.\"\"\"\n\n    def __init__(\n        self, base_url: str, id_token: str | None = None, auth: httpx.Auth | None = None\n    ) -> None:\n        self.id_token = id_token\n        super().__init__(base_url, auth=auth)\n\n    async def me(self) -> AuthMeResponse:\n        headers = (\n            {\"Authorization\": f\"Bearer {self.id_token}\"} if self.id_token else None\n        )\n        resp = await self.client.get(\"/api/v1/auth/me\", headers=headers, timeout=10.0)\n        resp.raise_for_status()\n        return AuthMeResponse.model_validate(resp.json())\n\n    async def create_agent_api_key(self, name: str) -> APIToken:\n        resp = await self.client.post(\n            \"/api/v1/api-keys\",\n            json={\"name\": name, \"project_id\": None},\n        )\n        resp.raise_for_status()\n        json = resp.json()\n        token = json[\"redacted_api_key\"]\n        id = json[\"id\"]\n        return APIToken(token=token, id=id)\n\n    async def delete_api_key(self, id: str) -> None:\n        response = await self.client.delete(f\"/api/v1/api-keys/{id}\")\n        response.raise_for_status()\n\n\nclass RefreshMiddleware(httpx.Auth):\n    def __init__(\n        self,\n        device_oidc: DeviceOIDC,\n        on_refresh: Callable[[DeviceOIDC], Awaitable[None]],\n    ) -> None:\n        self.device_oidc = device_oidc\n        self.on_refresh = on_refresh\n        self.lock = asyncio.Lock()\n\n    async def _refresh_and_update(self) -> None:\n        new_device_oidc = await refresh(self.device_oidc)\n        self.device_oidc = new_device_oidc\n        try:\n            await self.on_refresh(new_device_oidc)\n        except Exception:\n            logger.exception(\"Error in on_refresh callback\")\n\n    async def async_auth_flow(\n        self, request: httpx.Request\n    ) -> AsyncGenerator[httpx.Request, httpx.Response]:\n        token = self.device_oidc.device_access_token\n        request.headers[\"Authorization\"] = f\"Bearer {token}\"\n\n        response = yield request\n        if response.status_code == 401:\n            async with self.lock:\n                if token == self.device_oidc.device_access_token:\n                    await self._refresh_and_update()\n                    request.headers[\"Authorization\"] = (\n                        f\"Bearer {self.device_oidc.device_access_token}\"\n                    )\n                yield request\n\n\nclass DeviceAuthorizationRequest(BaseModel):\n    client_id: str\n    scope: str\n\n\nclass DeviceAuthorizationResponse(BaseModel):\n    device_code: str\n    user_code: str\n    verification_uri: str\n    verification_uri_complete: str | None = None\n    expires_in: int\n    interval: int | None = None\n\n\nclass TokenRequestDeviceCode(BaseModel):\n    grant_type: str = \"urn:ietf:params:oauth:grant-type:device_code\"\n    device_code: str\n    client_id: str\n\n\nclass TokenResponse(BaseModel):\n    # Success fields\n    id_token: str | None = None\n    access_token: str | None = None\n    refresh_token: str | None = None\n    expires_in: int | None = None\n    token_type: str | None = None\n    scope: str | None = None\n    # Error fields\n    error: str | None = None\n    error_description: str | None = None\n\n\nclass TokenRequestRefresh(BaseModel):\n    grant_type: str = \"refresh_token\"\n    refresh_token: str\n    client_id: str\n\n\nclass OIDCClient(ClientContextManager):\n    def __init__(self) -> None:\n        super().__init__(None)\n\n    async def fetch_provider_configuration(\n        self, discovery_url: str\n    ) -> OidcProviderConfiguration:\n        resp = await self.client.get(discovery_url, timeout=10.0)\n        resp.raise_for_status()\n        return OidcProviderConfiguration.model_validate(resp.json())\n\n    async def device_authorization(\n        self, device_endpoint: str, request: DeviceAuthorizationRequest\n    ) -> DeviceAuthorizationResponse:\n        resp = await self.client.post(\n            device_endpoint,\n            data=request.model_dump(),\n            headers={\n                \"Accept\": \"application/json\",\n                \"Content-Type\": \"application/x-www-form-urlencoded\",\n            },\n            timeout=10.0,\n        )\n        resp.raise_for_status()\n        return DeviceAuthorizationResponse.model_validate(resp.json())\n\n    async def token_with_device_code(\n        self, token_endpoint: str, request: TokenRequestDeviceCode\n    ) -> TokenResponse:\n        resp = await self.client.post(\n            token_endpoint,\n            data=request.model_dump(),\n            headers={\n                \"Accept\": \"application/json\",\n                \"Content-Type\": \"application/x-www-form-urlencoded\",\n            },\n            timeout=10.0,\n        )\n        # Do not raise for status; callers inspect error payloads during polling\n        try:\n            payload = resp.json()\n        except Exception:\n            # Fall back to minimal error information\n            return TokenResponse(error=\"invalid_response\", error_description=resp.text)\n        return TokenResponse.model_validate(payload)\n\n    async def token_with_refresh(\n        self, token_endpoint: str, request: TokenRequestRefresh\n    ) -> TokenResponse:\n        resp = await self.client.post(\n            token_endpoint,\n            data=request.model_dump(),\n            headers={\n                \"Accept\": \"application/json\",\n                \"Content-Type\": \"application/x-www-form-urlencoded\",\n            },\n            timeout=10.0,\n        )\n        try:\n            payload = resp.json()\n        except Exception:\n            return TokenResponse(error=\"invalid_response\", error_description=resp.text)\n        return TokenResponse.model_validate(payload)\n\n    async def get_jwks(self, jwks_uri: str) -> JsonWebKeySet:\n        resp = await self.client.get(jwks_uri, timeout=10.0)\n        resp.raise_for_status()\n        return JsonWebKeySet.model_validate(resp.json())\n\n\nasync def decode_jwt_claims_from_device_oidc(\n    oidc_device: DeviceOIDC,\n    verify_audience: bool = True,\n    verify_expiration: bool = True,\n    audience: str | None = None,\n) -> dict[str, Any]:\n    \"\"\"Decode JWT claims by discovering provider and verifying via JWKS.\n\n    Assumes RSA signing. Audience verification can be toggled and, when enabled,\n    an audience value can be provided.\n    \"\"\"\n    if not oidc_device.device_id_token:\n        raise ValueError(\"Device ID token is missing. Cannot decode claims.\")\n    async with OIDCClient() as oidc:\n        provider = await oidc.fetch_provider_configuration(oidc_device.discovery_url)\n        jwks_uri = provider.jwks_uri\n        if not jwks_uri:\n            raise ValueError(\"Provider does not expose jwks_uri\")\n    return await decode_jwt_claims(\n        oidc_device.device_id_token,\n        jwks_uri,\n        verify_audience,\n        verify_expiration,\n        audience,\n    )\n\n\nasync def decode_jwt_claims(\n    token: str,\n    jwks_uri: str,\n    verify_audience: bool = True,\n    verify_expiration: bool = True,\n    audience: str | None = None,\n) -> dict[str, Any]:\n    async with OIDCClient() as oidc:\n        jwks = await oidc.get_jwks(jwks_uri)\n\n    # Select key\n    header = jwt.get_unverified_header(token)\n    kid = header.get(\"kid\")\n    keys = jwks.keys\n    key = next((k for k in keys if k.kid == kid), None) or next(iter(keys), None)\n    if not key:\n        raise ValueError(\"Signing key not found in JWKS\")\n\n    # Build public key (RSA-only)\n    if key.kty != \"RSA\":\n        raise ValueError(\"Unsupported JWK kty; only RSA is supported\")\n    key_json = key.model_dump_json()\n    raw_key = RSAAlgorithm.from_jwk(key_json)\n    if not isinstance(raw_key, RSAPublicKey):\n        raise ValueError(\"Unsupported RSA key type; expected RSAPublicKey from JWKS\")\n    public_key = raw_key\n\n    return jwt.decode(\n        token,\n        public_key,\n        algorithms=[\"RS256\"],\n        options={\"verify_aud\": verify_audience, \"verify_exp\": verify_expiration},\n        audience=audience,\n    )\n\n\nasync def refresh(device_oidc: DeviceOIDC) -> DeviceOIDC:\n    \"\"\"\n    Run a refresh on the access token, storing updated tokens in a new DeviceOIDC.\n    \"\"\"\n    async with OIDCClient() as oidc:\n        provider = await oidc.fetch_provider_configuration(device_oidc.discovery_url)\n        token_endpoint = provider.token_endpoint\n        if not token_endpoint:\n            raise ValueError(\"Provider does not expose token_endpoint\")\n        if not device_oidc.device_refresh_token:\n            raise ValueError(\"Device refresh token is missing. Cannot refresh.\")\n        token = await oidc.token_with_refresh(\n            token_endpoint,\n            TokenRequestRefresh(\n                refresh_token=device_oidc.device_refresh_token,\n                client_id=device_oidc.client_id,\n            ),\n        )\n        copy = device_oidc.model_copy()\n        if not token.access_token:\n            raise ValueError(\"Refresh failed: token response missing access_token\")\n        copy.device_access_token = token.access_token\n        copy.device_refresh_token = token.refresh_token or copy.device_refresh_token\n        copy.device_id_token = token.id_token or copy.device_id_token\n        return copy\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/client.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, Any, AsyncGenerator\n\nimport click\nfrom llama_agents.cli.env_settings import LlamactlEnvSettings, read_env_settings\nfrom llama_agents.cli.interactive import is_interactive_session\n\nif TYPE_CHECKING:\n    from llama_agents.core.client.manage_client import ControlPlaneClient, ProjectClient\n\n\n_ENV_AUTH_WARNING_EMITTED = False\n\n\n@dataclass(frozen=True)\nclass _AuthContext:\n    base_url: str\n    project_id: str\n    api_key: str | None\n    auth_middleware: Any | None\n\n\ndef _env_auth_context_or_none(\n    settings: LlamactlEnvSettings,\n    project_id_override: str | None,\n) -> _AuthContext | None:\n    if settings.cloud_auth_disabled:\n        return None\n\n    if not settings.has_complete_cloud_auth:\n        return None\n\n    api_key = settings.llama_cloud_api_key\n    project_id = settings.llama_agents_project_id\n    assert api_key is not None\n    assert project_id is not None\n\n    return _AuthContext(\n        base_url=settings.normalized_base_url,\n        project_id=project_id_override or project_id,\n        api_key=api_key,\n        auth_middleware=None,\n    )\n\n\ndef _profile_auth_context_or_none(\n    project_id_override: str | None,\n) -> _AuthContext | None:\n    from llama_agents.cli.config.env_service import service\n\n    auth_svc = service.current_auth_service()\n    profile = auth_svc.get_current_profile()\n    if profile is None:\n        return None\n\n    return _AuthContext(\n        base_url=profile.api_url.rstrip(\"/\"),\n        project_id=project_id_override or profile.project_id,\n        api_key=profile.api_key,\n        auth_middleware=auth_svc.auth_middleware(),\n    )\n\n\ndef _auth_context_or_none(\n    project_id_override: str | None = None,\n) -> _AuthContext | None:\n    settings = read_env_settings()\n    context = _env_auth_context_or_none(settings, project_id_override)\n    if context is not None:\n        _warn_if_env_auth_overrides_profile(settings)\n        return context\n    _warn_if_partial_env_auth(settings)\n    return _profile_auth_context_or_none(project_id_override)\n\n\ndef _warn_if_env_auth_overrides_profile(settings: LlamactlEnvSettings) -> None:\n    global _ENV_AUTH_WARNING_EMITTED\n\n    if _ENV_AUTH_WARNING_EMITTED:\n        return\n    if settings.completion_active:\n        return\n\n    from llama_agents.cli.config.env_service import service\n\n    try:\n        profile = service.current_auth_service().get_current_profile()\n    except Exception:\n        return\n    if not profile:\n        return\n\n    click.echo(\n        \"Using LLAMA_CLOUD_API_KEY from environment \"\n        f\"(overriding profile '{profile.name}'). \"\n        \"Set LLAMA_CLOUD_USE_PROFILE=1 to use the profile instead.\",\n        err=True,\n    )\n    _ENV_AUTH_WARNING_EMITTED = True\n\n\ndef _warn_if_partial_env_auth(settings: LlamactlEnvSettings) -> None:\n    global _ENV_AUTH_WARNING_EMITTED\n\n    if _ENV_AUTH_WARNING_EMITTED:\n        return\n    if settings.completion_active:\n        return\n    if settings.cloud_auth_disabled:\n        return\n    if not settings.llama_cloud_api_key:\n        return\n\n    click.echo(\n        \"LLAMA_CLOUD_API_KEY is set but LLAMA_AGENTS_PROJECT_ID is missing. \"\n        \"Set it or pass --project for env var auth.\",\n        err=True,\n    )\n    _ENV_AUTH_WARNING_EMITTED = True\n\n\ndef get_control_plane_client() -> ControlPlaneClient:\n    from llama_agents.core.client.manage_client import ControlPlaneClient\n\n    context = _auth_context_or_none()\n    if context is not None:\n        return ControlPlaneClient(\n            context.base_url, context.api_key, context.auth_middleware\n        )\n\n    # Fallback: allow env-scoped client construction for env operations\n    from llama_agents.cli.config.env_service import service\n\n    env = service.get_current_environment()\n    resolved_base_url = env.api_url.rstrip(\"/\")\n    return ControlPlaneClient(resolved_base_url)\n\n\ndef get_project_client(project_id_override: str | None = None) -> ProjectClient:\n    \"\"\"Return a ProjectClient bound to env auth or the active profile.\n\n    If ``project_id_override`` is provided, the client uses that project ID\n    instead of the env/profile default. This mirrors ``kubectl -n <ns>``.\n    \"\"\"\n    from llama_agents.core.client.manage_client import ProjectClient\n\n    context = _auth_context_or_none(project_id_override)\n    if context is not None:\n        return ProjectClient(\n            context.base_url,\n            context.project_id,\n            context.api_key,\n            context.auth_middleware,\n        )\n\n    if is_interactive_session():\n        # Deferred: auth command imports browser/prompt dependencies and also\n        # depends on client helpers for project discovery.\n        from llama_agents.cli.commands.auth import validate_authenticated_profile\n\n        validate_authenticated_profile()\n        context = _auth_context_or_none(project_id_override)\n        if context is not None:\n            return ProjectClient(\n                context.base_url,\n                context.project_id,\n                context.api_key,\n                context.auth_middleware,\n            )\n\n    from llama_agents.cli.config.env_service import service\n\n    auth_svc = service.current_auth_service()\n    command = (\n        \"llamactl auth login\" if auth_svc.env.requires_auth else \"llamactl auth token\"\n    )\n    raise click.ClickException(f\"No profile configured. To get started, run: {command}\")\n\n\n@asynccontextmanager\nasync def project_client_context(\n    project_id_override: str | None = None,\n) -> AsyncGenerator[ProjectClient, None]:\n    client = get_project_client(project_id_override=project_id_override)\n    try:\n        yield client\n    finally:\n        try:\n            await client.aclose()\n        except Exception:\n            pass\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/agentcore.py",
    "content": "from __future__ import annotations\n\nimport os\nimport subprocess\nfrom pathlib import Path\n\nimport click\nfrom llama_agents.cli.output import status\n\nfrom ..app import app\nfrom ..options import global_options\n\n\n@app.group(\n    help=\"LlamaAgents x Bedrock AgentCore deployment utilities.\",\n    no_args_is_help=True,\n)\n@global_options\ndef agentcore() -> None:\n    \"\"\"LlamaAgents x Bedrock AgentCore deployment utilities.\"\"\"\n    pass\n\n\n@agentcore.command(\"run\", help=\"Run AgentCore server\")\ndef run() -> None:\n    start_app()\n\n\n@agentcore.command(\n    \"test\",\n    help=\"Run AgentCore server locally with a local SQLite store (no AWS required). \"\n    \"Send requests to POST http://localhost:8080/invocations\",\n)\ndef test() -> None:\n    start_app(local=True)\n\n\n@agentcore.command(\n    \"export\",\n    help=\"Export generated code to a `.agentcore` folder in the current working directory.\",\n)\ndef export() -> None:\n    export_generated_entrypoint_code()\n\n\ndef export_generated_entrypoint_code() -> None:\n    from llama_agents.agentcore.export import (\n        export_generated_entrypoint_code as _export,\n    )\n\n    _export()\n\n\ndef start_app_in_target_venv(path: Path, *, local: bool = False) -> None:\n    from llama_agents.appserver.process_utils import run_process\n    from llama_agents.appserver.workflow_loader import (\n        _exclude_venv_warning,\n    )\n\n    args = [\n        \"uv\",\n        \"run\",\n        \"--no-progress\",\n        \"python\",\n        \"-m\",\n        \"llama_agents.agentcore.main\",\n    ]\n    if local:\n        args.append(\"--local\")\n    else:\n        args.append(\"--run\")\n\n    run_process(\n        args,\n        cwd=path,\n        env=os.environ.copy(),\n        line_transform=_exclude_venv_warning,\n    )\n\n\ndef start_app(local: bool = False) -> None:\n    from llama_agents.appserver.deployment_config_parser import get_deployment_config\n    from llama_agents.appserver.settings import configure_settings, settings\n    from llama_agents.appserver.workflow_loader import (\n        inject_appserver_into_target,\n    )\n\n    configure_settings(deployment_file_path=Path.cwd(), app_root=Path.cwd())\n    cfg = get_deployment_config()\n    inject_appserver_into_target(cfg, source_root=Path.cwd())\n    base_dir = Path.cwd()\n    path = settings.resolved_config_parent.relative_to(base_dir)\n    try:\n        start_app_in_target_venv(path, local=local)\n    except subprocess.CalledProcessError as exc:\n        status(\"failed to run agentcore; see errors above\")\n        raise click.exceptions.Exit(exc.returncode)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/aliased_group.py",
    "content": "\"\"\"Fully lifted from https://click.palletsprojects.com/en/stable/extending-click/\"\"\"\n\nimport click\n\n\nclass AliasedGroup(click.Group):\n    \"\"\"\n    Implements a subclass of Group that accepts a prefix for a command.\n    If there was a command called push, it would accept pus as an alias (so long as it was unique):\n    \"\"\"\n\n    def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:\n        rv = super().get_command(ctx, cmd_name)\n\n        if rv is not None:\n            return rv\n\n        matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]\n\n        if not matches:\n            return None\n\n        if len(matches) == 1:\n            return click.Group.get_command(self, ctx, matches[0])\n\n        ctx.fail(f\"Too many matches: {', '.join(sorted(matches))}\")\n\n    def resolve_command(\n        self, ctx: click.Context, args: list[str]\n    ) -> tuple[str, click.Command, list[str]]:\n        # always return the full command name\n        cmd_name, cmd, args = super().resolve_command(ctx, args)\n        assert cmd is not None\n        full_name: str = (\n            cmd.name\n            if cmd.name\n            else cmd_name\n            if isinstance(cmd_name, str)\n            else \"<none>\"  # shouldn't happen\n        )\n        return (full_name, cmd, args)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/auth.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport os\nimport platform\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport click\nfrom llama_agents.cli.interactive import is_interactive_session, select_or_exit\nfrom llama_agents.cli.output import status, warning\nfrom llama_agents.cli.param_types import ProfileType\nfrom llama_agents.cli.utils.capabilities import probe_organizations_support\n\nfrom ..app import app\nfrom ..display import AuthProfileDisplay\nfrom ..options import global_options, render_output, simple_output_option\n\nif TYPE_CHECKING:\n    from llama_agents.cli.config.auth_service import AuthService\n    from llama_agents.cli.config.env_service import EnvService\n    from llama_agents.core.schema.projects import OrgSummary, ProjectSummary\n\n    from ..config.schema import Auth, DeviceOIDC\n\n\nclass NoProjectsFoundError(Exception):\n    \"\"\"Raised when the authenticated user has no accessible projects on an org-less server.\"\"\"\n\n\nclass AuthGroup(click.Group):\n    _removed_command_hints = {\n        \"list\": \"Use `llamactl auth get` instead.\",\n        \"switch\": \"Use `llamactl auth use` instead.\",\n        \"env\": \"Use `llamactl environments get` instead.\",\n        \"project\": \"Use `llamactl projects` instead.\",\n        \"organizations\": \"Use `llamactl organizations get` instead.\",\n        \"destroy\": \"Use `llamactl config destroy` instead.\",\n        \"show-db\": \"Use `llamactl config show-db` instead.\",\n    }\n\n    def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:\n        command = super().get_command(ctx, cmd_name)\n        if command is not None:\n            return command\n        if hint := self._removed_command_hints.get(cmd_name):\n            raise click.ClickException(f\"No such command '{cmd_name}'. {hint}\")\n        return None\n\n\n_ClickPath = getattr(click, \"Path\")\n\n\ndef _get_service() -> EnvService:\n    \"\"\"Return the EnvService instance lazily.\n\n    Imports ``service`` only when needed so CLI startup stays fast and tests\n    can patch ``llama_agents.cli.config.env_service.service`` directly.\n    \"\"\"\n    from llama_agents.cli.config.env_service import service  # local import on purpose\n\n    return service\n\n\n# Create sub-applications for organizing commands\n@app.group(\n    help=\"Manage login profiles and credentials.\",\n    cls=AuthGroup,\n    no_args_is_help=True,\n)\n@global_options\ndef auth() -> None:\n    \"\"\"Manage login profiles and credentials.\"\"\"\n    pass\n\n\n@auth.command(\"token\")\n@global_options\n@click.option(\n    \"--project\",\n    \"project_id\",\n    help=\"Project ID to use for the login when creating non-interactively.\",\n)\n@click.option(\n    \"--project-id\",\n    \"project_id\",\n    hidden=True,\n)\n@click.option(\n    \"--api-key\",\n    help=\"API key to use for the login when creating non-interactively\",\n)\ndef create_api_key_profile(\n    project_id: str | None,\n    api_key: str | None,\n) -> None:\n    \"\"\"Authenticate with an API key and create a profile in the current environment.\"\"\"\n    try:\n        auth_svc = _get_service().current_auth_service()\n\n        # Non-interactive mode: require both api-key and project.\n        if not is_interactive_session():\n            if not api_key or not project_id:\n                raise click.ClickException(\n                    \"--api-key and --project are required in non-interactive mode\"\n                )\n            created = auth_svc.create_profile_from_token(project_id, api_key)\n            status(f\"created API key profile {created.name} and set as current\")\n            return\n\n        # Interactive mode: prompt for token (masked) and validate\n        token_value = api_key or _prompt_for_api_key()\n        org = _discover_organization(auth_svc, api_key=token_value)\n        org_id_for_projects = org.org_id if org is not None else None\n        if org is not None:\n            status(f\"projects for organization {org.org_name}\")\n        projects = _prompt_validate_api_key_and_list_projects(\n            auth_svc, token_value, org_id=org_id_for_projects\n        )\n\n        # Select or enter project ID\n        selected_project_id = project_id or _select_or_enter_project(\n            projects, auth_svc.env.requires_auth\n        )\n        if not selected_project_id:\n            status(\"no project selected\")\n            return\n\n        # Create and set profile\n        created = auth_svc.create_profile_from_token(selected_project_id, token_value)\n        status(f\"created API key profile {created.name} and set as current\")\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@auth.command(\"login\")\n@global_options\ndef device_login() -> None:\n    \"\"\"Log in with a web browser.\"\"\"\n    from llama_agents.cli.auth.client import OIDCNotEnabledError\n\n    try:\n        created = _create_device_profile()\n        status(f\"created login profile {created.name} and set as current\")\n\n    except NoProjectsFoundError:\n        warning(\"no existing projects\")\n        status(\"looks like this may be your first time logging in\")\n        status(\"log in to https://cloud.llamaindex.ai to complete account setup\")\n        return\n\n    except OIDCNotEnabledError as e:\n        warning(\"this server does not have browser-based login (OIDC) configured\")\n        if str(e):\n            status(f\"server response: {e}\")\n        status(\"use llamactl auth token to log in with an API key instead\")\n        return\n\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@auth.command(\"get\")\n@click.argument(\"name\", required=False, type=ProfileType())\n@global_options\n@simple_output_option\ndef get_profiles(name: str | None, output: str) -> None:\n    \"\"\"List auth profiles or show one profile.\"\"\"\n    try:\n        auth_svc = _get_service().current_auth_service()\n        profiles = auth_svc.list_profiles()\n        current = auth_svc.get_current_profile()\n        if name:\n            profiles = [profile for profile in profiles if profile.name == name]\n            if not profiles:\n                raise click.ClickException(f\"Profile '{name}' not found\")\n\n        if not profiles and output == \"text\":\n            status(\"no profiles found\")\n            if auth_svc.env.requires_auth:\n                status(\"create one with: llamactl auth login\")\n            else:\n                status(\"create one with: llamactl auth token\")\n            return\n\n        current_name = current.name if current else None\n        displays = [\n            AuthProfileDisplay.from_profile(p, current_name=current_name)\n            for p in profiles\n        ]\n        render_output(displays[0] if name and len(displays) == 1 else displays, output)\n\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@auth.command(\"use\")\n@global_options\n@click.argument(\"name\", required=False, type=ProfileType())\ndef use_profile(name: str | None) -> None:\n    \"\"\"Switch to a different profile.\"\"\"\n    auth_svc = _get_service().current_auth_service()\n    try:\n        selected_auth = _select_profile(auth_svc, name, require_selection=True)\n        if not selected_auth:\n            status(\"no profile selected\")\n            return\n\n        auth_svc.set_current_profile(selected_auth.name)\n        status(f\"switched profile {selected_auth.name}\")\n\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@auth.command(\"logout\")\n@global_options\n@click.argument(\"name\", required=False, type=ProfileType())\ndef delete_profile(name: str | None) -> None:\n    \"\"\"Log out from a profile and wipe its local credentials.\"\"\"\n    try:\n        auth_svc = _get_service().current_auth_service()\n        auth = _select_profile(auth_svc, name)\n        if not auth:\n            if name:\n                raise click.ClickException(f\"Profile '{name}' not found\")\n            raise click.ClickException(\"No profile selected\")\n\n        if asyncio.run(auth_svc.delete_profile(auth.name)):\n            status(f\"logged out {auth.name}\")\n        else:\n            raise click.ClickException(f\"Profile '{auth.name}' not found\")\n\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n# Very simple introspection: decode current token via provider JWKS\n@auth.command(\"me\", hidden=True)\n@global_options\ndef me() -> None:\n    \"\"\"Print JWT claims for the current profile's token using provider JWKS.\n\n    Assumes the stored API key is a JWT (e.g., OIDC id_token).\n    \"\"\"\n    try:\n        from llama_agents.cli.auth.client import decode_jwt_claims_from_device_oidc\n\n        auth_svc = _get_service().current_auth_service()\n        profile = auth_svc.get_current_profile()\n        if not profile or not profile.device_oidc:\n            raise click.ClickException(\n                \"No OIDC profile selected. Run `llamactl auth login` or switch to an existing OIDC profile.\"\n            )\n\n        claims = asyncio.run(\n            decode_jwt_claims_from_device_oidc(\n                profile.device_oidc,\n                verify_audience=False,\n                verify_expiration=False,\n            )\n        )\n        click.echo(json.dumps(claims, indent=2, sort_keys=True))\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@auth.command(\"inject\")\n@global_options\n@click.option(\n    \"--env-file\",\n    \"env_file\",\n    default=Path(\".env\"),\n    type=_ClickPath(dir_okay=False, resolve_path=True, path_type=Path),\n    help=\"Path to the .env file to write\",\n)\ndef inject_env_vars(\n    env_file: Path,\n) -> None:\n    \"\"\"Inject profile credentials into a .env file.\n\n    Writes LLAMA_CLOUD_API_KEY, LLAMA_CLOUD_BASE_URL, and LLAMA_AGENTS_PROJECT_ID\n    based on the current profile. Always overwrites and creates the file if missing.\n    \"\"\"\n    try:\n        from dotenv import set_key\n        from llama_agents.cli.utils.env_inject import env_vars_from_profile\n\n        auth_svc = _get_service().current_auth_service()\n        profile = auth_svc.get_current_profile()\n        if not profile:\n            if is_interactive_session():\n                profile = validate_authenticated_profile()\n            else:\n                raise click.ClickException(\n                    \"No profile configured. Run `llamactl auth token` to create a profile.\"\n                )\n        if not profile.api_key:\n            raise click.ClickException(\n                \"Current profile is unauthenticated (missing API key)\"\n            )\n\n        vars = env_vars_from_profile(profile)\n        if not vars:\n            status(\"no variables to inject\")\n            return\n        env_file.parent.mkdir(parents=True, exist_ok=True)\n        for key, value in vars.items():\n            set_key(str(env_file), key, value)\n        rel = os.path.relpath(env_file, Path.cwd())\n        status(f\"wrote environment variables to {rel}\")\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\ndef _auto_device_name() -> str:\n    try:\n        if sys.platform == \"darwin\":  # macOS\n            return (\n                subprocess.check_output([\"scutil\", \"--get\", \"ComputerName\"])\n                .decode()\n                .strip()\n            )\n        elif sys.platform.startswith(\"win\"):\n            return os.environ[\"COMPUTERNAME\"]\n        else:  # Linux / Unix\n            return platform.node()\n    except Exception:\n        return platform.node()\n\n\nasync def _create_or_update_agent_api_key(auth_svc: AuthService, profile: Auth) -> None:\n    \"\"\"\n    Mutates and updates the profile with an agent API key if it does not exist or is invalid.\n    \"\"\"\n    import httpx\n    from llama_agents.cli.auth.client import PlatformAuthClient\n    from llama_agents.cli.utils.retry import run_with_network_retries\n\n    if profile.api_key is not None:\n        async with PlatformAuthClient(profile.api_url, profile.api_key) as client:\n            try:\n                await client.me()\n            except httpx.HTTPStatusError as e:\n                if e.response.status_code == 401:\n                    # must have been deleted\n                    profile.api_key = None\n                    profile.api_key_id = None\n                else:\n                    raise\n    if profile.api_key is None:\n        async with auth_svc.profile_client(profile) as client:\n            name = f\"{profile.name} llamactl on {profile.device_oidc.device_name if profile.device_oidc else 'unknown'}\"\n\n            # Non-idempotent POST: only retry connect-phase errors so we\n            # absorb initial-connectivity blips without risking duplicate\n            # keys from a read-timeout retry.\n            try:\n                api_key = await run_with_network_retries(\n                    lambda: client.create_agent_api_key(name),\n                    idempotent=False,\n                )\n            except httpx.HTTPStatusError:\n                # Do not treat HTTP errors as transient; re-raise for normal handling.\n                raise\n            except httpx.RequestError as e:\n                detail = str(e) or e.__class__.__name__\n                raise click.ClickException(\n                    \"Network error while provisioning an API token for llamactl. \"\n                    \"Your login may have succeeded, but we could not create a CLI API token. \"\n                    \"Please check your internet connection and try again. \"\n                    f\"Details: {detail}\"\n                ) from e\n\n        profile.api_key = api_key.token\n        profile.api_key_id = api_key.id\n        auth_svc.update_profile(profile)\n\n\ndef _create_device_profile() -> Auth:\n    auth_svc = _get_service().current_auth_service()\n    if not auth_svc.env.requires_auth:\n        raise click.ClickException(\"This environment does not support authentication\")\n\n    base_url = auth_svc.env.api_url.rstrip(\"/\")\n\n    oidc_device = asyncio.run(_run_device_authentication(base_url))\n    token = oidc_device.device_access_token\n\n    # Discover org for project scoping (pass token — no profile exists yet)\n    org = _discover_organization(auth_svc, api_key=token)\n    org_id = org.org_id if org is not None else None\n\n    # Obtain or prompt for project ID and create profile\n    projects = _list_projects(auth_svc, token, org_id=org_id)\n    if not projects:\n        if org is None:\n            # Legacy org-less server — account not set up yet\n            raise NoProjectsFoundError()\n        raise click.ClickException(\"No projects found for this account\")\n\n    if org is not None:\n        status(f\"projects for organization {org.org_name}\")\n\n    selected_project_id = _select_or_enter_project(projects, True)\n    if not selected_project_id:\n        # User cancelled selection despite having projects\n        raise click.ClickException(\"No project selected\")\n\n    created = auth_svc.create_or_update_profile_from_oidc(\n        selected_project_id, oidc_device\n    )\n\n    # Ensure login is atomic: if provisioning the CLI API key fails, clean up the\n    # partially created profile so we don't leave a \"logged-in but unusable\" state.\n    try:\n        asyncio.run(_create_or_update_agent_api_key(auth_svc, created))\n    except Exception:\n        try:\n            asyncio.run(auth_svc.delete_profile(created.name))\n        except Exception:\n            # Best-effort cleanup; original error is more important for the user.\n            pass\n        raise\n\n    return created\n\n\nasync def _run_device_authentication(base_url: str) -> DeviceOIDC:\n    import webbrowser\n\n    from llama_agents.cli.auth.client import (\n        DeviceAuthorizationRequest,\n        OIDCClient,\n        PlatformAuthDiscoveryClient,\n        TokenRequestDeviceCode,\n        decode_jwt_claims,\n    )\n\n    from ..config.schema import DeviceOIDC\n\n    device_name = _auto_device_name()\n    # 1) Discover upstream and CLI client_id via client\n    async with PlatformAuthDiscoveryClient(base_url) as discovery:\n        disc = await discovery.oidc_discovery()\n    upstream = disc.discovery_url\n    client_ids = disc.client_ids or {}\n    client_ids_list = list(client_ids.values())\n    client_id = client_ids.get(\"cli\") or (\n        client_ids_list[0] if len(client_ids_list) == 1 else None\n    )\n    if not client_id:\n        raise click.ClickException(\n            \"Expected 'cli' Client ID not found from auth discovery\"\n        )\n\n    # 2) Device flow via typed OIDC client\n    async with OIDCClient() as oidc:\n        provider = await oidc.fetch_provider_configuration(upstream)\n        device_endpoint = provider.device_authorization_endpoint\n        token_endpoint = provider.token_endpoint\n        if not device_endpoint or not token_endpoint:\n            raise click.ClickException(\"Device Authorization not supported by provider\")\n\n        scope_value = \" \".join(sorted({\"openid\", \"profile\", \"email\", \"offline_access\"}))\n\n        # 3) Start device authorization\n        da = await oidc.device_authorization(\n            device_endpoint,\n            DeviceAuthorizationRequest(client_id=client_id, scope=scope_value),\n        )\n\n        status(\n            \"complete authentication by visiting the verification URI and confirming the device:\"\n        )\n        if da.verification_uri:\n            status(\n                f\"verification URI: {da.verification_uri} (will open in your browser if supported)\"\n            )\n        if da.user_code:\n            status(f\"user code: {da.user_code} to confirm the device\")\n        if da.verification_uri_complete:\n            try:\n                webbrowser.open(da.verification_uri_complete)\n            except Exception:\n                pass\n\n        # 4) Poll token endpoint\n        interval = int(da.interval or 5)\n        while True:\n            await asyncio.sleep(interval)\n            token = await oidc.token_with_device_code(\n                token_endpoint,\n                TokenRequestDeviceCode(\n                    device_code=da.device_code,\n                    client_id=client_id,\n                ),\n            )\n            if token.error in {\"authorization_pending\", \"slow_down\"}:\n                if token.error == \"slow_down\":\n                    interval += 5\n                continue\n            if token.error:\n                raise click.ClickException(\n                    f\"Token polling failed: {token.error} {token.error_description or ''}\"\n                )\n            if token.id_token:\n                if not token.access_token:\n                    raise click.ClickException(\n                        \"Device flow failed: token response missing access_token\"\n                    )\n                if not provider.jwks_uri:\n                    raise click.ClickException(\"Provider does not expose jwks_uri\")\n                claims = await decode_jwt_claims(\n                    token.id_token,\n                    provider.jwks_uri,\n                    verify_audience=False,\n                )\n                email = claims.get(\"email\")\n                if not email:\n                    raise click.ClickException(\n                        \"Device flow failed: email not found in token\"\n                    )\n                user_id = claims.get(\"sub\") or email\n                return DeviceOIDC(\n                    device_name=device_name,\n                    email=email,\n                    user_id=user_id,\n                    client_id=client_id,\n                    discovery_url=upstream,\n                    device_access_token=token.access_token,\n                    device_refresh_token=token.refresh_token,\n                    device_id_token=token.id_token,\n                )\n            raise click.ClickException(\"Device flow failed: unexpected token response\")\n\n\ndef validate_authenticated_profile() -> Auth:\n    \"\"\"Validate that the user is authenticated within the current environment.\n\n    - If there is a current profile, return it.\n    - If multiple profiles exist in the current environment, prompt to select in interactive mode.\n    - If none exist:\n      - If environment requires_auth: run token flow inline.\n      - Else: create profile without token after selecting a project.\n    \"\"\"\n    auth_svc = _get_service().current_auth_service()\n    existing = auth_svc.get_current_profile()\n    if existing:\n        return existing\n\n    interactive = is_interactive_session()\n\n    if not interactive:\n        raise click.ClickException(\n            \"No profile configured. Run `llamactl auth token` to create a profile.\"\n        )\n\n    # Filter profiles by current environment\n    env_profiles = auth_svc.list_profiles()\n    current_env = auth_svc.env\n\n    if len(env_profiles) > 1:\n        choice = select_or_exit(\n            [(profile, profile.name) for profile in env_profiles],\n            \"Select profile\",\n            hint_flag=\"<profile>\",\n            hint_command=\"llamactl auth get\",\n        )\n        auth_svc.set_current_profile(choice.name)\n        return choice\n    if len(env_profiles) == 1:\n        only = env_profiles[0]\n        auth_svc.set_current_profile(only.name)\n        return only\n\n    # No profiles exist for this env\n    if current_env.requires_auth:\n        # Inline token flow\n        created = _create_device_profile()\n        return created\n    else:\n        # No auth required: select project and create a default profile without token\n        project_id = click.prompt(\n            \"Enter project ID\", default=\"\", show_default=False\n        ).strip()\n        if not project_id:\n            raise click.ClickException(\"No project ID provided\")\n        created = auth_svc.create_profile_from_token(project_id, None)\n        return created\n\n\n# -----------------------------\n# Helpers for token/profile flow\n# -----------------------------\n\n\ndef _prompt_for_api_key() -> str:\n    entered = click.prompt(\n        \"Enter API key token to login\",\n        hide_input=True,\n        default=\"\",\n        show_default=False,\n    )\n    if entered:\n        return entered.strip()\n    raise click.ClickException(\"No API key entered\")\n\n\ndef _list_projects(\n    auth_svc: AuthService,\n    api_key: str | None = None,\n    org_id: str | None = None,\n) -> list[ProjectSummary]:\n    async def _run() -> list[ProjectSummary]:\n        from llama_agents.core.client.manage_client import ControlPlaneClient\n\n        profile = auth_svc.get_current_profile()\n        async with ControlPlaneClient.ctx(\n            auth_svc.env.api_url,\n            api_key or (profile.api_key if profile else None),\n            None if api_key is not None else auth_svc.auth_middleware(profile),\n        ) as client:\n            return await client.list_projects(org_id=org_id)\n\n    return asyncio.run(_run())\n\n\ndef _list_organizations(\n    auth_svc: AuthService,\n    api_key: str | None = None,\n) -> list[OrgSummary]:\n    async def _run() -> list[OrgSummary]:\n        from llama_agents.core.client.manage_client import ControlPlaneClient\n\n        profile = auth_svc.get_current_profile()\n        async with ControlPlaneClient.ctx(\n            auth_svc.env.api_url,\n            api_key or (profile.api_key if profile else None),\n            None if api_key is not None else auth_svc.auth_middleware(profile),\n        ) as client:\n            return await client.list_organizations()\n\n    return asyncio.run(_run())\n\n\ndef _discover_organization(\n    auth_svc: AuthService, api_key: str | None = None\n) -> OrgSummary | None:\n    \"\"\"Discover the default organization from the server.\n\n    Returns the default OrgSummary (by is_default flag, falling back to first),\n    or None if the server doesn't support organizations.\n    \"\"\"\n    if not probe_organizations_support():\n        return None\n    organizations = _list_organizations(auth_svc, api_key=api_key)\n    if not organizations:\n        return None\n    return next((o for o in organizations if o.is_default), organizations[0])\n\n\ndef _prompt_validate_api_key_and_list_projects(\n    auth_svc: AuthService, api_key: str, org_id: str | None = None\n) -> list[ProjectSummary]:\n    import httpx\n\n    try:\n        return _list_projects(auth_svc, api_key, org_id=org_id)\n    except httpx.HTTPStatusError as e:\n        if e.response.status_code == 401:\n            status(\"invalid API key; please try again\")\n            return _prompt_validate_api_key_and_list_projects(\n                auth_svc, _prompt_for_api_key(), org_id=org_id\n            )\n        if e.response.status_code == 403:\n            status(\"this environment requires a valid API key\")\n            return _prompt_validate_api_key_and_list_projects(\n                auth_svc, _prompt_for_api_key(), org_id=org_id\n            )\n        raise\n    except Exception as e:\n        raise click.ClickException(f\"Failed to validate API key: {e}\")\n\n\ndef _select_or_enter_project(\n    projects: list[ProjectSummary], requires_auth: bool\n) -> str | None:\n    if not projects:\n        return None\n    if len(projects) == 1 and requires_auth:\n        return projects[0].project_id\n    return select_or_exit(\n        [\n            (\n                p.project_id,\n                f\"{p.project_id}  {p.project_name} ({p.deployment_count} deployments)\",\n            )\n            for p in projects\n        ],\n        \"Select a project\",\n        hint_flag=\"<project_id>\",\n        hint_command=\"llamactl projects use <project_id>\",\n    )\n\n\ndef _token_flow_for_env(auth_service: AuthService) -> Auth:\n    token_value = _prompt_for_api_key()\n    projects = _prompt_validate_api_key_and_list_projects(auth_service, token_value)\n    project_id = _select_or_enter_project(projects, auth_service.env.requires_auth)\n    if not project_id:\n        raise click.ClickException(\"No project selected\")\n    created = auth_service.create_profile_from_token(project_id, token_value)\n    return created\n\n\ndef _select_profile(\n    auth_svc: AuthService, profile_name: str | None, *, require_selection: bool = False\n) -> Auth | None:\n    \"\"\"\n    Select a profile interactively if name not provided.\n    Returns the selected profile name or None if cancelled.\n\n    In non-interactive sessions, returns None if profile_name is not provided\n    unless ``require_selection`` is true.\n    \"\"\"\n    if profile_name:\n        profile = auth_svc.get_profile(profile_name)\n        if profile:\n            return profile\n\n    if not require_selection and not is_interactive_session():\n        return None\n\n    try:\n        profiles = auth_svc.list_profiles()\n\n        if not profiles:\n            status(\"no profiles found\")\n            return None\n\n        current = auth_svc.get_current_profile()\n        choices = []\n        current_idx = 0\n        for i, profile in enumerate(profiles):\n            title = f\"{profile.name} ({profile.api_url})\"\n            if profile == current:\n                title += \" [current]\"\n                current_idx = i\n            choices.append((profile, title))\n\n        return select_or_exit(\n            choices,\n            \"Select profile:\",\n            hint_flag=\"<name>\",\n            hint_command=\"llamactl auth get\",\n            selected=current_idx,\n        )\n\n    except click.ClickException:\n        raise\n    except Exception as e:\n        warning(f\"error loading profiles: {e}\")\n        return None\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/completion.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport os\nimport re\nfrom pathlib import Path\n\nimport click\nfrom click.shell_completion import get_completion_class\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.options import global_options\nfrom llama_agents.cli.output import status\nfrom llama_agents.cli.paths import (\n    bash_completion_dir,\n    bash_rc_path,\n    fish_completion_dir,\n    zsh_completion_dir,\n    zsh_rc_path,\n)\n\n\n@app.group(help=\"Shell completion helpers.\", no_args_is_help=True)\n@global_options\ndef completion() -> None:\n    pass\n\n\ndef _completion_source(shell: str) -> str:\n    \"\"\"Build and return the shell completion script for the given shell.\"\"\"\n    ctx = click.get_current_context()\n    root_cmd = ctx.find_root().command\n    cls = get_completion_class(shell)\n    if cls is None:\n        raise click.ClickException(f\"Unsupported shell: {shell}\")\n    comp = cls(root_cmd, {}, \"llamactl\", \"_LLAMACTL_COMPLETE\")\n    return comp.source()\n\n\n@completion.command(\"generate\")\n@click.argument(\"shell\", type=click.Choice([\"bash\", \"zsh\", \"fish\"]))\n@global_options\ndef generate(shell: str) -> None:\n    \"\"\"Print shell completion script to stdout.\n\n    Example: llamactl completion generate zsh > ~/.zfunc/_llamactl\n    \"\"\"\n    click.echo(_completion_source(shell))\n\n\n@completion.command(\"install\")\n@click.option(\n    \"--shell\",\n    type=click.Choice([\"bash\", \"zsh\", \"fish\"]),\n    default=None,\n    help=\"Override auto-detected shell.\",\n)\n@click.option(\n    \"--dry-run\",\n    is_flag=True,\n    help=\"Show what would be done without modifying anything.\",\n)\n@global_options\ndef install(shell: str | None, dry_run: bool) -> None:\n    \"\"\"Auto-detect your shell and install completions.\n\n    Example: llamactl completion install\n    \"\"\"\n    if shell is None:\n        shell = _detect_shell()\n\n    source = _completion_source(shell)\n\n    if shell == \"bash\":\n        _install_bash(source, dry_run)\n    elif shell == \"zsh\":\n        _install_zsh(source, dry_run)\n    elif shell == \"fish\":\n        _install_fish(source, dry_run)\n\n    if not dry_run:\n        status(f\"detected shell {shell}\")\n        status(\"restart your shell or source the config to activate completions\")\n\n\n# ---------------------------------------------------------------------------\n# Shell-specific install helpers\n# ---------------------------------------------------------------------------\n\n_MARKER = \"# llamactl shell completion\"\n_ZSH_BLOCK_START = \"# >>> llamactl completion >>>\"\n_ZSH_BLOCK_END = \"# <<< llamactl completion <<<\"\n_ZSH_FPATH_LINE = \"fpath=(~/.zfunc $fpath)\"\n_ZSH_COMPINIT_LINE = \"autoload -Uz compinit && compinit\"\n\n\ndef _detect_shell() -> str:\n    shell_env = os.environ.get(\"SHELL\", \"\")\n    name = os.path.basename(shell_env)\n    if name in (\"bash\", \"zsh\", \"fish\"):\n        return name\n    return \"bash\"\n\n\ndef _install_bash(source: str, dry_run: bool) -> None:\n    comp_dir = bash_completion_dir()\n    target = comp_dir / \"llamactl\"\n\n    if dry_run:\n        status(f\"would write completion script to {target}\")\n        return\n\n    comp_dir.mkdir(parents=True, exist_ok=True)\n    target.write_text(source)\n    status(f\"wrote completions to {target}\")\n\n    # Ensure .bashrc sources the completion dir\n    bashrc = bash_rc_path()\n    _ensure_source_line(\n        bashrc,\n        f\"source {target}\",\n        dry_run,\n    )\n\n\ndef _install_zsh(source: str, dry_run: bool) -> None:\n    zfunc = zsh_completion_dir()\n    target = zfunc / \"_llamactl\"\n\n    if dry_run:\n        status(f\"would write completion script to {target}\")\n        status(\"would ensure ~/.zfunc is in fpath in ~/.zshrc\")\n        return\n\n    zfunc.mkdir(parents=True, exist_ok=True)\n    target.write_text(source)\n    status(f\"wrote completions to {target}\")\n\n    zshrc = zsh_rc_path()\n    _ensure_zsh_fpath(zshrc, dry_run)\n\n\ndef _install_fish(source: str, dry_run: bool) -> None:\n    comp_dir = fish_completion_dir()\n    target = comp_dir / \"llamactl.fish\"\n\n    if dry_run:\n        status(f\"would write completion script to {target}\")\n        return\n\n    comp_dir.mkdir(parents=True, exist_ok=True)\n    target.write_text(source)\n    status(f\"wrote completions to {target}\")\n\n\ndef _ensure_zsh_fpath(zshrc: Path, dry_run: bool) -> None:\n    \"\"\"Ensure llamactl's zsh block appears before the first live compinit.\"\"\"\n    if zshrc.exists():\n        content = zshrc.read_text()\n    else:\n        content = \"\"\n\n    updated_content, actions = _plan_zshrc_update(content)\n    if not actions:\n        return\n\n    if dry_run:\n        for action in actions:\n            status(f\"would update {zshrc}: {action}\")\n        return\n\n    zshrc.write_text(updated_content)\n    for action in actions:\n        status(f\"updated {zshrc}: {action}\")\n\n\ndef _plan_zshrc_update(content: str) -> tuple[str, list[str]]:\n    lines = content.splitlines()\n    had_trailing_newline = content.endswith(\"\\n\")\n    sanitized_lines, removed_block, removed_compinit = _strip_managed_zsh_lines(lines)\n\n    first_compinit_index = _first_live_compinit_index(sanitized_lines)\n    first_fpath_index = _first_live_zfunc_fpath_index(sanitized_lines)\n    needs_fpath_block = (\n        first_fpath_index is None\n        or first_compinit_index is not None\n        and first_fpath_index > first_compinit_index\n    )\n\n    updated_lines = list(sanitized_lines)\n    actions: list[str] = []\n    if needs_fpath_block:\n        insert_at = (\n            first_compinit_index\n            if first_compinit_index is not None\n            else len(updated_lines)\n        )\n        updated_lines[insert_at:insert_at] = _managed_zsh_fpath_block()\n        actions.append(\n            \"ensure ~/.zfunc is added before compinit via the llamactl block\"\n        )\n    elif removed_block:\n        actions.append(\"remove stale llamactl zsh block\")\n\n    has_compinit = _first_live_compinit_index(updated_lines) is not None\n    if not has_compinit:\n        if updated_lines and updated_lines[-1] != \"\":\n            updated_lines.append(\"\")\n        updated_lines.append(f\"{_ZSH_COMPINIT_LINE}  {_MARKER}\")\n        actions.append(\"add compinit because none was configured\")\n    elif removed_compinit:\n        actions.append(\"remove stale llamactl-managed compinit line\")\n\n    updated_content = \"\\n\".join(updated_lines)\n    if updated_content and had_trailing_newline:\n        updated_content += \"\\n\"\n    elif updated_content and not had_trailing_newline:\n        updated_content += \"\\n\"\n    return updated_content, actions\n\n\ndef _strip_managed_zsh_lines(\n    lines: list[str],\n) -> tuple[list[str], bool, bool]:\n    sanitized: list[str] = []\n    in_block = False\n    removed_block = False\n    removed_compinit = False\n\n    for line in lines:\n        stripped = line.strip()\n        if stripped == _ZSH_BLOCK_START:\n            in_block = True\n            removed_block = True\n            continue\n        if stripped == _ZSH_BLOCK_END:\n            in_block = False\n            continue\n        if in_block:\n            continue\n        if stripped == f\"{_ZSH_FPATH_LINE}  {_MARKER}\":\n            removed_block = True\n            continue\n        if stripped == f\"{_ZSH_COMPINIT_LINE}  {_MARKER}\":\n            removed_compinit = True\n            continue\n        sanitized.append(line)\n    return sanitized, removed_block, removed_compinit\n\n\ndef _first_live_compinit_index(lines: list[str]) -> int | None:\n    for index, line in enumerate(lines):\n        if _is_live_compinit_line(line):\n            return index\n    return None\n\n\ndef _first_live_zfunc_fpath_index(lines: list[str]) -> int | None:\n    for index, line in enumerate(lines):\n        stripped = line.lstrip()\n        if stripped.startswith(\"#\"):\n            continue\n        if \"~/.zfunc\" in line and \"fpath\" in line:\n            return index\n    return None\n\n\ndef _is_live_compinit_line(line: str) -> bool:\n    stripped = line.lstrip()\n    if stripped.startswith(\"#\"):\n        return False\n    return bool(re.search(r\"\\bcompinit\\b\", stripped))\n\n\ndef _managed_zsh_fpath_block() -> list[str]:\n    return [\n        _ZSH_BLOCK_START,\n        f\"{_ZSH_FPATH_LINE}  {_MARKER}\",\n        _ZSH_BLOCK_END,\n    ]\n\n\ndef _ensure_source_line(rc_file: Path, line: str, dry_run: bool) -> None:\n    \"\"\"Append line to rc_file if not already present.\"\"\"\n    if rc_file.exists():\n        content = rc_file.read_text()\n        if line in content:\n            return\n    else:\n        content = \"\"\n\n    if dry_run:\n        status(f\"would update {rc_file}: add {line}\")\n        return\n\n    with rc_file.open(\"a\") as f:\n        f.write(f\"\\n{line}  {_MARKER}\\n\")\n    status(f\"updated {rc_file}: add {line}\")\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/config.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport click\nfrom llama_agents.cli.config._config import ConfigManager\nfrom llama_agents.cli.interactive import is_interactive_session\nfrom llama_agents.cli.output import status\nfrom pydantic import BaseModel, ConfigDict\n\nfrom ..app import app\nfrom ..options import global_options, render_output, simple_output_option\nfrom .auth import _get_service, _list_projects\n\n\nclass ConfigContext(BaseModel):\n    model_config = ConfigDict(extra=\"forbid\")\n\n    environment: str | None\n    profile: str | None\n    project_id: str | None\n    project_name: str | None = None\n\n\n@app.group(\n    invoke_without_command=True,\n    help=\"Show local configuration.\",\n)\n@click.pass_context\n@global_options\n@simple_output_option\ndef config(ctx: click.Context, output: str) -> None:\n    if ctx.invoked_subcommand is not None:\n        return\n    context = _build_config_context()\n    render_output(context, output, text_renderer=lambda: _render_config_text(context))\n\n\n@config.command(\"destroy\", hidden=True)\n@global_options\ndef destroy_database() -> None:\n    \"\"\"Destroy the database.\"\"\"\n    if is_interactive_session() and not click.confirm(\n        \"Are you sure you want to destroy all of your local logins? This action cannot be undone.\"\n    ):\n        return\n    ConfigManager(init_database=False).destroy_database()\n    status(\"database destroyed\")\n\n\n@config.command(\"show-db\", hidden=True)\n@global_options\ndef show_database() -> None:\n    \"\"\"Show the database path.\"\"\"\n    path = _get_service().config_manager().db_path\n    status(path)\n\n\ndef _build_config_context() -> ConfigContext:\n    service = _get_service()\n    current_env = service.get_current_environment()\n    auth_svc = service.current_auth_service()\n    profile = auth_svc.get_current_profile()\n    project_id = profile.project_id if profile else None\n    return ConfigContext(\n        environment=current_env.api_url if current_env else None,\n        profile=profile.name if profile else None,\n        project_id=project_id,\n        project_name=_resolve_project_name(auth_svc, project_id),\n    )\n\n\ndef _resolve_project_name(auth_svc: Any, project_id: str | None) -> str | None:\n    if project_id is None:\n        return None\n    try:\n        projects = _list_projects(auth_svc)\n    except Exception:\n        return None\n    project = next(\n        (candidate for candidate in projects if candidate.project_id == project_id),\n        None,\n    )\n    return project.project_name if project else None\n\n\ndef _render_config_text(context: ConfigContext) -> None:\n    project = context.project_id or \"(none)\"\n    if context.project_id and context.project_name:\n        project = f\"{context.project_name} ({context.project_id})\"\n    click.echo(f\"environment:  {context.environment or '(none)'}\")\n    click.echo(f\"profile:      {context.profile or '(none)'}\")\n    click.echo(f\"project:      {project}\")\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/deployment.py",
    "content": "\"\"\"CLI commands for managing LlamaDeploy deployments.\n\nThis command group lets you list, create, edit, refresh, and delete deployments.\nA deployment points the control plane at your Git repository and deployment file\n(e.g., `llama_deploy.yaml`). The control plane pulls your code at the selected\ngit ref, reads the config, and runs your app.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any, Literal, NoReturn\n\nimport click\nimport yaml\nfrom llama_agents.cli.interactive import (\n    is_interactive_session,\n    require_or_list_choices,\n    select_or_exit,\n)\nfrom llama_agents.cli.output import status, warning\nfrom llama_agents.cli.param_types import DeploymentType, GitShaType\nfrom llama_agents.core.git.git_util import is_git_repo\nfrom llama_agents.core.schema import LogEvent\nfrom llama_agents.core.schema.deployments import (\n    INTERNAL_CODE_REPO_SCHEME,\n    DeploymentCreate,\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    DeploymentUpdate,\n)\nfrom llama_agents.core.schema.git_validation import RepositoryValidationResponse\nfrom pydantic import ValidationError\n\nfrom ..app import app\nfrom ..apply_yaml import (\n    SPEC_FIELDS,\n    ApplyYamlError,\n    FieldError,\n    annotate_yaml_with_errors,\n    parse_apply_yaml,\n    parse_delete_yaml_name,\n)\nfrom ..client import get_project_client, project_client_context\nfrom ..display import (\n    PUSH_MODE_REPO_URL,\n    DeploymentDisplay,\n    DeploymentSpec,\n    PayloadError,\n    ReleaseDisplay,\n)\nfrom ..local_context import gather_local_context\nfrom ..log_format import parse_log_body, render_plain\nfrom ..options import (\n    global_options,\n    output_option,\n    output_option_with_template,\n    project_option,\n    render_output,\n)\nfrom ..render import short_sha\nfrom ..utils.git_push import (\n    configure_git_remote,\n    get_deployment_git_url,\n    has_deployment_git_remote,\n    internal_push_refspec,\n    push_to_remote,\n)\nfrom ..yaml_template import render as render_yaml_template\n\nDeploymentApplyMode = Literal[\"apply\", \"create\", \"update\"]\nDeploymentOperationAction = Literal[\"create\", \"update\"]\nPushPolicy = Literal[\"auto\", \"always\", \"never\"]\n\n\n@dataclass(frozen=True)\nclass _DeploymentIntent:\n    display: DeploymentDisplay\n    mode: DeploymentApplyMode\n    update_target: str | None = None\n\n\n@dataclass(frozen=True)\nclass _ResolvedDeploymentOperation:\n    action: DeploymentOperationAction\n    display: DeploymentDisplay\n    payload: DeploymentCreate | DeploymentUpdate\n    existing: DeploymentResponse | None = None\n\n\nclass PushFailedError(click.ClickException):\n    \"\"\"Raised when apply's push step fails.\"\"\"\n\n\nclass RepositoryValidationError(click.ClickException):\n    \"\"\"Raised when validate-repository blocks apply.\"\"\"\n\n    def __init__(self, message: str, path: tuple[str | int, ...]) -> None:\n        self.path = path\n        super().__init__(message)\n\n\ndef _error(path: tuple[str | int, ...], message: str) -> FieldError:\n    return FieldError(path=path, message=message)\n\n\ndef _wire_path_from_loc(\n    loc: tuple[Any, ...], *, display: DeploymentDisplay | None = None\n) -> tuple[str | int, ...]:\n    \"\"\"Map an API/pydantic error ``loc`` back to the corresponding YAML path.\"\"\"\n    parts = tuple(\n        part\n        for part in loc\n        if isinstance(part, (str, int)) and part not in {\"body\", \"query\"}\n    )\n    if not parts:\n        if display is not None and display.name is not None:\n            return (\"name\",)\n        return ()\n    if parts[0] == \"id\":\n        return (\"name\", *parts[1:])\n    if parts[0] == \"display_name\":\n        return (\"generate_name\", *parts[1:])\n    if parts[0] in SPEC_FIELDS or parts[0] == \"secrets\":\n        return (\"spec\", *parts)\n    return ()\n\n\ndef _http_error_to_field_errors(\n    exc: Any, *, display: DeploymentDisplay | None = None\n) -> list[FieldError]:\n    \"\"\"Extract structured field errors from an ``httpx.HTTPStatusError``.\"\"\"\n    try:\n        detail = exc.response.json().get(\"detail\")\n    except ValueError:\n        return [_error((), str(exc))]\n\n    if isinstance(detail, list):\n        errors: list[FieldError] = []\n        for item in detail:\n            if not isinstance(item, dict):\n                errors.append(_error((), str(item)))\n                continue\n            loc = item.get(\"loc\")\n            message = str(item.get(\"msg\", item))\n            if isinstance(loc, (list, tuple)):\n                path = _wire_path_from_loc(tuple(loc), display=display)\n            else:\n                path = ()\n            errors.append(_error(path, message))\n        return errors\n    if isinstance(detail, str):\n        return [_error((), detail)]\n    return [_error((), str(exc))]\n\n\n_PYDANTIC_VALUE_ERROR_PREFIX = \"Value error, \"\n\n\ndef _strip_pydantic_prefix(msg: str) -> str:\n    if msg.startswith(_PYDANTIC_VALUE_ERROR_PREFIX):\n        return msg[len(_PYDANTIC_VALUE_ERROR_PREFIX) :]\n    return msg\n\n\ndef _validation_error_to_field_errors(\n    exc: ValidationError, *, display: DeploymentDisplay\n) -> list[FieldError]:\n    return [\n        _error(\n            _wire_path_from_loc(tuple(d[\"loc\"]), display=display),\n            _strip_pydantic_prefix(str(d[\"msg\"])),\n        )\n        for d in exc.errors()\n    ]\n\n\ndef _validate_dry_run_payload(display: DeploymentDisplay) -> None:\n    try:\n        if display.name:\n            display.to_update_payload()\n        elif display.generate_name:\n            display.to_create_payload()\n        else:\n            msg = \"YAML must include top-level 'name' or 'generate_name'\"\n            raise ApplyYamlError(msg, errors=[_error((\"generate_name\",), msg)])\n    except PayloadError as exc:\n        raise ApplyYamlError(str(exc), errors=[_error(exc.path, str(exc))]) from exc\n    except ValidationError as exc:\n        raise ApplyYamlError(\n            str(exc),\n            errors=_validation_error_to_field_errors(exc, display=display),\n            original_error=exc,\n        ) from exc\n\n\ndef _repository_error_path(\n    message: str, display: DeploymentDisplay\n) -> tuple[str | int, ...]:\n    lowered = message.lower()\n    if any(token in lowered for token in (\"auth\", \"pat\", \"token\", \"403\")):\n        return (\"spec\", \"personal_access_token\")\n    if display.spec.repo_url:\n        return (\"spec\", \"repo_url\")\n    return ()\n\n\ndef _github_app_access_url(vr: RepositoryValidationResponse) -> str | None:\n    return vr.github_app_settings_url or vr.github_app_installation_url\n\n\ndef _is_github_app_connect_url(url: str | None) -> bool:\n    return (\n        url is not None\n        and \"/api/internal/external-credentials/github-app/connect\" in url\n    )\n\n\ndef _github_app_authorization_url(vr: RepositoryValidationResponse) -> str | None:\n    if vr.github_app_authorization_url:\n        return vr.github_app_authorization_url\n    if _is_github_app_connect_url(vr.github_app_installation_url):\n        return vr.github_app_installation_url\n    return None\n\n\ndef _github_app_recovery_url(vr: RepositoryValidationResponse) -> str | None:\n    return _github_app_authorization_url(vr) or _github_app_access_url(vr)\n\n\ndef _repository_validation_error_message(vr: RepositoryValidationResponse) -> str:\n    message = vr.message\n    authorization_url = _github_app_authorization_url(vr)\n    if authorization_url:\n        message += f\"\\n\\nConnect GitHub: {authorization_url}\"\n    else:\n        url = _github_app_access_url(vr)\n        if url:\n            message += f\"\\n\\nInstall the GitHub App: {url}\"\n    return message\n\n\nclass _GitHubCallbackServer:\n    \"\"\"Local callback server for GitHub OAuth/App redirects.\"\"\"\n\n    def __init__(self, port: int = 41010) -> None:\n        self.port = port\n        self.callback_received = asyncio.Event()\n        self.app: Any | None = None\n        self.runner: Any | None = None\n        self.site: Any | None = None\n\n    async def start(self) -> None:\n        # Deferred: aiohttp is only needed for the browser callback path.\n        from aiohttp.web_app import Application\n        from aiohttp.web_response import Response\n        from aiohttp.web_runner import AppRunner, TCPSite\n\n        async def _handle_callback(_: Any) -> Response:\n            self.callback_received.set()\n            return Response(text=self._success_html(), content_type=\"text/html\")\n\n        self.app = Application()\n        self.app.router.add_get(\"/\", _handle_callback)\n        self.app.router.add_get(\"/{path:.*}\", _handle_callback)\n        self.runner = AppRunner(self.app, logger=None)\n        await self.runner.setup()\n        self.site = TCPSite(self.runner, \"localhost\", self.port)\n        await self.site.start()\n\n    async def wait(self, timeout: float) -> None:\n        try:\n            await asyncio.wait_for(self.callback_received.wait(), timeout=timeout)\n        except asyncio.TimeoutError:\n            raise TimeoutError(f\"GitHub callback timed out after {timeout} seconds\")\n\n    async def stop(self) -> None:\n        if self.site is not None:\n            await self.site.stop()\n            self.site = None\n        if self.runner is not None:\n            await self.runner.cleanup()\n            self.runner = None\n        self.app = None\n        self.callback_received.clear()\n\n    def _success_html(self) -> str:\n        return (\n            \"<!DOCTYPE html>\"\n            \"<html>\"\n            \"<head><title>llamactl - Authentication Complete</title></head>\"\n            \"<body>\"\n            \"<h1>Authentication complete</h1>\"\n            \"<p>Return to your terminal to continue.</p>\"\n            \"</body>\"\n            \"</html>\"\n        )\n\n\nasync def _wait_for_callback_or_interval(\n    server: _GitHubCallbackServer,\n    interval: int,\n) -> bool:\n    callback_task = asyncio.create_task(server.wait(timeout=300))\n    sleep_task = asyncio.create_task(asyncio.sleep(interval))\n    done, pending = await asyncio.wait(\n        {callback_task, sleep_task},\n        return_when=asyncio.FIRST_COMPLETED,\n    )\n    for task in pending:\n        task.cancel()\n    for task in done:\n        task.result()\n    return callback_task in done\n\n\nasync def _open_github_url_and_poll_access(\n    client: Any,\n    *,\n    url: str,\n    wait_message: str,\n    repo_url: str,\n    deployment_id: str | None,\n    pat: str | None,\n) -> RepositoryValidationResponse:\n    # Deferred: browser launch support is only needed on this auth recovery path.\n    import webbrowser\n\n    server = _GitHubCallbackServer()\n    await server.start()\n    try:\n        click.echo(f\"Open this URL: {url}\", err=True)\n        webbrowser.open(url)\n\n        interval = 10\n        elapsed = 0\n        while True:\n            callback_received = await _wait_for_callback_or_interval(server, interval)\n            if not callback_received:\n                elapsed += interval\n                click.echo(\n                    f\"\\r● {wait_message}... ({elapsed}s)\",\n                    nl=False,\n                    err=True,\n                )\n            vr = await client.validate_repository(\n                repo_url=repo_url,\n                deployment_id=deployment_id,\n                pat=pat,\n            )\n            if vr.accessible:\n                click.echo(err=True)\n                return vr\n            if callback_received:\n                return vr\n    finally:\n        await server.stop()\n\n\nasync def _resolve_github_app_access(\n    client: Any,\n    vr: RepositoryValidationResponse,\n    repo_url: str,\n    deployment_id: str | None,\n    pat: str | None,\n) -> RepositoryValidationResponse:\n    \"\"\"Open GitHub App access URL and poll until repo validation succeeds.\"\"\"\n    authorization_url = _github_app_authorization_url(vr)\n    install_url = _github_app_access_url(vr)\n    if authorization_url == install_url:\n        install_url = None\n    if authorization_url is None and install_url is None:\n        return vr\n\n    app_name = vr.github_app_name or \"configured\"\n    click.echo(\n        f\"GitHub App '{app_name}' does not have access to this repository.\",\n        err=True,\n    )\n\n    if authorization_url is not None:\n        click.echo(\"Opening browser to connect GitHub...\", err=True)\n        vr = await _open_github_url_and_poll_access(\n            client,\n            url=authorization_url,\n            wait_message=\"waiting for GitHub authorization\",\n            repo_url=repo_url,\n            deployment_id=deployment_id,\n            pat=pat,\n        )\n        if vr.accessible:\n            return vr\n        install_url = _github_app_access_url(vr)\n\n    if install_url is None:\n        return vr\n\n    click.echo(\n        \"Opening browser to install the GitHub App... (press Ctrl+C to cancel)\",\n        err=True,\n    )\n    return await _open_github_url_and_poll_access(\n        client,\n        url=install_url,\n        wait_message=\"waiting for GitHub App installation\",\n        repo_url=repo_url,\n        deployment_id=deployment_id,\n        pat=pat,\n    )\n\n\ndef _read_apply_input(filename: str) -> str:\n    if filename == \"-\":\n        return click.get_text_stream(\"stdin\").read()\n    return Path(filename).read_text()\n\n\ndef _handle_annotated_apply_error(\n    *,\n    filename: str,\n    text: str,\n    errors: list[FieldError],\n) -> None:\n    annotated = annotate_yaml_with_errors(text, errors)\n    if filename == \"-\":\n        click.echo(annotated, nl=False)\n    else:\n        Path(filename).write_text(annotated)\n        click.echo(f\"apply failed; see annotations in {filename}\", err=True)\n    raise click.exceptions.Exit(1)\n\n\ndef _format_apply_yaml_error(exc: ApplyYamlError) -> str:\n    if len(exc.errors) <= 1:\n        return str(exc)\n\n    lines = [\"deployment YAML has errors:\"]\n    for error in exc.errors:\n        if error.path:\n            path = \".\".join(str(part) for part in error.path)\n            lines.append(f\"- {path}: {error.message}\")\n        else:\n            lines.append(f\"- {error.message}\")\n    return \"\\n\".join(lines)\n\n\ndef _raise_apply_yaml_click_error(exc: ApplyYamlError) -> NoReturn:\n    raise click.ClickException(_format_apply_yaml_error(exc)) from exc\n\n\ndef _new_deployment_template_yaml(\n    *,\n    action_hint: str = \"Edit, then run: llamactl deployments apply -f <file>\",\n    logged_in_email: str | None = None,\n) -> str:\n    ctx = gather_local_context()\n\n    cwd_name: str = Path.cwd().name\n    preferred_name: str = ctx.generate_name or cwd_name\n    secrets: dict[str, str | None] | None = None\n    if ctx.required_secret_names:\n        secrets = {name: f\"${{{name}}}\" for name in ctx.required_secret_names}\n\n    if ctx.is_git_repo:\n        spec = DeploymentSpec(\n            repo_url=PUSH_MODE_REPO_URL,\n            deployment_file_path=ctx.deployment_file_path,\n            git_ref=ctx.git_ref,\n            appserver_version=ctx.installed_appserver_version,\n            secrets=secrets,\n        )\n        required: tuple[str, ...] = ()\n    else:\n        spec = DeploymentSpec(\n            appserver_version=ctx.installed_appserver_version,\n            secrets=secrets,\n        )\n        required = (\"repo_url\",)\n\n    display = DeploymentDisplay(name=None, generate_name=preferred_name, spec=spec)\n\n    head: list[str] = []\n    if logged_in_email:\n        head.append(f\"Logged in as {logged_in_email}\")\n    head.extend(f\"WARNING: {warning}\" for warning in ctx.warnings)\n    if ctx.warnings:\n        head.append(\"\")\n    head.append(action_hint)\n    if not ctx.is_git_repo:\n        head.extend(\n            [\n                \"\",\n                \"NOT IN A GIT REPO — set repo_url, or cd into a working tree \"\n                \"and re-run.\",\n            ]\n        )\n\n    field_alternatives: dict[str, tuple[str, str]] = {}\n    if ctx.is_git_repo and ctx.repo_url:\n        field_alternatives[\"repo_url\"] = (\n            ctx.repo_url,\n            \"auto-detected from your git remotes\",\n        )\n\n    secret_comments: dict[str, str] = {}\n    for name_ in ctx.required_secret_names:\n        if name_ in ctx.available_secrets:\n            secret_comments[name_] = \"from your .env\"\n        else:\n            secret_comments[name_] = \"not in your .env — add it before apply\"\n\n    return render_yaml_template(\n        display,\n        head=head,\n        secret_comments=secret_comments,\n        field_alternatives=field_alternatives,\n        required=required,\n        name_example=preferred_name,\n        scaffold_generate_name=True,\n    )\n\n\ndef _existing_deployment_template_yaml(deployment: DeploymentResponse) -> str:\n    return render_yaml_template(DeploymentDisplay.from_response(deployment))\n\n\ndef _existing_deployment_editor_yaml(deployment: DeploymentResponse) -> str:\n    return render_yaml_template(DeploymentDisplay.from_response(deployment))\n\n\ndef _parse_deployment_yaml_text(text: str) -> DeploymentDisplay:\n    return parse_apply_yaml(text)\n\n\ndef _push_policy_from_flags(*, push: bool = False, no_push: bool = False) -> PushPolicy:\n    if push and no_push:\n        raise click.ClickException(\"--push and --no-push are mutually exclusive\")\n    if push:\n        return \"always\"\n    if no_push:\n        return \"never\"\n    return \"auto\"\n\n\ndef _warn_missing_deployment_remote(deployment_id: str) -> None:\n    remote_name = f\"llamaagents-{deployment_id}\"\n    warning(\n        f\"not pushing code; no {remote_name} remote in this repo. \"\n        f\"Run llamactl deployments configure-git-remote {deployment_id} \"\n        \"or pass --push.\"\n    )\n\n\nasync def _apply_deployment_intent(\n    *,\n    project: str | None,\n    intent: _DeploymentIntent,\n    push_policy: PushPolicy = \"auto\",\n) -> None:\n    async with project_client_context(project_id_override=project) as client:\n        await _apply_deployment_from_yaml(\n            client,\n            intent.display,\n            push_policy=push_policy,\n            mode=intent.mode,\n            update_target=intent.update_target,\n        )\n\n\ndef _apply_deployment_display(\n    display: DeploymentDisplay,\n    *,\n    project: str | None,\n    push_policy: PushPolicy = \"auto\",\n    mode: DeploymentApplyMode = \"apply\",\n    update_target: str | None = None,\n) -> None:\n    asyncio.run(\n        _apply_deployment_intent(\n            project=project,\n            intent=_DeploymentIntent(\n                display=display,\n                mode=mode,\n                update_target=update_target,\n            ),\n            push_policy=push_policy,\n        )\n    )\n\n\ndef _apply_deployment_yaml_text(\n    text: str,\n    *,\n    project: str | None,\n    push_policy: PushPolicy = \"auto\",\n    mode: DeploymentApplyMode = \"apply\",\n    update_target: str | None = None,\n) -> None:\n    display = _parse_deployment_yaml_text(text)\n    _apply_deployment_display(\n        display,\n        project=project,\n        push_policy=push_policy,\n        mode=mode,\n        update_target=update_target,\n    )\n\n\ndef _apply_deployment_yaml_file(\n    *,\n    filename: str,\n    project: str | None,\n    push_policy: PushPolicy,\n    mode: DeploymentApplyMode = \"apply\",\n    update_target: str | None = None,\n) -> None:\n    text = _read_apply_input(filename)\n    try:\n        display = _parse_deployment_yaml_text(text)\n    except ApplyYamlError as exc:\n        _raise_apply_yaml_click_error(exc)\n\n    try:\n        _apply_deployment_display(\n            display,\n            project=project,\n            push_policy=push_policy,\n            mode=mode,\n            update_target=update_target,\n        )\n    except ApplyYamlError as exc:\n        _raise_apply_yaml_click_error(exc)\n\n\nasync def _fetch_deployment_for_editor(\n    *, project: str | None, deployment_id: str\n) -> tuple[str, DeploymentResponse]:\n    async with project_client_context(project_id_override=project) as client:\n        return client.project_id, await client.get_deployment(deployment_id)\n\n\ndef _requires_file_for_editor() -> bool:\n    return not is_interactive_session()\n\n\ndef _has_non_comment_yaml_lines(text: str) -> bool:\n    return any(\n        stripped and not stripped.startswith(\"#\")\n        for stripped in (line.lstrip() for line in text.splitlines())\n    )\n\n\ndef _open_deployment_yaml_editor(current_text: str) -> str | None:\n    return click.edit(text=current_text, extension=\".yaml\")\n\n\ndef _editor_cancelled() -> None:\n    status(\"cancelled\")\n\n\ndef _editor_text_unchanged(current_text: str, last_opened_text: str) -> bool:\n    return current_text.rstrip(\"\\n\") == last_opened_text.rstrip(\"\\n\")\n\n\ndef _editor_noop(mode: DeploymentApplyMode) -> None:\n    if mode == \"create\":\n        status(\"no changes saved; fill in the YAML before creating\")\n    else:\n        status(\"no changes saved\")\n\n\ndef _editor_empty() -> None:\n    status(\"no deployment YAML saved; nothing applied\")\n\n\ndef _editor_comments_only() -> None:\n    status(\"no deployment fields saved; add YAML fields and run again\")\n\n\ndef _edit_deployment_yaml_loop(\n    *,\n    initial_yaml: str,\n    project: str | None,\n    push_policy: PushPolicy,\n    mode: DeploymentApplyMode,\n    update_target: str | None = None,\n) -> None:\n    last_opened_text = initial_yaml\n    while True:\n        current_text = _open_deployment_yaml_editor(last_opened_text)\n\n        if current_text is None:\n            _editor_cancelled()\n            return\n        if _editor_text_unchanged(current_text, last_opened_text):\n            _editor_noop(mode)\n            return\n        if not current_text.strip():\n            _editor_empty()\n            return\n        if not _has_non_comment_yaml_lines(current_text):\n            _editor_comments_only()\n            return\n\n        try:\n            _apply_deployment_yaml_text(\n                current_text,\n                project=project,\n                push_policy=push_policy,\n                mode=mode,\n                update_target=update_target,\n            )\n            return\n        except ApplyYamlError as exc:\n            last_opened_text = annotate_yaml_with_errors(current_text, exc.errors)\n\n\n@app.group(\n    help=\"Deploy your app to the cloud.\",\n    no_args_is_help=True,\n)\n@global_options\ndef deployments() -> None:\n    \"\"\"Manage deployments\"\"\"\n    pass\n\n\ndef friendly_http_error(\n    exc: Exception,\n    *,\n    deployment_id: str | None = None,\n    project_id: str | None = None,\n) -> str | None:\n    \"\"\"Translate well-known HTTP errors into a one-line CLI message.\n\n    Returns ``None`` when the caller should fall back to the verbose default\n    rendering. We only collapse the cases where a richer message would just\n    be debug noise to the user — currently a 404 on a known deployment id.\n    Other 4xx/5xx and non-HTTP errors keep their existing message so we\n    don't swallow useful info on unexpected paths.\n    \"\"\"\n    # Defer httpx import: `llamactl --help` is held to a no-httpx startup\n    # budget by tests/test_cli_imports.py; only error paths need the type.\n    import httpx\n\n    if not isinstance(exc, httpx.HTTPStatusError):\n        return None\n    if exc.response.status_code != 404 or not deployment_id:\n        return None\n    msg = f\"deployment '{deployment_id}' not found\"\n    if project_id:\n        msg += f\" in project '{project_id}'\"\n    return msg\n\n\ndef _do_get(\n    deployment_id: str | None,\n    output: str,\n    project: str | None,\n) -> None:\n    \"\"\"Implementation of ``deployments get`` shared with the hidden ``list`` alias.\n\n    No ``deployment_id`` → list all deployments (kubectl-style). With an ID →\n    a single-row table for that deployment. For a live view use\n    ``deployments logs --follow``.\n    \"\"\"\n    mode = output.lower()\n    if mode == \"template\" and not deployment_id:\n        raise click.ClickException(\"-o template requires a deployment name\")\n\n    # Fall back to the user-supplied override if client construction itself\n    # raises; `client.project_id` resolves the active project when no override.\n    effective_project: str | None = project\n    try:\n        client = get_project_client(project_id_override=project)\n        effective_project = client.project_id\n\n        if not deployment_id:\n            deployments = asyncio.run(client.list_deployments())\n\n            if not deployments and mode == \"text\":\n                status(f\"no deployments found for project {client.project_id}\")\n                return\n\n            displays = [DeploymentDisplay.from_response(d) for d in deployments]\n            render_output(displays, output)\n            return\n\n        deployment = asyncio.run(client.get_deployment(deployment_id))\n        display = DeploymentDisplay.from_response(deployment)\n        if mode == \"template\":\n            click.echo(_existing_deployment_template_yaml(deployment), nl=False)\n            return\n        render_output(display, output)\n\n    except Exception as e:\n        friendly = friendly_http_error(\n            e, deployment_id=deployment_id, project_id=effective_project\n        )\n        message = friendly if friendly is not None else str(e)\n        raise click.ClickException(message) from e\n\n\n@deployments.command(\"get\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@output_option_with_template\n@project_option\ndef get_deployment(\n    deployment_id: str | None,\n    output: str,\n    project: str | None,\n) -> None:\n    \"\"\"Get one or more deployments.\n\n    With no argument: lists all deployments in the project (kubectl-style).\n    With a deployment ID: prints details for that deployment.\n\n    Use ``-o json`` or ``-o yaml`` for machine-readable output. Use\n    ``llamactl deployments logs <name> --follow`` to stream logs.\n    \"\"\"\n    _do_get(deployment_id, output, project)\n\n\n@deployments.command(\"list\", hidden=True)\n@global_options\n@output_option\n@project_option\ndef list_deployments(\n    output: str,\n    project: str | None,\n) -> None:\n    \"\"\"Hidden alias for ``deployments get``. Kept for backward compatibility.\"\"\"\n    _do_get(None, output, project)\n\n\n@deployments.command(\"template\")\n@global_options\ndef template_deployment() -> None:\n    \"\"\"Print an apply-shaped YAML scaffold for a new deployment.\n\n    Reads the local working tree (git remote and ref, deployment config,\n    .env, required secrets) and emits a YAML scaffold with ``##`` instruction\n    comments. Edit the output, then run ``llamactl deployments apply -f\n    <file>``. Offline by design — no auth profile required.\n    \"\"\"\n    click.echo(_new_deployment_template_yaml(), nl=False)\n\n\n@deployments.command(\"create\")\n@global_options\n@click.option(\n    \"-f\",\n    \"--filename\",\n    default=None,\n    type=click.Path(allow_dash=True, exists=True, dir_okay=False, path_type=str),\n    help=\"Path to YAML file, or '-' for stdin.\",\n)\n@click.option(\n    \"--no-push\",\n    is_flag=True,\n    default=False,\n    help=\"Skip pushing local code even when the deployment uses push-mode.\",\n)\n@project_option\ndef create_deployment(\n    filename: str | None,\n    no_push: bool,\n    project: str | None,\n) -> None:\n    \"\"\"Create a new deployment.\"\"\"\n    push_policy = _push_policy_from_flags(no_push=no_push)\n    if filename is not None:\n        _apply_deployment_yaml_file(\n            filename=filename,\n            project=project,\n            push_policy=push_policy,\n            mode=\"create\",\n        )\n        return\n\n    if _requires_file_for_editor():\n        raise click.ClickException(\"pass -f <file> for non-interactive create\")\n\n    # Deferred: auth command imports browser/prompt dependencies and client.py\n    # imports this command module indirectly during inline auth.\n    from llama_agents.cli.commands.auth import validate_authenticated_profile\n\n    profile = validate_authenticated_profile()\n    logged_in_email = profile.device_oidc.email if profile.device_oidc else None\n\n    _edit_deployment_yaml_loop(\n        initial_yaml=_new_deployment_template_yaml(\n            action_hint=\"Edit, save, and close to create the deployment\",\n            logged_in_email=logged_in_email,\n        ),\n        project=project,\n        push_policy=push_policy,\n        mode=\"create\",\n    )\n\n\n@deployments.command(\"configure-git-remote\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@project_option\ndef configure_git_remote_cmd(deployment_id: str | None, project: str | None) -> None:\n    \"\"\"Configure a git remote for a deployment.\n\n    Sets up authentication and a git remote named 'llamaagents-<deployment_id>'\n    so you can push with:\n      git push llamaagents-<deployment_id>\n\n    Tip: 'llamactl deployments update' handles pushing and redeployment in one\n    step. This command is useful for troubleshooting git push issues.\n    \"\"\"\n    deployment_id = _require_deployment_id(\n        deployment_id, \"configure-git-remote\", project\n    )\n    try:\n        if not is_git_repo():\n            raise click.ClickException(\"Not a git repository\")\n\n        client = get_project_client(project_id_override=project)\n        git_url = get_deployment_git_url(client.base_url, deployment_id)\n        remote_name = configure_git_remote(\n            git_url, client.api_key, client.project_id, deployment_id\n        )\n\n        status(f\"configured git remote {remote_name} for {deployment_id}\")\n        status(f\"push with: git push {remote_name}\")\n\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@deployments.command(\"delete\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@click.option(\n    \"-f\",\n    \"--filename\",\n    default=None,\n    type=click.Path(allow_dash=True, exists=True, dir_okay=False, path_type=str),\n    help=\"Path to YAML file; name is read from the file. Mutually exclusive with positional ID.\",\n)\n@project_option\ndef delete_deployment(\n    deployment_id: str | None,\n    filename: str | None,\n    project: str | None,\n) -> None:\n    \"\"\"Delete a deployment\"\"\"\n    if filename is not None and deployment_id is not None:\n        raise click.ClickException(\n            \"--filename and deployment ID are mutually exclusive\"\n        )\n\n    if filename is not None:\n        try:\n            deployment_id = parse_delete_yaml_name(_read_apply_input(filename))\n        except ApplyYamlError as exc:\n            raise click.ClickException(str(exc)) from exc\n\n    deployment_id = _require_deployment_id(deployment_id, \"delete\", project)\n\n    try:\n        client = get_project_client(project_id_override=project)\n\n        asyncio.run(client.delete_deployment(deployment_id))\n        status(f\"deleted {deployment_id}\")\n\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\ndef _apply_push(\n    client: Any,\n    deployment_id: str,\n    git_ref: str | None,\n) -> None:\n    \"\"\"Push local code to the deployment's internal bare repo.\n\n    Used in the push-then-save flow (existing push-mode → push-mode update)\n    and also called by ``_apply_push_after_save`` for the save-then-push flow.\n    Raises ``click.ClickException`` on failure.\n    \"\"\"\n    git_url = get_deployment_git_url(client.base_url, deployment_id)\n    remote_name = configure_git_remote(\n        git_url, client.api_key, client.project_id, deployment_id\n    )\n    local_ref, target_ref = internal_push_refspec(git_ref)\n    status(\"pushing code\")\n    push_result = push_to_remote(\n        remote_name, local_ref=local_ref, target_ref=target_ref\n    )\n    if push_result.returncode != 0:\n        stderr = \" \".join(\n            push_result.stderr.decode(errors=\"replace\").strip().splitlines()\n        )\n        raise PushFailedError(\n            f\"push failed: {stderr}\\n\"\n            f\"To debug, try: llamactl deployments configure-git-remote {deployment_id}\"\n        )\n\n\ndef _apply_push_after_save(\n    client: Any,\n    deployment_id: str,\n    git_ref: str | None,\n) -> None:\n    \"\"\"Push after a successful create/update (bootstrap push).\n\n    On push failure the save already succeeded, so the error message includes\n    a recovery hint. Raises ``click.ClickException`` on failure.\n    \"\"\"\n    try:\n        _apply_push(client, deployment_id, git_ref)\n    except PushFailedError as exc:\n        raise PushFailedError(\n            f\"{exc.message}\\n\"\n            \"re-run `llamactl deployments apply -f <file>` to retry the push\"\n        ) from exc\n\n\ndef _create_identity_error() -> FieldError:\n    msg = \"set top-level 'name' or 'generate_name'\"\n    return _error((\"name\",), msg)\n\n\ndef _create_source_error() -> FieldError:\n    msg = 'set spec.repo_url for create (use \"\" for push-mode)'\n    return _error((\"spec\", \"repo_url\"), msg)\n\n\ndef _normalize_create_display(display: DeploymentDisplay) -> DeploymentDisplay:\n    if display.name is not None and display.generate_name is None:\n        return display.model_copy(update={\"generate_name\": display.name})\n    return display\n\n\ndef _validate_create_intent(display: DeploymentDisplay) -> None:\n    errors: list[FieldError] = []\n    if not display.name and not display.generate_name:\n        errors.append(_create_identity_error())\n    if \"repo_url\" not in display.spec.model_fields_set or display.spec.repo_url is None:\n        errors.append(_create_source_error())\n    if errors:\n        message = (\n            errors[0].message if len(errors) == 1 else \"deployment YAML has errors\"\n        )\n        raise ApplyYamlError(message, errors=errors)\n\n\nasync def _resolve_deployment_operation(\n    client: Any,\n    intent: _DeploymentIntent,\n) -> _ResolvedDeploymentOperation:\n    # Deferred: llamactl startup budget avoids importing httpx at module level.\n    import httpx\n\n    display = intent.display\n\n    if intent.mode == \"create\":\n        display = _normalize_create_display(display)\n        _validate_create_intent(display)\n        return _ResolvedDeploymentOperation(\n            action=\"create\",\n            display=display,\n            payload=display.to_create_payload(),\n        )\n\n    if intent.mode == \"update\":\n        update_target = intent.update_target or display.name\n        if update_target is None:\n            msg = \"YAML must include top-level 'name' for edit\"\n            raise ApplyYamlError(msg, errors=[_error((\"name\",), msg)])\n        if display.name is not None and display.name != update_target:\n            msg = (\n                f\"YAML name '{display.name}' does not match deployment \"\n                f\"'{update_target}'\"\n            )\n            raise ApplyYamlError(msg, errors=[_error((\"name\",), msg)])\n\n        display = display.model_copy(update={\"name\": update_target})\n        existing = await client.get_deployment(update_target)\n        return _ResolvedDeploymentOperation(\n            action=\"update\",\n            display=display,\n            payload=display.to_update_payload(),\n            existing=existing,\n        )\n\n    if display.name is not None:\n        try:\n            existing = await client.get_deployment(display.name)\n            return _ResolvedDeploymentOperation(\n                action=\"update\",\n                display=display,\n                payload=display.to_update_payload(),\n                existing=existing,\n            )\n        except httpx.HTTPStatusError as exc:\n            if exc.response.status_code != 404:\n                raise\n\n    display = _normalize_create_display(display)\n    _validate_create_intent(display)\n    return _ResolvedDeploymentOperation(\n        action=\"create\",\n        display=display,\n        payload=display.to_create_payload(),\n    )\n\n\nasync def _execute_deployment_operation(\n    client: Any,\n    operation: _ResolvedDeploymentOperation,\n    *,\n    push_policy: PushPolicy = \"auto\",\n) -> None:\n    display = operation.display\n    existing = operation.existing\n    is_update = operation.action == \"update\"\n\n    # Pre-flight validate-repository (skipped for push-mode, dry-run,\n    # and when repo_url is unset on the update path).\n    repo_url = display.spec.repo_url\n    skip_validation = (\n        repo_url is None\n        or repo_url == \"\"\n        or repo_url == INTERNAL_CODE_REPO_SCHEME\n        or (is_update and \"repo_url\" not in display.spec.model_fields_set)\n    )\n    if not skip_validation:\n        assert repo_url is not None\n        vr = await client.validate_repository(\n            repo_url=repo_url,\n            deployment_id=existing.id if existing else None,\n            pat=display.spec.personal_access_token,\n        )\n        if not vr.accessible:\n            if _github_app_recovery_url(vr) and is_interactive_session():\n                vr = await _resolve_github_app_access(\n                    client,\n                    vr,\n                    repo_url,\n                    existing.id if existing else None,\n                    display.spec.personal_access_token,\n                )\n        if not vr.accessible:\n            message = _repository_validation_error_message(vr)\n            raise RepositoryValidationError(\n                message, _repository_error_path(message, display)\n            )\n\n    # Push ordering matrix:\n    #   (no deployment, push)    -> save then push (bootstrap)\n    #   (push, push)             -> push then save (bare repo must hold\n    #                              new ref before update resolves git_ref)\n    #   (external, push)         -> save then push (switch into push mode)\n    #   (*, external)            -> save only\n    #   (*, same-as-current)     -> save only (repo_url omitted in YAML)\n    current_is_push = (\n        existing is not None and existing.repo_url == INTERNAL_CODE_REPO_SCHEME\n    )\n    if \"repo_url\" not in display.spec.model_fields_set:\n        desired_is_push = current_is_push\n    elif display.spec.repo_url in {\"\", INTERNAL_CODE_REPO_SCHEME}:\n        desired_is_push = True\n    else:\n        desired_is_push = False\n\n    push_before_save = current_is_push and desired_is_push\n\n    if desired_is_push and push_policy == \"never\":\n        desired_is_push = False\n        push_before_save = False\n\n    if desired_is_push and not is_git_repo():\n        warning(\"not in a git repo; skipping push, server will use last pushed code\")\n        desired_is_push = False\n        push_before_save = False\n\n    if (\n        push_before_save\n        and push_policy == \"auto\"\n        and existing is not None\n        and not has_deployment_git_remote(existing.id)\n    ):\n        _warn_missing_deployment_remote(existing.id)\n        desired_is_push = False\n        push_before_save = False\n\n    if push_before_save:\n        assert existing is not None and display.name is not None\n        _apply_push(\n            client,\n            existing.id,\n            display.spec.git_ref or existing.git_ref,\n        )\n        response = await client.update_deployment(display.name, operation.payload)\n        status(f\"updated {response.id}\")\n    elif operation.action == \"update\":\n        assert display.name is not None\n        response = await client.update_deployment(display.name, operation.payload)\n        status(f\"updated {response.id}\")\n        if desired_is_push:\n            _apply_push_after_save(client, response.id, display.spec.git_ref)\n    else:\n        response = await client.create_deployment(operation.payload)\n        status(f\"created {response.id}\")\n        if desired_is_push:\n            _apply_push_after_save(client, response.id, display.spec.git_ref)\n\n\ndef _create_conflict_error(display: DeploymentDisplay) -> ApplyYamlError:\n    deployment_id = display.name or display.generate_name or \"deployment\"\n    msg = (\n        f\"deployment '{deployment_id}' already exists; use \"\n        \"`llamactl deployments edit -f` or `llamactl deployments apply -f` \"\n        \"to update\"\n    )\n    return ApplyYamlError(msg, errors=[_error((\"name\",), msg)])\n\n\nasync def _apply_deployment_from_yaml(\n    client: Any,\n    display: DeploymentDisplay,\n    *,\n    push_policy: PushPolicy = \"auto\",\n    mode: DeploymentApplyMode = \"apply\",\n    update_target: str | None = None,\n) -> None:\n    # Deferred: llamactl startup budget avoids importing httpx at module level.\n    import httpx\n\n    try:\n        intent = _DeploymentIntent(\n            display=display,\n            mode=mode,\n            update_target=update_target,\n        )\n        operation = await _resolve_deployment_operation(client, intent)\n        await _execute_deployment_operation(client, operation, push_policy=push_policy)\n    except ApplyYamlError:\n        raise\n    except RepositoryValidationError as exc:\n        raise ApplyYamlError(\n            exc.message, errors=[_error(exc.path, exc.message)]\n        ) from exc\n    except PushFailedError as exc:\n        raise ApplyYamlError(exc.message, errors=[_error((), exc.message)]) from exc\n    except PayloadError as exc:\n        raise ApplyYamlError(str(exc), errors=[_error(exc.path, str(exc))]) from exc\n    except ValidationError as exc:\n        raise ApplyYamlError(\n            str(exc),\n            errors=_validation_error_to_field_errors(exc, display=display),\n            original_error=exc,\n        ) from exc\n    except httpx.HTTPStatusError as exc:\n        if (\n            mode == \"create\"\n            and exc.response.status_code == 409\n            and display.name is not None\n        ):\n            raise _create_conflict_error(display) from exc\n        raise ApplyYamlError(\n            str(exc),\n            errors=_http_error_to_field_errors(exc, display=display),\n            original_error=exc,\n        ) from exc\n    except Exception as exc:\n        raise ApplyYamlError(str(exc)) from exc\n\n\n@deployments.command(\"apply\")\n@global_options\n@click.option(\n    \"-f\",\n    \"--filename\",\n    required=True,\n    type=click.Path(allow_dash=True, exists=True, dir_okay=False, path_type=str),\n    help=\"Path to YAML file, or '-' for stdin.\",\n)\n@click.option(\n    \"--dry-run\",\n    is_flag=True,\n    default=False,\n    help=\"Validate and print the resolved payload without making API calls.\",\n)\n@click.option(\n    \"--no-push\",\n    is_flag=True,\n    default=False,\n    help=\"Skip pushing local code for push-mode deployments.\",\n)\n@click.option(\n    \"--push\",\n    is_flag=True,\n    default=False,\n    help=\"Push local code even if this repo is not linked to the deployment.\",\n)\n@click.option(\n    \"--annotate-on-error\",\n    is_flag=True,\n    default=False,\n    help=\"Write apply errors back into the YAML input.\",\n)\n@project_option\ndef apply_deployment(\n    filename: str,\n    dry_run: bool,\n    no_push: bool,\n    push: bool,\n    annotate_on_error: bool,\n    project: str | None,\n) -> None:\n    \"\"\"Apply a deployment from a YAML file.\n\n    Creates the deployment if it doesn't exist, or updates it if it does.\n    Reads the file (or stdin with ``-f -``), resolves ``${VAR}`` references\n    from the environment, and issues the appropriate API call.\n    \"\"\"\n    push_policy = _push_policy_from_flags(push=push, no_push=no_push)\n    text = _read_apply_input(filename)\n    try:\n        display = _parse_deployment_yaml_text(text)\n    except ApplyYamlError as exc:\n        if annotate_on_error and not dry_run:\n            _handle_annotated_apply_error(\n                filename=filename,\n                text=text,\n                errors=exc.errors,\n            )\n        _raise_apply_yaml_click_error(exc)\n\n    if dry_run:\n        try:\n            _validate_dry_run_payload(display)\n        except ApplyYamlError as exc:\n            _raise_apply_yaml_click_error(exc)\n\n        verdict = (\n            f\"would upsert deployment '{display.name}'\"\n            if display.name\n            else f\"would create deployment '{display.generate_name}'\"\n        )\n\n        click.echo(\n            yaml.safe_dump(\n                display.spec.as_redacted().model_dump(mode=\"json\", exclude_unset=True),\n                sort_keys=False,\n            )\n        )\n        status(verdict)\n        return\n\n    try:\n        _apply_deployment_display(\n            display,\n            project=project,\n            push_policy=push_policy,\n        )\n    except ApplyYamlError as exc:\n        if annotate_on_error:\n            _handle_annotated_apply_error(\n                filename=filename,\n                text=text,\n                errors=exc.errors,\n            )\n        _raise_apply_yaml_click_error(exc)\n\n\n@deployments.command(\"edit\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@click.option(\n    \"-f\",\n    \"--filename\",\n    default=None,\n    type=click.Path(allow_dash=True, exists=True, dir_okay=False, path_type=str),\n    help=\"Path to YAML file, or '-' for stdin.\",\n)\n@click.option(\n    \"--no-push\",\n    is_flag=True,\n    default=False,\n    help=\"Skip pushing local code for push-mode deployments.\",\n)\n@click.option(\n    \"--push\",\n    is_flag=True,\n    default=False,\n    help=\"Push local code even if this repo is not linked to the deployment.\",\n)\n@project_option\ndef edit_deployment(\n    deployment_id: str | None,\n    filename: str | None,\n    no_push: bool,\n    push: bool,\n    project: str | None,\n) -> None:\n    \"\"\"Edit a deployment in $EDITOR.\"\"\"\n    push_policy = _push_policy_from_flags(push=push, no_push=no_push)\n    if filename is not None:\n        _apply_deployment_yaml_file(\n            filename=filename,\n            project=project,\n            push_policy=push_policy,\n            mode=\"update\",\n            update_target=deployment_id,\n        )\n        return\n\n    deployment_id = _require_deployment_id(deployment_id, \"edit\", project)\n\n    if _requires_file_for_editor():\n        raise click.ClickException(\"pass -f <file> for non-interactive edit\")\n\n    effective_project: str | None = project\n    try:\n        effective_project, current_deployment = asyncio.run(\n            _fetch_deployment_for_editor(project=project, deployment_id=deployment_id)\n        )\n        _edit_deployment_yaml_loop(\n            initial_yaml=_existing_deployment_editor_yaml(current_deployment),\n            project=project,\n            push_policy=push_policy,\n            mode=\"update\",\n            update_target=deployment_id,\n        )\n\n    except click.ClickException:\n        raise\n    except Exception as e:\n        friendly = friendly_http_error(\n            e, deployment_id=deployment_id, project_id=effective_project\n        )\n        message = friendly if friendly is not None else str(e)\n        raise click.ClickException(message) from e\n\n\ndef _push_internal_for_update(\n    deployment_id: str,\n    git_ref: str | None,\n    client: Any,\n    push_policy: PushPolicy,\n) -> None:\n    \"\"\"Push local code to the internal repo before updating.\n\n    This ensures the S3-stored bare repo has the latest commits so the\n    server can resolve the ref to a fresh SHA.\n    \"\"\"\n    if not is_git_repo():\n        warning(\"not in a git repo; skipping push, server will use last pushed code\")\n        return\n\n    if push_policy == \"auto\" and not has_deployment_git_remote(deployment_id):\n        _warn_missing_deployment_remote(deployment_id)\n        return\n\n    git_url = get_deployment_git_url(client.base_url, deployment_id)\n    remote_name = configure_git_remote(\n        git_url, client.api_key, client.project_id, deployment_id\n    )\n    local_ref, target_ref = internal_push_refspec(git_ref)\n    status(\"pushing code\")\n    push_result = push_to_remote(\n        remote_name, local_ref=local_ref, target_ref=target_ref\n    )\n    if push_result.returncode != 0:\n        stderr = \" \".join(\n            push_result.stderr.decode(errors=\"replace\").strip().splitlines()\n        )\n        warning(f\"push failed: {stderr}\")\n        warning(\n            \"continuing with update using last pushed code; \"\n            f\"run llamactl deployments configure-git-remote {deployment_id} to debug\"\n        )\n\n\n@deployments.command(\"update\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@click.option(\n    \"--git-ref\",\n    help=\"Reference branch, tag, or commit SHA for the deployment. If not provided, the current reference and latest commit on it will be used.\",\n    default=None,\n)\n@click.option(\n    \"--no-push\",\n    is_flag=True,\n    default=False,\n    help=\"Skip pushing local code for internal-repo deployments.\",\n)\n@click.option(\n    \"--push\",\n    is_flag=True,\n    default=False,\n    help=\"Push local code even if this repo is not linked to the deployment.\",\n)\n@project_option\ndef refresh_deployment(\n    deployment_id: str | None,\n    git_ref: str | None,\n    no_push: bool,\n    push: bool,\n    project: str | None,\n) -> None:\n    \"\"\"Update the deployment, pulling the latest code from its branch.\"\"\"\n    push_policy = _push_policy_from_flags(push=push, no_push=no_push)\n    deployment_id = _require_deployment_id(deployment_id, \"update\", project)\n    try:\n        # Single asyncio.run with one client: reusing a ProjectClient across\n        # two asyncio.run calls binds the underlying httpx pool to a closed\n        # loop and the next request raises \"Event loop is closed\".\n        async def _do_update() -> tuple[DeploymentResponse, DeploymentResponse]:\n            async with project_client_context(project_id_override=project) as client:\n                current = await client.get_deployment(deployment_id)\n                effective_git_ref = git_ref or current.git_ref\n                if (\n                    current.repo_url == INTERNAL_CODE_REPO_SCHEME\n                    and push_policy != \"never\"\n                ):\n                    _push_internal_for_update(\n                        deployment_id,\n                        effective_git_ref,\n                        client=client,\n                        push_policy=push_policy,\n                    )\n                # Re-resolves the branch to the latest commit SHA on the server.\n                status(f\"refreshing {deployment_id}\")\n                updated = await client.update_deployment(\n                    deployment_id,\n                    DeploymentUpdate(git_ref=effective_git_ref),\n                )\n                return current, updated\n\n        current_deployment, updated_deployment = asyncio.run(_do_update())\n\n        old_git_sha = current_deployment.git_sha or \"\"\n        new_git_sha = updated_deployment.git_sha or \"\"\n        old_short = short_sha(old_git_sha) if old_git_sha else \"-\"\n        new_short = short_sha(new_git_sha) if new_git_sha else \"-\"\n\n        if old_git_sha == new_git_sha:\n            status(f\"no changes {deployment_id} already at {new_short}\")\n        else:\n            status(f\"updated {deployment_id} {old_short} -> {new_short}\")\n\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@deployments.command(\"history\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@output_option\n@project_option\ndef show_history(\n    deployment_id: str | None,\n    output: str,\n    project: str | None,\n) -> None:\n    \"\"\"Show release history for a deployment.\"\"\"\n    deployment_id = _require_deployment_id(deployment_id, \"history\", project)\n    try:\n\n        async def _fetch_history() -> DeploymentHistoryResponse:\n            async with project_client_context(project_id_override=project) as client:\n                return await client.get_deployment_history(deployment_id)\n\n        history = asyncio.run(_fetch_history())\n        items_sorted = sorted(\n            history.history,\n            key=lambda it: it.released_at,\n            reverse=True,\n        )\n\n        if not items_sorted and output == \"text\":\n            status(f\"no history {deployment_id}\")\n            return\n\n        displays = [ReleaseDisplay.from_response(item) for item in items_sorted]\n        render_output(displays, output)\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@deployments.command(\"rollback\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@click.option(\n    \"--git-sha\", required=False, type=GitShaType(), help=\"Git SHA to roll back to\"\n)\n@project_option\ndef rollback(\n    deployment_id: str | None,\n    git_sha: str | None,\n    project: str | None,\n) -> None:\n    \"\"\"Rollback a deployment to a previous git sha.\"\"\"\n    deployment_id = _require_deployment_id(deployment_id, \"rollback\", project)\n    try:\n        if not git_sha:\n            # If not provided, prompt from history\n            async def _fetch_current_and_history() -> tuple[\n                DeploymentResponse, DeploymentHistoryResponse\n            ]:\n                async with project_client_context(\n                    project_id_override=project\n                ) as client:\n                    current = await client.get_deployment(deployment_id)\n                    hist = await client.get_deployment_history(deployment_id)\n                    return current, hist\n\n            current_deployment, history = asyncio.run(_fetch_current_and_history())\n            current_sha = current_deployment.git_sha or \"\"\n\n            items_sorted = sorted(\n                history.history or [], key=lambda it: it.released_at, reverse=True\n            )\n            choices: list[tuple[str, str]] = []\n            current_idx = 0\n            for i, it in enumerate(items_sorted):\n                short = short_sha(it.git_sha)\n                if current_sha and it.git_sha == current_sha:\n                    suffix = \" [current]\"\n                    current_idx = i\n                else:\n                    suffix = \"\"\n                choices.append((it.git_sha, f\"{short}{suffix} ({it.released_at})\"))\n            git_sha = select_or_exit(\n                choices,\n                \"Select git sha:\",\n                hint_flag=\"--git-sha\",\n                hint_command=f\"llamactl deployments history {deployment_id}\",\n                empty_message=\"No history available\",\n                selected=current_idx,\n            )\n\n        async def _do_rollback() -> DeploymentResponse:\n            async with project_client_context(project_id_override=project) as client:\n                return await client.rollback_deployment(deployment_id, git_sha)\n\n        updated = asyncio.run(_do_rollback())\n        new_short = short_sha(updated.git_sha) if updated.git_sha else \"-\"\n        status(f\"rolled back {deployment_id} to {new_short}\")\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@deployments.command(\"logs\")\n@global_options\n@click.argument(\"deployment_id\", required=False, type=DeploymentType())\n@click.option(\n    \"--follow\",\n    \"-f\",\n    is_flag=True,\n    default=False,\n    help=\"Stream logs continuously until interrupted (Ctrl-C).\",\n)\n@click.option(\n    \"--json\",\n    \"json_lines\",\n    is_flag=True,\n    default=False,\n    help=\"Output one LogEvent JSON object per line (jsonl).\",\n)\n@click.option(\n    \"--tail\",\n    \"tail\",\n    type=click.IntRange(min=1),\n    default=200,\n    show_default=True,\n    help=\"Number of lines to retrieve from the end of the logs initially.\",\n)\n@click.option(\n    \"--since-seconds\",\n    \"since_seconds\",\n    type=click.IntRange(min=0),\n    default=None,\n    help=\"Only return logs newer than this many seconds.\",\n)\n@click.option(\n    \"--include-init-containers\",\n    is_flag=True,\n    default=False,\n    help=\"Include init container logs.\",\n)\n@project_option\ndef deployment_logs(\n    deployment_id: str | None,\n    follow: bool,\n    json_lines: bool,\n    tail: int,\n    since_seconds: int | None,\n    include_init_containers: bool,\n    project: str | None,\n) -> None:\n    \"\"\"Stream or fetch logs for a deployment.\n\n    By default, prints recent logs and exits. Use ``--follow`` to keep the\n    stream open until you Ctrl-C. Use ``--json`` to emit one JSON\n    ``LogEvent`` per line for downstream tooling (jsonl).\n    \"\"\"\n    deployment_id = _require_deployment_id(deployment_id, \"logs\", project)\n\n    async def _consume() -> int:\n        events_seen = 0\n        async with project_client_context(project_id_override=project) as client:\n            async for ev in client.stream_deployment_logs(\n                deployment_id,\n                include_init_containers=include_init_containers,\n                tail_lines=tail,\n                since_seconds=since_seconds,\n                follow=follow,\n            ):\n                events_seen += 1\n                _emit_log_event(ev, json_lines=json_lines)\n        return events_seen\n\n    try:\n        events_seen = asyncio.run(_consume())\n    except KeyboardInterrupt:\n        # Clean exit on Ctrl-C; no traceback.\n        return\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n    if events_seen == 0 and not follow:\n        click.echo(\"no logs available yet\", err=True)\n\n\ndef _emit_log_event(ev: LogEvent, *, json_lines: bool) -> None:\n    \"\"\"Render a single LogEvent to stdout per the requested format.\"\"\"\n    if json_lines:\n        click.echo(ev.model_dump_json())\n        return\n\n    parsed = parse_log_body(ev.text)\n    body = render_plain(parsed)\n    pod = f\"{ev.pod}/{ev.container}\"\n    # Skip the envelope timestamp when the structured body already carries one,\n    # otherwise structlog lines render with two side-by-side timestamps.\n    ts = \"\" if parsed.timestamp else (ev.timestamp.isoformat() if ev.timestamp else \"\")\n    prefix = \" \".join(p for p in (ts, pod) if p)\n    click.echo(f\"{prefix} {body}\" if prefix else body)\n\n\ndef _require_deployment_id(\n    deployment_id: str | None,\n    subcommand: str,\n    project_id_override: str | None = None,\n) -> str:\n    \"\"\"Return deployment_id if provided, otherwise list deployments and error.\"\"\"\n    if deployment_id:\n        return deployment_id\n\n    client = get_project_client(project_id_override=project_id_override)\n    all_deployments = asyncio.run(client.list_deployments())\n    require_or_list_choices(\n        [(d.id, f\"{d.id} - {d.status}\") for d in all_deployments],\n        hint_command=f\"llamactl deployments {subcommand} <deployment_id>\",\n        empty_message=f\"No deployments found for project {client.project_id}\",\n    )\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/dev.py",
    "content": "from __future__ import annotations\n\nimport os\nimport subprocess\nfrom pathlib import Path\n\nimport click\nfrom click.exceptions import Abort, Exit\nfrom llama_agents.cli.commands.aliased_group import AliasedGroup\nfrom llama_agents.cli.commands.serve import (\n    _maybe_inject_llama_cloud_credentials,\n    _print_connection_summary,\n)\nfrom llama_agents.cli.commands.serve import (\n    serve as serve_command,\n)\nfrom llama_agents.cli.options import global_options\nfrom llama_agents.cli.output import status\nfrom llama_agents.core.config import DEFAULT_DEPLOYMENT_FILE_PATH\nfrom llama_agents.core.deployment_config import DeploymentConfig\n\nfrom ..app import app\n\n_ClickPath = getattr(click, \"Path\")\n\n\n@app.group(\n    name=\"dev\",\n    help=\"Development utilities for llama-deploy projects.\",\n    cls=AliasedGroup,\n    no_args_is_help=True,\n)\n@global_options\ndef dev() -> None:\n    \"\"\"Collection of development commands.\"\"\"\n\n\ndev.add_command(serve_command, name=\"serve\")\n\n\n@dev.command(\n    \"validate\",\n    help=\"Load configured workflows and run their validation hooks\",\n)\n@click.argument(\n    \"deployment_file\",\n    required=False,\n    default=DEFAULT_DEPLOYMENT_FILE_PATH,\n    type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),\n)\n@click.option(\n    \"--validate-env\",\n    is_flag=True,\n    help=(\n        \"Validate that required environment variables are set. By default, missing \"\n        \"env vars are filled with placeholder values to allow structural validation \"\n        \"without actual environment variable values.\"\n    ),\n)\n@global_options\ndef validate_command(deployment_file: Path, validate_env: bool) -> None:\n    \"\"\"Validate workflows defined in the deployment configuration.\"\"\"\n    config_dir = _ensure_project_layout(\n        deployment_file, command_name=\"llamactl dev validate\"\n    )\n    # Ensure cloud credentials/env are available to the subprocess (if required)\n    _maybe_inject_llama_cloud_credentials(deployment_file, require_cloud=False)\n\n    # By default, skip env validation (fill missing with placeholders)\n    skip_env_validation = not validate_env\n\n    prepare_server(\n        deployment_file=deployment_file,\n        install=True,\n        build=False,\n        install_ui_deps=False,\n        skip_env_validation=skip_env_validation,\n    )\n\n    # Delegate venv-targeted invocation to the appserver helper (mirrors start_server_in_target_venv)\n\n    try:\n        start_preflight_in_target_venv(\n            cwd=Path.cwd(),\n            deployment_file=deployment_file,\n            skip_env_validation=skip_env_validation,\n        )\n    except subprocess.CalledProcessError as exc:\n        status(\"workflow validation failed; see errors above\")\n        raise Exit(exc.returncode)\n\n    _print_connection_summary()\n    status(f\"validated workflows in {config_dir}\")\n\n\n@dev.command(\n    \"export-json-graph\",\n    help=\"Produce a JSON graph representation of registered workflows\",\n    hidden=True,  # perhaps expose if we have a built in visualization (mermaid, etc.)\n)\n@click.argument(\n    \"deployment_file\",\n    required=False,\n    default=DEFAULT_DEPLOYMENT_FILE_PATH,\n    type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),\n)\n@click.option(\n    \"--output\",\n    help=(\n        \"File where output JSON graph will be saved. \"\n        \"Defaults to workflows.json in the current directory.\"\n    ),\n    required=False,\n    default=None,\n    type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),\n)\n@global_options\ndef export_json_graph_command(\n    deployment_file: Path,\n    output: Path | None,\n) -> None:\n    \"\"\"Export the configured workflows to a JSON document that may be used for graph visualization.\"\"\"\n    if not deployment_file.exists():\n        raise click.ClickException(\n            f\"Deployment file '{deployment_file}' does not exist\"\n        )\n\n    _ensure_project_layout(\n        deployment_file, command_name=\"llamactl dev export-json-graph\"\n    )\n    _maybe_inject_llama_cloud_credentials(deployment_file, require_cloud=False)\n\n    prepare_server(\n        deployment_file=deployment_file,\n        install=True,\n        build=False,\n        install_ui_deps=False,\n    )\n\n    wd = Path.cwd()\n    if output is None:\n        output = wd / \"workflows.json\"\n\n    try:\n        start_export_json_graph_in_target_venv(\n            cwd=wd,\n            deployment_file=deployment_file,\n            output=output,\n        )\n    except subprocess.CalledProcessError as exc:\n        status(\"workflow JSON graph export failed; see errors above\")\n        raise Exit(exc.returncode)\n    status(f\"exported workflow JSON graph to {output}\")\n\n\n@dev.command(\n    \"run\",\n    help=(\n        \"Load env configuration and execute a command. Use '--' before the command \"\n        \"to avoid option parsing.\"\n    ),\n    context_settings={\"ignore_unknown_options\": True, \"allow_extra_args\": True},\n)\n@global_options\n@click.option(\n    \"deployment_file\",\n    \"--deployment-file\",\n    default=DEFAULT_DEPLOYMENT_FILE_PATH,\n    type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),\n    help=\"The deployment file to use for the command\",\n)\n@click.option(\n    \"no_auth\",\n    \"--no-auth\",\n    is_flag=True,\n    help=\"Do not inject/authenticate with Llama Cloud credentials\",\n)\n@click.argument(\"cmd\", nargs=-1, type=click.UNPROCESSED)\ndef run_command(deployment_file: Path, no_auth: bool, cmd: tuple[str, ...]) -> None:\n    \"\"\"Execute COMMAND with deployment environment variables applied.\"\"\"\n    if not cmd:\n        raise click.ClickException(\n            \"No command provided. Use '--' before the command arguments if needed.\"\n        )\n\n    try:\n        config, config_parent = _prepare_environment(\n            deployment_file, require_cloud=not no_auth\n        )\n        env_overrides = parse_environment_variables(config, config_parent)\n        env = os.environ.copy()\n        env.update({k: v for k, v in env_overrides.items() if v is not None})\n\n        _print_connection_summary()\n        result = subprocess.run(cmd, env=env, check=False)\n        if result.returncode != 0:\n            raise SystemExit(result.returncode)\n    except (Exit, Abort, SystemExit, click.ClickException):\n        raise\n    except FileNotFoundError as exc:\n        raise click.ClickException(f\"Command not found: {exc.filename}\") from exc\n    except Exception as exc:  # pragma: no cover - unexpected errors reported to user\n        raise click.ClickException(str(exc)) from exc\n\n\ndef _ensure_project_layout(deployment_file: Path, *, command_name: str) -> Path:\n    if not deployment_file.exists():\n        raise click.ClickException(f\"Deployment file '{deployment_file}' not found\")\n\n    config_dir = deployment_file if deployment_file.is_dir() else deployment_file.parent\n    if not (config_dir / \"pyproject.toml\").exists():\n        raise click.ClickException(\n            f\"No pyproject.toml found at {config_dir}.\\n\"\n            f\"Add a pyproject.toml to your project and re-run '{command_name}'.\"\n        )\n    return config_dir\n\n\ndef _prepare_environment(\n    deployment_file: Path, *, require_cloud: bool\n) -> tuple[DeploymentConfig, Path]:\n    from llama_agents.appserver.deployment_config_parser import (\n        get_deployment_config,\n    )\n    from llama_agents.appserver.settings import configure_settings, settings\n    from llama_agents.appserver.workflow_loader import (\n        load_environment_variables,\n        validate_required_env_vars,\n    )\n\n    _maybe_inject_llama_cloud_credentials(deployment_file, require_cloud=require_cloud)\n    configure_settings(\n        deployment_file_path=deployment_file,\n        app_root=Path.cwd(),\n    )\n    config = get_deployment_config()\n    config_parent = settings.resolved_config_parent\n    load_environment_variables(config, config_parent)\n    validate_required_env_vars(config)\n    return config, config_parent\n\n\ndef prepare_server(\n    *,\n    deployment_file: Path,\n    install: bool,\n    build: bool,\n    install_ui_deps: bool,\n    skip_env_validation: bool = False,\n) -> None:\n    \"\"\"Thin wrapper so tests can monkeypatch `dev.prepare_server` without importing appserver at import time.\"\"\"\n    from llama_agents.appserver.app import prepare_server as _prepare_server\n\n    _prepare_server(\n        deployment_file=deployment_file,\n        install=install,\n        build=build,\n        install_ui_deps=install_ui_deps,\n        skip_env_validation=skip_env_validation,\n    )\n\n\ndef start_preflight_in_target_venv(\n    *, cwd: Path, deployment_file: Path, skip_env_validation: bool = False\n) -> None:\n    \"\"\"Thin wrapper so tests can monkeypatch `dev.start_preflight_in_target_venv`.\"\"\"\n    from llama_agents.appserver.app import (\n        start_preflight_in_target_venv as _start_preflight_in_target_venv,\n    )\n\n    _start_preflight_in_target_venv(\n        cwd=cwd,\n        deployment_file=deployment_file,\n        skip_env_validation=skip_env_validation,\n    )\n\n\ndef start_export_json_graph_in_target_venv(\n    *, cwd: Path, deployment_file: Path, output: Path\n) -> None:\n    \"\"\"Thin wrapper so tests can monkeypatch `dev.start_export_json_graph_in_target_venv`.\"\"\"\n    from llama_agents.appserver.app import (\n        start_export_json_graph_in_target_venv as _start_export_json_graph_in_target_venv,\n    )\n\n    _start_export_json_graph_in_target_venv(\n        cwd=cwd,\n        deployment_file=deployment_file,\n        output=output,\n    )\n\n\ndef parse_environment_variables(\n    config: DeploymentConfig, config_parent: Path\n) -> dict[str, str]:\n    \"\"\"Wrapper used by tests; imports workflow loader lazily.\"\"\"\n    from llama_agents.appserver.workflow_loader import (\n        parse_environment_variables as _parse_environment_variables,\n    )\n\n    return _parse_environment_variables(config, config_parent)\n\n\n__all__ = [\"dev\", \"validate_command\", \"run_command\", \"export_json_graph_command\"]\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/environments.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom importlib import metadata as importlib_metadata\nfrom typing import TYPE_CHECKING\n\nimport click\nfrom llama_agents.cli.config.schema import Environment\nfrom llama_agents.cli.interactive import is_interactive_session, select_or_exit\nfrom llama_agents.cli.output import status, warning\nfrom llama_agents.cli.param_types import EnvironmentType\nfrom packaging import version as packaging_version\n\nfrom ..app import app\nfrom ..display import EnvDisplay\nfrom ..options import global_options, render_output, simple_output_option\n\nif TYPE_CHECKING:\n    from llama_agents.cli.config.env_service import EnvService\n\n\ndef _env_service() -> EnvService:\n    \"\"\"Return the shared EnvService instance via a local import.\n\n    This keeps CLI startup light while remaining easy to patch in tests via\n    ``llama_agents.cli.config.env_service.service``.\n    \"\"\"\n    from ..config.env_service import service\n\n    return service\n\n\n@app.group(\n    name=\"environments\",\n    help=\"Manage control plane API URLs.\",\n    no_args_is_help=True,\n)\n@global_options\ndef environments() -> None:\n    pass\n\n\n@environments.command(\"get\")\n@click.argument(\"api_url\", required=False, type=EnvironmentType())\n@global_options\n@simple_output_option\ndef get_environments_cmd(api_url: str | None, output: str) -> None:\n    \"\"\"List environments or show one environment.\"\"\"\n    try:\n        service = _env_service()\n        envs = service.list_environments()\n        current_env = service.get_current_environment()\n        if api_url:\n            normalized = api_url.rstrip(\"/\")\n            envs = [env for env in envs if env.api_url == normalized]\n            if not envs:\n                raise click.ClickException(f\"Environment '{normalized}' not found\")\n\n        if not envs and output == \"text\":\n            status(\"no environments found\")\n            return\n\n        current_url = current_env.api_url if current_env else None\n        displays = [\n            EnvDisplay.from_environment(env, current_url=current_url) for env in envs\n        ]\n        render_output(\n            displays[0] if api_url and len(displays) == 1 else displays, output\n        )\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@environments.command(\"add\")\n@click.argument(\"api_url\", required=False)\n@global_options\ndef add_environment_cmd(api_url: str | None) -> None:\n    \"\"\"Probe and store an environment.\"\"\"\n    try:\n        service = _env_service()\n        if not api_url:\n            if not is_interactive_session():\n                raise click.ClickException(\n                    \"Pass <api_url> as an argument. To see existing environments, run: llamactl environments get\"\n                )\n            current_env = service.get_current_environment()\n            entered = click.prompt(\n                \"Enter control plane API URL\",\n                default=current_env.api_url if current_env else \"\",\n                show_default=current_env is not None,\n            )\n            if not entered:\n                status(\"no environment entered\")\n                return\n            api_url = entered.strip()\n\n        if api_url is None:\n            raise click.ClickException(\"API URL is required\")\n        api_url = api_url.rstrip(\"/\")\n        env = service.probe_environment(api_url)\n        service.create_or_update_environment(env)\n        requires_auth = str(env.requires_auth).lower()\n        status(\n            f\"added environment {env.api_url} requires_auth={requires_auth} min_llamactl_version={env.min_llamactl_version or '-'}\"\n        )\n        _maybe_warn_min_version(env.min_llamactl_version)\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@environments.command(\"delete\")\n@click.argument(\"api_url\", required=False, type=EnvironmentType())\n@global_options\ndef delete_environment_cmd(api_url: str | None) -> None:\n    \"\"\"Delete an environment and its profiles.\"\"\"\n    try:\n        service = _env_service()\n        if not api_url:\n            result = _select_environment(\n                service.list_environments(),\n                service.get_current_environment(),\n                \"Select environment to delete\",\n            )\n            api_url = result.api_url\n\n        if api_url is None:\n            raise click.ClickException(\"API URL is required\")\n        api_url = api_url.rstrip(\"/\")\n        deleted = service.delete_environment(api_url)\n        if not deleted:\n            raise click.ClickException(f\"Environment '{api_url}' not found\")\n        status(f\"deleted environment {api_url} and associated profiles\")\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@environments.command(\"use\")\n@click.argument(\"api_url\", required=False, type=EnvironmentType())\n@global_options\ndef use_environment_cmd(api_url: str | None) -> None:\n    \"\"\"Set the active environment.\"\"\"\n    try:\n        service = _env_service()\n        selected_url = api_url\n\n        if not selected_url:\n            result = _select_environment(\n                service.list_environments(),\n                service.get_current_environment(),\n                \"Select environment\",\n            )\n            selected_url = result.api_url\n\n        selected_url = selected_url.rstrip(\"/\")\n\n        # Ensure environment exists and switch\n        env = service.switch_environment(selected_url)\n        try:\n            env = service.auto_update_env(env)\n        except Exception as e:\n            warning(f\"failed to resolve environment: {e}\")\n            return\n        service.current_auth_service().select_any_profile()\n        status(f\"switched environment {env.api_url}\")\n        _maybe_warn_min_version(env.min_llamactl_version)\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\ndef _get_cli_version() -> str | None:\n    try:\n        return importlib_metadata.version(\"llamactl\")\n    except Exception:\n        return None\n\n\ndef _maybe_warn_min_version(min_required: str | None) -> None:\n    if not min_required:\n        return\n    current = _get_cli_version()\n    if not current:\n        return\n    try:\n        if packaging_version.parse(current) < packaging_version.parse(min_required):\n            warning(\n                f\"this environment requires llamactl >= {min_required}; you have {current}\"\n            )\n    except Exception:\n        # If packaging is not available or parsing fails, skip strict comparison\n        pass\n\n\ndef _select_environment(\n    envs: list[Environment],\n    current_env: Environment | None,\n    message: str = \"Select environment\",\n) -> Environment:\n    if not envs:\n        raise click.ClickException(\n            \"No environments found. This is a bug and shouldn't happen.\"\n        )\n    items = []\n    current_idx = 0\n    for i, env in enumerate(envs):\n        label = env.api_url\n        if current_env is not None and env.api_url == current_env.api_url:\n            label += \" [current]\"\n            current_idx = i\n        items.append((env, label))\n    return select_or_exit(\n        items,\n        message,\n        hint_flag=\"<api_url>\",\n        hint_command=\"llamactl environments get\",\n        selected=current_idx,\n    )\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/init.py",
    "content": "from __future__ import annotations\n\nimport os\nimport shutil\nimport subprocess\nfrom pathlib import Path\n\nimport click\nfrom click.exceptions import Exit\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.interactive import is_interactive_session, select_or_exit\nfrom llama_agents.cli.options import global_options\nfrom llama_agents.cli.output import status, warning\nfrom llama_agents.cli.param_types import TemplateType\nfrom llama_agents.cli.templates import (\n    ALL_TEMPLATES,\n    HEADLESS_TEMPLATES,\n    UI_TEMPLATES,\n    TemplateOption,\n)\n\n_ClickPath = getattr(click, \"Path\")\n\n\n@app.command()\n@click.option(\n    \"--update\",\n    is_flag=True,\n    help=\"Instead of creating a new app, update the current app to the latest version. Other options will be ignored.\",\n)\n@click.option(\n    \"--template\",\n    type=TemplateType(),\n    help=\"The template to use for the new app\",\n)\n@click.option(\n    \"--dir\",\n    help=\"The directory to create the new app in\",\n    type=_ClickPath(\n        file_okay=False,\n        dir_okay=True,\n        writable=True,\n        resolve_path=True,\n        path_type=Path,\n    ),\n)\n@click.option(\n    \"--force\",\n    is_flag=True,\n    help=\"Force overwrite the directory if it exists\",\n)\n@global_options\ndef init(\n    update: bool,\n    template: str | None,\n    dir: Path | None,\n    force: bool,\n) -> None:\n    \"\"\"Create a new app repository from a template.\"\"\"\n    if update:\n        _update()\n    else:\n        _create(template, dir, force)\n\n\ndef _create(template: str | None, dir: Path | None, force: bool) -> None:\n    interactive = is_interactive_session()\n    # Initialize git repository if git is available\n    has_git = False\n    git_initialized = False\n    try:\n        subprocess.run([\"git\", \"--version\"], check=True, capture_output=True)\n        has_git = True\n    except (subprocess.CalledProcessError, FileNotFoundError):\n        # git is not available or broken; continue without git\n        has_git = False\n\n    if not has_git:\n        status(\n            \"git is required to initialize a template. Make sure you have it installed and available in your PATH.\"\n        )\n        raise Exit(1)\n\n    if template is None:\n        if interactive:\n            status(\n                \"select a template to start from. either with javascript frontend UI, or just a python workflow that can be used as an API.\"\n            )\n        template = (\n            select_or_exit(\n                [\n                    *[(o.id, f\"{o.id} - {o.description}\") for o in UI_TEMPLATES],\n                    *[(o.id, f\"{o.id} - {o.description}\") for o in HEADLESS_TEMPLATES],\n                ],\n                \"\",\n                hint_flag=\"--template\",\n                hint_command=\"llamactl init --help\",\n            )\n            or None\n        )\n    if template is None:\n        raise Exit(1)\n    if dir is None:\n        if interactive:\n            dir_str = click.prompt(\n                \"Enter the directory to create the new app in\",\n                default=template,\n            )\n            if dir_str:\n                dir = Path(dir_str)\n            else:\n                return\n        else:\n            status(f\"no directory provided; defaulting to {template}\")\n            dir = Path(template)\n\n    resolved_template: TemplateOption | None = next(\n        (o for o in ALL_TEMPLATES if o.id == template), None\n    )\n    if resolved_template is None:\n        status(f\"template {template} not found\")\n        raise Exit(1)\n    if dir.exists():\n        is_ok = force or (\n            interactive and click.confirm(\"Directory exists. Overwrite?\", default=False)\n        )\n\n        if not is_ok:\n            status(\n                f\"try again with another directory or pass --force to overwrite the existing directory {str(dir)}\"\n            )\n            raise Exit(1)\n        else:\n            shutil.rmtree(dir, ignore_errors=True)\n\n    # Import copier lazily at call time to keep CLI startup light while still\n    # allowing tests to patch ``copier.run_copy`` directly.\n    import copier\n\n    copier.run_copy(\n        resolved_template.source.url,\n        dir,\n        quiet=True,\n        defaults=not interactive,\n    )\n\n    # Change to the new directory and initialize git repo\n    original_cwd = Path.cwd()\n    os.chdir(dir)\n\n    try:\n        # Copy agent instructions and MCP configs from scaffold\n        _copy_scaffold()\n        # Create symlinks so all agents find the instructions\n        for alternate in [\"CLAUDE.md\", \"GEMINI.md\"]:\n            alt_path = Path(alternate)\n            agents_path = Path(\"AGENTS.md\")\n            if agents_path.exists() and not alt_path.exists():\n                alt_path.symlink_to(\"AGENTS.md\")\n\n        # Initialize a git repo unless we're already inside one.\n        if has_git:\n            # Detect whether the target directory is already inside a git work tree\n            inside_existing_repo = False\n            try:\n                result = subprocess.run(\n                    [\"git\", \"rev-parse\", \"--is-inside-work-tree\"],\n                    check=True,\n                    capture_output=True,\n                    text=True,\n                )\n                inside_existing_repo = result.stdout.strip().lower() == \"true\"\n            except (subprocess.CalledProcessError, FileNotFoundError):\n                inside_existing_repo = False\n\n            if inside_existing_repo:\n                # Do not create a nested repo; user likely wants this within the parent repo\n                warning(\n                    \"skipping git initialization: existing Git repository in a parent directory\"\n                )\n                # Treat as initialized for purposes of what instructions to show later\n                git_initialized = True\n            else:\n                try:\n                    subprocess.run([\"git\", \"init\"], check=True, capture_output=True)\n                    subprocess.run([\"git\", \"add\", \".\"], check=True, capture_output=True)\n                    subprocess.run(\n                        [\"git\", \"commit\", \"-m\", \"Initial commit\"],\n                        check=True,\n                        capture_output=True,\n                    )\n                    git_initialized = True\n                except (subprocess.CalledProcessError, FileNotFoundError) as e:\n                    # Extract a short error message if present\n                    err_msg = \"\"\n                    if isinstance(e, subprocess.CalledProcessError):\n                        stderr_bytes = e.stderr or b\"\"\n                        if isinstance(stderr_bytes, (bytes, bytearray)):\n                            try:\n                                stderr_text = stderr_bytes.decode(\"utf-8\", \"ignore\")\n                            except Exception:\n                                stderr_text = \"\"\n                        else:\n                            stderr_text = str(stderr_bytes)\n                        if stderr_text.strip():\n                            err_msg = stderr_text.strip().split(\"\\n\")[-1]\n                    elif isinstance(e, FileNotFoundError):\n                        err_msg = \"git executable not found\"\n\n                    status(\"\")\n                    warning(f\"skipping git initialization: {err_msg or 'git error'}\")\n                    if err_msg:\n                        status(f\"    {err_msg}\")\n                    status(\"    You can initialize it manually:\")\n                    status(\n                        \"      git init && git add . && git commit -m 'Initial commit'\"\n                    )\n                    status(\"\")\n    finally:\n        os.chdir(original_cwd)\n\n    # If git is not available at all, let the user know how to proceed\n    if not has_git:\n        status(\"\")\n        warning(\"skipping git initialization: git executable not found\")\n        status(\"    You can initialize it manually:\")\n        status(\"      git init && git add . && git commit -m 'Initial commit'\")\n        status(\"\")\n\n    status(f\"created {dir} using the {resolved_template.name} template\")\n    status(\"\")\n    status(\"to run locally:\")\n    status(f\"    cd {dir}\")\n    status(\"    uvx llamactl serve\")\n    status(\"\")\n    status(\"to deploy:\")\n    # Only show manual git init steps if repository failed to initialize earlier\n    if not git_initialized:\n        status(\"    git init\")\n        status(\"    git add .\")\n        status(\"    git commit -m 'Initial commit'\")\n        status(\"\")\n    status(\"(Create a new repo and add it as a remote)\")\n    status(\"\")\n    status(\"    git remote add origin <your-repo-url>\")\n    status(\"    git push -u origin main\")\n    status(\"\")\n    status(\"    uvx llamactl deployments create\")\n    status(\"\")\n\n\ndef _update() -> None:\n    \"\"\"Update the app to the latest version\"\"\"\n    try:\n        # Import copier lazily so the init command remains lightweight when\n        # unused, while tests can patch ``copier.run_update`` directly.\n        import copier\n\n        copier.run_update(\n            overwrite=True,\n            skip_answered=True,\n            quiet=True,\n        )\n    except Exception as e:  # scoped to copier errors; type opaque here\n        status(f\"{e}\")\n        raise Exit(1)\n\n    # Check git status and warn about conflicts\n    try:\n        result = subprocess.run(\n            [\"git\", \"status\", \"--porcelain\"],\n            check=True,\n            capture_output=True,\n            text=True,\n        )\n\n        if result.stdout.strip():\n            conflicted_files = []\n            modified_files = []\n\n            for line in result.stdout.strip().split(\"\\n\"):\n                git_status = line[:2]\n                filename = line[3:]\n\n                if \"UU\" in git_status or \"AA\" in git_status or \"DD\" in git_status:\n                    conflicted_files.append(filename)\n                elif git_status.strip():\n                    modified_files.append(filename)\n\n            if conflicted_files:\n                status(\"\")\n                warning(\"files with conflicts detected:\")\n                for file in conflicted_files:\n                    status(f\"    {file}\")\n                status(\"\")\n                status(\n                    \"Please manually resolve conflicts with a merge editor before proceeding.\"\n                )\n\n    except (subprocess.CalledProcessError, FileNotFoundError):\n        # Git not available or not in a git repo - continue silently\n        pass\n\n\n_SCAFFOLD_DIR = Path(__file__).resolve().parent.parent / \"scaffold\"\n\n\ndef _copy_scaffold() -> None:\n    \"\"\"Copy scaffold files (AGENTS.md, MCP configs) into the current directory.\"\"\"\n    for item in _SCAFFOLD_DIR.iterdir():\n        dest = Path(item.name)\n        if item.is_dir():\n            shutil.copytree(item, dest, dirs_exist_ok=True)\n        else:\n            shutil.copy2(item, dest)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/organizations.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport click\nfrom llama_agents.cli.output import status, warning\nfrom llama_agents.cli.utils.capabilities import probe_organizations_support\n\nfrom ..app import app\nfrom ..display import OrgDisplay\nfrom ..options import global_options, render_output, simple_output_option\nfrom .auth import _get_service, _list_organizations\n\n\n@app.group(\n    help=\"Inspect organizations.\",\n    no_args_is_help=True,\n)\n@global_options\ndef organizations() -> None:\n    pass\n\n\n@organizations.command(\"get\")\n@global_options\n@simple_output_option\ndef get_organizations(output: str) -> None:\n    \"\"\"List organizations available to the current profile.\"\"\"\n    try:\n        auth_svc = _get_service().current_auth_service()\n        if not probe_organizations_support():\n            if output == \"text\":\n                warning(\"this server does not support organizations\")\n                return\n            render_output([], output)\n            return\n\n        orgs = _list_organizations(auth_svc)\n        if not orgs and output == \"text\":\n            status(\"no organizations found\")\n            return\n\n        default_org = next((o.org_id for o in orgs if o.is_default), None)\n        displays = [\n            OrgDisplay.from_org_summary(org, current_org_id=default_org) for org in orgs\n        ]\n        render_output(displays, output)\n\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/pkg.py",
    "content": "from pathlib import Path\n\nimport click\nfrom llama_agents.cli.pkg import (\n    DEFAULT_DOCKER_IGNORE,\n    build_dockerfile_content,\n    infer_python_version,\n    pkg_container_options,\n)\nfrom llama_agents.core.deployment_config import (\n    read_deployment_config_from_git_root_or_cwd,\n)\n\nfrom ..app import app\n\nSUPPORTED_FORMATS = [\"Docker\", \"Podman\"]\nSUPPORTED_FORMATS_STR = \", \".join(SUPPORTED_FORMATS)\n\n\n@app.group(\n    help=f\"Package your application in different formats. Currently supported: {SUPPORTED_FORMATS_STR}.\",\n    no_args_is_help=True,\n    context_settings={\"max_content_width\": None},\n)\ndef pkg() -> None:\n    \"\"\"Package application in different formats (Dockerfile, Podman config, Nixpack...)\"\"\"\n    pass\n\n\n@pkg.command(\n    \"container\",\n    help=\"Generate a minimal, build-ready file to containerize your workflows through Docker or Podman (currently frontend is not supported).\",\n)\n@pkg_container_options\ndef create_container_file(\n    deployment_file: Path,\n    python_version: str | None = None,\n    port: int = 4501,\n    exclude: tuple[str, ...] | None = None,\n    output_file: str = \"Dockerfile\",\n    dockerignore_path: str = \".dockerignore\",\n    overwrite: bool = False,\n) -> None:\n    _create_file_for_container(\n        deployment_file=deployment_file,\n        python_version=python_version,\n        port=port,\n        exclude=exclude,\n        output_file=output_file,\n        dockerignore_path=dockerignore_path,\n        overwrite=overwrite,\n    )\n\n\ndef _check_deployment_config(deployment_file: Path) -> Path:\n    if not deployment_file.exists():\n        raise click.ClickException(f\"Deployment file '{deployment_file}' not found\")\n\n    # Early check: appserver requires a pyproject.toml in the config directory\n    config_dir = deployment_file if deployment_file.is_dir() else deployment_file.parent\n    if not (config_dir / \"pyproject.toml\").exists():\n        raise click.ClickException(\n            f\"No pyproject.toml found at {config_dir}.\\n\"\n            \"Add a pyproject.toml to your project and re-run 'llamactl serve'.\"\n        )\n\n    try:\n        config = read_deployment_config_from_git_root_or_cwd(\n            Path.cwd(), deployment_file\n        )\n    except Exception:\n        raise click.ClickException(\n            \"Could not read a deployment config. This doesn't appear to be a valid llama-deploy project.\"\n        )\n    if config.ui:\n        raise click.ClickException(\n            \"Containerized UI builds are currently not supported. Please remove the UI configuration from your deployment file if you wish to proceed.\"\n        )\n    return config_dir\n\n\ndef _create_file_for_container(\n    deployment_file: Path,\n    output_file: str = \"Dockerfile\",\n    python_version: str | None = None,\n    port: int = 4501,\n    exclude: tuple[str, ...] | None = None,\n    dockerignore_path: str = \".dockerignore\",\n    overwrite: bool = False,\n) -> None:\n    config_dir = _check_deployment_config(deployment_file=deployment_file)\n\n    if not python_version:\n        python_version = infer_python_version(config_dir)\n\n    dockerignore_content = DEFAULT_DOCKER_IGNORE\n    if exclude:\n        for item in exclude:\n            dockerignore_content += \"\\n\" + item\n\n    dockerfile_content = build_dockerfile_content(python_version, port)\n\n    if Path(output_file).exists() and not overwrite:\n        raise click.ClickException(\n            f\"{output_file} already exists. If you wish to overwrite the file, pass `--overwrite` as a flag to the command.\"\n        )\n    with open(output_file, \"w\") as f:\n        f.write(dockerfile_content)\n    if Path(dockerignore_path).exists() and not overwrite:\n        raise click.ClickException(\n            f\"{dockerignore_path} already exists. If you wish to overwrite the file, pass `--overwrite` as a flag to the command.\"\n        )\n    with open(dockerignore_path, \"w\") as f:\n        f.write(dockerignore_content)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/projects.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport click\nfrom llama_agents.cli.interactive import is_interactive_session, select_or_exit\nfrom llama_agents.cli.output import status\nfrom llama_agents.cli.param_types import OrgType, ProjectType\n\nfrom ..app import app\nfrom ..display import ProjectDisplay\nfrom ..options import global_options, render_output, simple_output_option\nfrom .auth import (\n    _discover_organization,\n    _get_service,\n    _list_projects,\n    validate_authenticated_profile,\n)\n\n\n@app.group(\n    help=\"Inspect and select projects.\",\n    no_args_is_help=True,\n)\n@global_options\ndef projects() -> None:\n    pass\n\n\n@projects.command(\"get\")\n@click.argument(\"project_id\", required=False, type=ProjectType())\n@click.option(\n    \"--org\",\n    \"org_id\",\n    default=None,\n    type=OrgType(),\n    help=\"Organization ID to scope projects to\",\n)\n@global_options\n@simple_output_option\ndef get_projects(project_id: str | None, org_id: str | None, output: str) -> None:\n    \"\"\"List projects available to the current profile.\"\"\"\n    try:\n        auth_svc = _get_service().current_auth_service()\n        profile = validate_authenticated_profile()\n\n        if project_id:\n            # Look up a specific project across all accessible orgs,\n            # unless the user explicitly scoped with --org.\n            projects = _list_projects(auth_svc, org_id=org_id)\n            projects = [\n                project for project in projects if project.project_id == project_id\n            ]\n            if not projects:\n                raise click.ClickException(f\"Project {project_id} not found\")\n        else:\n            # Scope the listing to the default org when --org is not given.\n            if org_id is None:\n                org = _discover_organization(auth_svc)\n                if org is not None:\n                    org_id = org.org_id\n            projects = _list_projects(auth_svc, org_id=org_id)\n\n        if not projects and output == \"text\":\n            status(\"no projects found\")\n            return\n\n        displays = [\n            ProjectDisplay.from_project_summary(\n                project, current_project_id=profile.project_id\n            )\n            for project in projects\n        ]\n        render_output(\n            displays[0] if project_id and len(displays) == 1 else displays, output\n        )\n\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\n@projects.command(\"use\")\n@click.argument(\"project_id\", required=False, type=ProjectType())\n@click.option(\n    \"--org\",\n    \"org_id\",\n    default=None,\n    type=OrgType(),\n    help=\"Organization ID to scope projects to\",\n)\n@global_options\ndef use_project(project_id: str | None, org_id: str | None) -> None:\n    \"\"\"Set the active project for the current profile.\"\"\"\n    auth_svc = _get_service().current_auth_service()\n    profile = validate_authenticated_profile()\n\n    try:\n        if project_id and profile.project_id == project_id:\n            return\n\n        if project_id:\n            if auth_svc.env.requires_auth:\n                # Validate across all accessible orgs (matching tab completion),\n                # unless the user explicitly scoped with --org.\n                projects = _list_projects(auth_svc, org_id=org_id)\n                if not next(\n                    (\n                        project\n                        for project in projects\n                        if project.project_id == project_id\n                    ),\n                    None,\n                ):\n                    raise click.ClickException(f\"Project {project_id} not found\")\n            auth_svc.set_project(profile.name, project_id)\n            status(f\"switched project {project_id}\")\n            return\n\n        # Scope the interactive listing to the default org when --org is not given.\n        if org_id is None:\n            org = _discover_organization(auth_svc)\n            if org is not None:\n                org_id = org.org_id\n        projects = _list_projects(auth_svc, org_id=org_id)\n\n        if not projects:\n            status(\"no projects found\")\n            return\n\n        current_project_id = profile.project_id\n        items = []\n        current_idx = 0\n        for i, project in enumerate(projects):\n            label = f\"{project.project_name}  {project.project_id} ({project.deployment_count} deployments)\"\n            if project.project_id == current_project_id:\n                label += \" [current]\"\n                current_idx = i\n            items.append((project.project_id, label))\n        if not auth_svc.env.requires_auth:\n            items.append((\"__CREATE__\", \"Create new project\"))\n\n        result = select_or_exit(\n            items,\n            \"Select a project\",\n            hint_flag=\"<project_id>\",\n            hint_command=\"llamactl projects get\",\n            selected=current_idx,\n        )\n        if result == \"__CREATE__\":\n            if not is_interactive_session():\n                raise click.ClickException(\"Pass <project_id> to choose one\")\n            result = click.prompt(\n                \"Enter project ID\", default=\"\", show_default=False\n            ).strip()\n        if result:\n            selected_project = next(\n                (project for project in projects if project.project_id == result), None\n            )\n            name = selected_project.project_name if selected_project else result\n            auth_svc.set_project(profile.name, result)\n            status(f\"switched project {name}\")\n        else:\n            status(\"no project selected\")\n    except click.ClickException:\n        raise\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/commands/serve.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport logging\nimport os\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Literal\n\nimport click\nfrom click.exceptions import Abort, Exit\nfrom llama_agents.cli.commands.auth import validate_authenticated_profile\nfrom llama_agents.cli.env_settings import read_env_settings\nfrom llama_agents.cli.interactive import is_interactive_session, select_or_exit\nfrom llama_agents.cli.options import native_tls_option\nfrom llama_agents.cli.output import status, warning\nfrom llama_agents.cli.utils.capabilities import probe_organizations_support\nfrom llama_agents.cli.utils.redact import redact_api_key\nfrom llama_agents.core.config import DEFAULT_DEPLOYMENT_FILE_PATH\nfrom llama_agents.core.deployment_config import (\n    read_deployment_config_from_git_root_or_cwd,\n)\nfrom llama_agents.core.schema.projects import OrgSummary, ProjectSummary\n\nfrom ..app import app\n\nif TYPE_CHECKING:\n    from llama_agents.cli.config.schema import Auth\n\nlogger = logging.getLogger(__name__)\n_ClickPath = getattr(click, \"Path\")\n\n\n@app.command(\n    \"serve\",\n    help=\"Serve a LlamaDeploy app locally for development and testing.\",\n)\n@click.argument(\n    \"deployment_file\",\n    required=False,\n    default=DEFAULT_DEPLOYMENT_FILE_PATH,\n    type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),\n)\n@click.option(\n    \"--no-install\", is_flag=True, help=\"Skip installing python and js dependencies\"\n)\n@click.option(\n    \"--no-reload\", is_flag=True, help=\"Skip reloading the API server on code changes\"\n)\n@click.option(\"--no-open-browser\", is_flag=True, help=\"Skip opening the browser\")\n@click.option(\n    \"--preview\",\n    is_flag=True,\n    help=\"Preview mode pre-builds the UI to static files, like a production build\",\n)\n@click.option(\"--port\", type=int, help=\"The port to run the API server on\")\n@click.option(\"--ui-port\", type=int, help=\"The port to run the UI proxy server on\")\n@click.option(\n    \"--log-level\",\n    type=click.Choice(\n        [\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"], case_sensitive=False\n    ),\n    help=\"The log level to run the API server at\",\n)\n@click.option(\n    \"--log-format\",\n    type=click.Choice([\"console\", \"json\"], case_sensitive=False),\n    help=\"The format to use for logging\",\n)\n@click.option(\n    \"--persistence\",\n    type=click.Choice([\"memory\", \"local\", \"cloud\"]),\n    help=\"The persistence mode to use for the workflow server\",\n)\n@click.option(\n    \"--local-persistence-path\",\n    type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),\n    help=\"The path to the sqlite database to use for the workflow server if using local persistence\",\n)\n@click.option(\n    \"--host\",\n    type=str,\n    help=\"The host to run the API server on. Default is 127.0.0.1. Use 0.0.0.0 to allow remote access.\",\n)\n@native_tls_option\ndef serve(\n    deployment_file: Path,\n    no_install: bool,\n    no_reload: bool,\n    no_open_browser: bool,\n    preview: bool,\n    port: int | None = None,\n    ui_port: int | None = None,\n    log_level: str | None = None,\n    log_format: str | None = None,\n    persistence: Literal[\"memory\", \"local\", \"cloud\"] | None = None,\n    local_persistence_path: Path | None = None,\n    host: str | None = None,\n) -> None:\n    \"\"\"Run llama_deploy API Server in the foreground. Reads the deployment configuration from the current directory. Can optionally specify a deployment file path.\"\"\"\n    if not deployment_file.exists():\n        raise click.ClickException(f\"Deployment file '{deployment_file}' not found\")\n\n    # Early check: appserver requires a pyproject.toml in the config directory\n    config_dir = deployment_file if deployment_file.is_dir() else deployment_file.parent\n    if not (config_dir / \"pyproject.toml\").exists():\n        raise click.ClickException(\n            f\"No pyproject.toml found at {config_dir}.\\n\"\n            \"Add a pyproject.toml to your project and re-run 'llamactl serve'.\"\n        )\n\n    try:\n        # Pre-check: if the template requires llama cloud access, ensure credentials\n        _maybe_inject_llama_cloud_credentials(\n            deployment_file, require_cloud=persistence == \"cloud\"\n        )\n\n        # Defer heavy appserver imports until the `serve` command is actually invoked\n        from llama_agents.appserver.app import (\n            prepare_server,\n            start_server_in_target_venv,\n        )\n        from llama_agents.appserver.deployment_config_parser import (\n            get_deployment_config,\n        )\n\n        prepare_server(\n            deployment_file=deployment_file,\n            install=not no_install,\n            build=preview,\n        )\n        deployment_config = get_deployment_config()\n        _print_connection_summary()\n        start_server_in_target_venv(\n            cwd=Path.cwd(),\n            deployment_file=deployment_file,\n            proxy_ui=not preview,\n            reload=not no_reload,\n            open_browser=not no_open_browser,\n            port=port,\n            ui_port=ui_port,\n            log_level=log_level.upper() if log_level else None,\n            log_format=log_format.lower() if log_format else None,\n            persistence=persistence if persistence else \"local\",\n            local_persistence_path=str(local_persistence_path)\n            if local_persistence_path and persistence == \"local\"\n            else None,\n            cloud_persistence_name=f\"_public:serve_workflows_{deployment_config.name}\"\n            if persistence == \"cloud\"\n            else None,\n            host=host,\n        )\n\n    except (Exit, Abort):\n        raise\n\n    except KeyboardInterrupt:\n        logger.debug(\"Shutting down...\")\n\n    except Exception as e:\n        raise click.ClickException(str(e)) from e\n\n\ndef _set_env_vars_from_profile(profile: Auth) -> None:\n    if profile.api_key:\n        _set_env_vars(profile.api_key, profile.api_url)\n    _set_project_id_from_profile(profile)\n\n\ndef _set_env_vars_from_env(env_vars: dict[str, str]) -> None:\n    key = env_vars.get(\"LLAMA_CLOUD_API_KEY\")\n    url = env_vars.get(\"LLAMA_CLOUD_BASE_URL\", \"https://api.cloud.llamaindex.ai\")\n    # Also propagate project id if present in the environment\n    _set_project_id_from_env(env_vars)\n    if key:\n        _set_env_vars(key, url)\n\n\ndef _set_env_vars(key: str, url: str) -> None:\n    os.environ[\"LLAMA_CLOUD_API_KEY\"] = key\n    os.environ[\"LLAMA_CLOUD_BASE_URL\"] = url\n    # Inject prefixed copies for local dev. PUBLIC_ is the canonical convention\n    # templates should read; VITE_ and NEXT_PUBLIC_ are kept for framework\n    # defaults. These are only set by the CLI so production deployments must\n    # opt in explicitly rather than always exposing the token to client code.\n    for prefix in [\"PUBLIC_\", \"VITE_\", \"NEXT_PUBLIC_\"]:\n        os.environ[f\"{prefix}LLAMA_CLOUD_API_KEY\"] = key\n        os.environ[f\"{prefix}LLAMA_CLOUD_BASE_URL\"] = url\n\n\ndef _set_project_id_from_env(env_vars: dict[str, str]) -> None:\n    project_id = env_vars.get(\"LLAMA_AGENTS_PROJECT_ID\") or env_vars.get(\n        \"LLAMA_DEPLOY_PROJECT_ID\"\n    )\n    if project_id:\n        _set_project_id(project_id)\n\n\ndef _set_project_id_from_profile(profile: Auth) -> None:\n    if profile.project_id:\n        _set_project_id(profile.project_id)\n\n\ndef _set_project_id(project_id: str) -> None:\n    os.environ[\"LLAMA_AGENTS_PROJECT_ID\"] = project_id\n    os.environ[\"LLAMA_DEPLOY_PROJECT_ID\"] = project_id\n\n\ndef _maybe_inject_llama_cloud_credentials(\n    deployment_file: Path, require_cloud: bool\n) -> None:\n    \"\"\"If the deployment config indicates Llama Cloud usage, ensure LLAMA_CLOUD_API_KEY is set.\n\n    Behavior:\n    - If LLAMA_CLOUD_API_KEY is already set, use it.\n    - Else, try to read current profile's api_key and inject.\n    - If no profile/api_key and session is interactive, prompt to log in and inject afterward.\n    - If user declines or session is non-interactive, warn that deployment may not work.\n    \"\"\"\n    interactive = is_interactive_session()\n    from llama_agents.appserver.workflow_loader import parse_environment_variables\n    from llama_agents.cli.config.env_service import service\n\n    # Read config directly to avoid cached global settings\n    try:\n        config = read_deployment_config_from_git_root_or_cwd(\n            Path.cwd(), deployment_file\n        )\n    except Exception:\n        raise click.ClickException(\n            \"Could not read a deployment config. This doesn't appear to be a valid llama-deploy project.\"\n        )\n\n    if not config.llama_cloud and not require_cloud:\n        return\n\n    vars = parse_environment_variables(\n        config, deployment_file.parent if deployment_file.is_file() else deployment_file\n    )\n\n    # Ensure project id is available to the app and UI processes\n    _set_project_id_from_env({**os.environ, **vars})\n\n    settings = read_env_settings()\n    existing = None\n    if not settings.cloud_auth_disabled:\n        existing = settings.llama_cloud_api_key or vars.get(\"LLAMA_CLOUD_API_KEY\")\n    if existing:\n        _set_env_vars_from_env({**os.environ, **vars})\n        if interactive:\n            if not read_env_settings().llama_agents_project_id:\n                _maybe_select_project_for_env_key()\n        return\n\n    env = service.get_current_environment()\n    if not env.requires_auth:\n        warning(\n            \"LLAMA_CLOUD_API_KEY is not set and no logged-in profile was found; the app may not work\"\n        )\n        return\n\n    auth_svc = service.current_auth_service()\n    profile = auth_svc.get_current_profile()\n    if profile and profile.api_key:\n        _set_env_vars_from_profile(profile)\n        return\n\n    # No key available; consider prompting if interactive\n    if interactive:\n        should_login = click.confirm(\n            \"This deployment requires Llama Cloud. Login now to inject credentials? Otherwise the app may not work.\",\n            default=True,\n        )\n        if should_login:\n            authed = validate_authenticated_profile()\n            if authed.api_key:\n                _set_env_vars_from_profile(authed)\n                return\n        warning(\n            \"LLAMA_CLOUD_API_KEY is not set and no logged-in profile was found; the app may not work\"\n        )\n        return\n\n    # Non-interactive session\n    warning(\n        \"LLAMA_CLOUD_API_KEY is not set and no logged-in profile was found; the app may not work\"\n    )\n\n\ndef _maybe_select_project_for_env_key() -> None:\n    \"\"\"When using an env API key, ensure LLAMA_AGENTS_PROJECT_ID is set.\n\n    If more than one project exists, prompt the user to select one.\n    \"\"\"\n    from llama_agents.core.client.manage_client import ControlPlaneClient\n\n    settings = read_env_settings()\n    api_key = settings.llama_cloud_api_key\n    base_url = settings.normalized_base_url\n    if not api_key:\n        return\n    try:\n        supports_organizations = probe_organizations_support()\n\n        async def _run() -> tuple[OrgSummary | None, list[ProjectSummary]]:\n            async with ControlPlaneClient.ctx(base_url, api_key, None) as client:\n                org: OrgSummary | None = None\n                if supports_organizations:\n                    organizations = await client.list_organizations()\n                    org = next(\n                        (o for o in organizations if o.is_default),\n                        organizations[0] if organizations else None,\n                    )\n                org_id = org.org_id if org is not None else None\n                projects = await client.list_projects(org_id=org_id)\n                return org, projects\n\n        org, projects = asyncio.run(_run())\n        if not projects:\n            return\n        if len(projects) == 1:\n            _set_project_id(projects[0].project_id)\n            return\n\n        if org is not None:\n            status(f\"projects for organization {org.org_name}\")\n\n        # Multiple: prompt selection\n        current_project_id = settings.llama_agents_project_id\n        project_items = []\n        current_idx = 0\n        for i, p in enumerate(projects):\n            label = (\n                f\"{p.project_id}  {p.project_name} ({p.deployment_count} deployments)\"\n            )\n            if p.project_id == current_project_id:\n                label += \" [current]\"\n                current_idx = i\n            project_items.append((p.project_id, label))\n        choice = select_or_exit(\n            project_items,\n            \"Select a project\",\n            hint_flag=\"LLAMA_AGENTS_PROJECT_ID\",\n            hint_command=\"llamactl projects use <project_id>\",\n            selected=current_idx,\n        )\n        _set_project_id(choice)\n    except Exception:\n        # Best-effort; if we fail to list, do nothing\n        pass\n\n\ndef _print_connection_summary() -> None:\n    settings = read_env_settings()\n    if not settings.has_cloud_connection_summary:\n        return\n\n    base_url = settings.llama_cloud_base_url\n    project_id = settings.llama_agents_project_id\n    api_key = settings.llama_cloud_api_key\n    redacted = redact_api_key(api_key)\n    env_text = base_url or \"-\"\n    proj_text = project_id or \"-\"\n    status(\n        f\"connecting to environment {env_text}, project {proj_text}, api key {redacted}\"\n    )\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/_config.py",
    "content": "\"\"\"Configuration and profile management for llamactl\"\"\"\n\nimport functools\nimport sqlite3\nimport uuid\nfrom pathlib import Path\nfrom typing import Any\n\nfrom llama_agents.cli.paths import resolve_llamactl_config_dir\n\nfrom ._migrations import run_migrations\nfrom .schema import DEFAULT_ENVIRONMENT, Auth, DeviceOIDC, Environment\n\n\ndef _serialize_device_oidc(value: DeviceOIDC | None) -> str | None:\n    if value is None:\n        return None\n    return value.model_dump_json()\n\n\ndef _deserialize_device_oidc(value: str | None) -> DeviceOIDC | None:\n    if not value:\n        return None\n    return DeviceOIDC.model_validate_json(value)\n\n\ndef _to_auth(row: Any) -> Auth:\n    return Auth(\n        id=row[0],\n        name=row[1],\n        api_url=row[2],\n        project_id=row[3],\n        api_key=row[4],\n        api_key_id=row[5],\n        device_oidc=_deserialize_device_oidc(row[6]),\n    )\n\n\ndef _to_environment(row: Any) -> Environment:\n    return Environment(\n        api_url=row[0],\n        requires_auth=bool(row[1]),\n        min_llamactl_version=row[2],\n    )\n\n\nclass ConfigManager:\n    \"\"\"Manages profiles and configuration using SQLite\"\"\"\n\n    def __init__(self, init_database: bool = True):\n        self.config_dir = self._get_config_dir()\n        self.db_path = self.config_dir / \"profiles.db\"\n        self._ensure_config_dir()\n        if init_database:\n            self._init_database()\n\n    def _get_config_dir(self) -> Path:\n        \"\"\"Get the configuration directory path based on OS.\n\n        Honors LLAMACTL_CONFIG_DIR when set. This helps tests isolate state.\n        \"\"\"\n        return resolve_llamactl_config_dir()\n\n    def _ensure_config_dir(self) -> None:\n        \"\"\"Create configuration directory if it doesn't exist\"\"\"\n        self.config_dir.mkdir(parents=True, exist_ok=True)\n\n    def _init_database(self) -> None:\n        \"\"\"Initialize SQLite database and run migrations; then seed defaults.\"\"\"\n\n        with sqlite3.connect(self.db_path) as conn:\n            # Apply ad-hoc SQL migrations based on PRAGMA user_version\n            # Pass db_path to enable file-based locking across processes\n            run_migrations(conn, self.db_path)\n\n            conn.commit()\n\n    def destroy_database(self) -> None:\n        \"\"\"Destroy the database\"\"\"\n        self.db_path.unlink()\n        self._init_database()\n\n    #############################################\n    ## Settings\n    #############################################\n\n    def set_settings_current_profile(self, name: str | None) -> None:\n        \"\"\"Set or clear the current active profile.\n\n        If name is None, the setting is removed.\n        \"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            if name is None:\n                conn.execute(\"DELETE FROM settings WHERE key = 'current_profile'\")\n            else:\n                conn.execute(\n                    \"INSERT OR REPLACE INTO settings (key, value) VALUES ('current_profile', ?)\",\n                    (name,),\n                )\n            conn.commit()\n\n    def get_settings_current_profile_name(self) -> str | None:\n        \"\"\"Get the name of the current active profile\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            cursor = conn.execute(\n                \"SELECT value FROM settings WHERE key = 'current_profile'\"\n            )\n            row = cursor.fetchone()\n            return row[0] if row else None\n\n    def set_settings_current_environment(self, api_url: str) -> None:\n        \"\"\"Set the current environment by URL.\n\n        Requires the environment row to already exist (validated elsewhere, e.g. via\n        a probe before creation). Raises ValueError if not found.\n        \"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            conn.execute(\n                \"INSERT OR REPLACE INTO settings (key, value) VALUES ('current_environment_api_url', ?)\",\n                (api_url,),\n            )\n            conn.commit()\n\n    def create_profile(\n        self,\n        name: str,\n        api_url: str,\n        project_id: str,\n        api_key: str | None = None,\n        api_key_id: str | None = None,\n        device_oidc: DeviceOIDC | None = None,\n    ) -> Auth:\n        \"\"\"Create a new auth profile\"\"\"\n        if not project_id.strip():\n            raise ValueError(\"Project ID is required\")\n        profile = Auth(\n            id=str(uuid.uuid4()),\n            name=name,\n            api_url=api_url,\n            project_id=project_id,\n            api_key=api_key,\n            api_key_id=api_key_id,\n            device_oidc=device_oidc,\n        )\n\n        with sqlite3.connect(self.db_path) as conn:\n            try:\n                conn.execute(\n                    \"INSERT INTO profiles (id, name, api_url, project_id, api_key, api_key_id, device_oidc) VALUES (?, ?, ?, ?, ?, ?, ?)\",\n                    (\n                        profile.id,\n                        profile.name,\n                        profile.api_url,\n                        profile.project_id,\n                        profile.api_key,\n                        profile.api_key_id,\n                        _serialize_device_oidc(profile.device_oidc),\n                    ),\n                )\n                conn.commit()\n            except sqlite3.IntegrityError:\n                raise ValueError(\n                    f\"Profile '{name}' already exists for environment '{api_url}'\"\n                )\n\n        return profile\n\n    def get_current_profile(self, env_url: str) -> Auth | None:\n        \"\"\"Get the current active profile\"\"\"\n        current_name = self.get_settings_current_profile_name()\n        if current_name:\n            return self.get_profile(current_name, env_url)\n        return None\n\n    def get_current_environment(self) -> Environment:\n        \"\"\"Get the current active environment\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            cursor = conn.execute(\n                \"SELECT value FROM settings WHERE key = 'current_environment_api_url'\"\n            )\n            row = cursor.fetchone()\n            api_url = row[0] if row else DEFAULT_ENVIRONMENT.api_url\n\n            env_row = conn.execute(\n                \"SELECT api_url, requires_auth, min_llamactl_version FROM environments WHERE api_url = ?\",\n                (api_url,),\n            ).fetchone()\n            if env_row:\n                return _to_environment(env_row)\n\n        # Fallback: return an in-memory default without writing to DB\n        if api_url == DEFAULT_ENVIRONMENT.api_url:\n            return DEFAULT_ENVIRONMENT\n        return Environment(\n            api_url=api_url, requires_auth=False, min_llamactl_version=None\n        )\n\n    ##################################\n    ## Profiles\n    ##################################\n\n    def get_profile(self, name: str, env_url: str) -> Auth | None:\n        \"\"\"Get a profile by name\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            row = conn.execute(\n                \"SELECT id, name, api_url, project_id, api_key, api_key_id, device_oidc FROM profiles WHERE name = ? AND api_url = ?\",\n                (name, env_url),\n            ).fetchone()\n            if row:\n                return _to_auth(row)\n        return None\n\n    def get_profile_by_id(self, id: str) -> Auth | None:\n        \"\"\"Get a profile by ID\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            row = conn.execute(\n                \"SELECT id, name, api_url, project_id, api_key, api_key_id, device_oidc FROM profiles WHERE id = ?\",\n                (id,),\n            ).fetchone()\n            if row:\n                return _to_auth(row)\n        return None\n\n    def get_profile_by_api_key(self, env_url: str, api_key: str) -> Auth | None:\n        \"\"\"Get a profile by api_key within an environment URL.\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            row = conn.execute(\n                \"\"\"\n                SELECT id, name, api_url, project_id, api_key, api_key_id, device_oidc\n                FROM profiles\n                WHERE api_url = ? AND api_key = ?\n                LIMIT 1\n                \"\"\",\n                (env_url, api_key),\n            ).fetchone()\n            if row:\n                return _to_auth(row)\n        return None\n\n    def get_profile_by_device_user_id(self, env_url: str, user_id: str) -> Auth | None:\n        \"\"\"Get a profile by device OIDC user_id within an environment URL.\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            row = conn.execute(\n                \"\"\"\n                SELECT id, name, api_url, project_id, api_key, api_key_id, device_oidc\n                FROM profiles\n                WHERE api_url = ? AND JSON_EXTRACT(device_oidc, '$.user_id') = ?\n                LIMIT 1\n                \"\"\",\n                (env_url, user_id),\n            ).fetchone()\n            if row:\n                return _to_auth(row)\n        return None\n\n    def list_profiles(self, env_url: str) -> list[Auth]:\n        \"\"\"List all profiles\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            return [\n                _to_auth(row)\n                for row in conn.execute(\n                    \"SELECT id, name, api_url, project_id, api_key, api_key_id, device_oidc FROM profiles WHERE api_url = ? ORDER BY name\",\n                    (env_url,),\n                ).fetchall()\n            ]\n\n    def delete_profile(self, name: str, env_url: str) -> bool:\n        \"\"\"Delete a profile by name. Returns True if deleted, False if not found.\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            cursor = conn.execute(\n                \"DELETE FROM profiles WHERE name = ? AND api_url = ?\", (name, env_url)\n            )\n            conn.commit()\n\n            # If this was the active profile, clear it\n            if self.get_settings_current_profile_name() == name:\n                self.set_settings_current_profile(None)\n\n            return cursor.rowcount > 0\n\n    def set_project(self, profile_name: str, env_url: str, project_id: str) -> bool:\n        \"\"\"Set the project for a profile. Returns True if profile exists.\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            cursor = conn.execute(\n                \"UPDATE profiles SET project_id = ? WHERE name = ? AND api_url = ?\",\n                (project_id, profile_name, env_url),\n            )\n            conn.commit()\n            return cursor.rowcount > 0\n\n    def update_profile(self, profile: Auth) -> None:\n        \"\"\"Update a profile\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            conn.execute(\n                \"UPDATE profiles SET name = ?, api_url = ?, project_id = ?, api_key = ?, api_key_id = ?, device_oidc = ? WHERE id = ?\",\n                (\n                    profile.name,\n                    profile.api_url,\n                    profile.project_id,\n                    profile.api_key,\n                    profile.api_key_id,\n                    _serialize_device_oidc(profile.device_oidc),\n                    profile.id,\n                ),\n            )\n            conn.commit()\n\n    ##################################\n    ## Environments\n    ##################################\n    def create_or_update_environment(\n        self, api_url: str, requires_auth: bool, min_llamactl_version: str | None = None\n    ) -> None:\n        \"\"\"Create or update an environment row.\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            conn.execute(\n                \"INSERT OR REPLACE INTO environments (api_url, requires_auth, min_llamactl_version) VALUES (?, ?, ?)\",\n                (api_url, 1 if requires_auth else 0, min_llamactl_version),\n            )\n            conn.commit()\n\n    def get_environment(self, api_url: str) -> Environment | None:\n        \"\"\"Retrieve an environment by URL.\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            row = conn.execute(\n                \"SELECT api_url, requires_auth, min_llamactl_version FROM environments WHERE api_url = ?\",\n                (api_url,),\n            ).fetchone()\n            if row:\n                return _to_environment(row)\n        return None\n\n    def list_environments(self) -> list[Environment]:\n        \"\"\"List all environments.\"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            envs = [\n                _to_environment(row)\n                for row in conn.execute(\n                    \"SELECT api_url, requires_auth, min_llamactl_version FROM environments ORDER BY api_url\"\n                ).fetchall()\n            ]\n            if not envs:\n                envs = [DEFAULT_ENVIRONMENT]\n            return envs\n\n    def delete_environment(self, api_url: str) -> bool:\n        \"\"\"Delete an environment and all associated profiles.\n\n        Returns True if the environment existed and was deleted, False otherwise.\n        If the deleted environment was current, switch current to the default URL.\n        \"\"\"\n        with sqlite3.connect(self.db_path) as conn:\n            # Check existence\n            exists_cursor = conn.execute(\n                \"SELECT 1 FROM environments WHERE api_url = ?\",\n                (api_url,),\n            )\n            if exists_cursor.fetchone() is None:\n                return False\n\n            # Delete profiles tied to this environment\n            conn.execute(\"DELETE FROM profiles WHERE api_url = ?\", (api_url,))\n\n            # Delete environment row\n            conn.execute(\"DELETE FROM environments WHERE api_url = ?\", (api_url,))\n\n            # If current environment is this one, reset to default\n            setting_cursor = conn.execute(\n                \"SELECT value FROM settings WHERE key = 'current_environment_api_url'\"\n            )\n            row = setting_cursor.fetchone()\n            if row and row[0] == api_url:\n                conn.execute(\n                    \"INSERT OR REPLACE INTO settings (key, value) VALUES ('current_environment_api_url', ?)\",\n                    (DEFAULT_ENVIRONMENT.api_url,),\n                )\n\n            conn.commit()\n            return True\n\n\n# Global config manager instance\n@functools.cache\ndef config_manager() -> ConfigManager:\n    return ConfigManager()\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/_migrations.py",
    "content": "\"\"\"Ad-hoc SQLite schema migrations using PRAGMA user_version.\n\nInspired by https://eskerda.com/sqlite-schema-migrations-python/\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport re\nimport sqlite3\nfrom contextlib import contextmanager\nfrom importlib import import_module, resources\nfrom pathlib import Path\nfrom typing import Any, Generator\n\nlogger = logging.getLogger(__name__)\n\n\n_MIGRATIONS_PKG = \"llama_agents.cli.config.migrations\"\n_USER_VERSION_PATTERN = re.compile(r\"pragma\\s+user_version\\s*=\\s*(\\d+)\", re.IGNORECASE)\n\n\ndef _lock_file_unix(fd: int) -> None:\n    \"\"\"Acquire exclusive lock on Unix using fcntl.\"\"\"\n    import fcntl\n\n    fcntl.flock(fd, fcntl.LOCK_EX)\n\n\ndef _unlock_file_unix(fd: int) -> None:\n    \"\"\"Release lock on Unix using fcntl.\"\"\"\n    import fcntl\n\n    fcntl.flock(fd, fcntl.LOCK_UN)\n\n\n@contextmanager\ndef _file_lock(lock_path: Path) -> Generator[None, None, None]:\n    \"\"\"File lock to serialize migrations across processes.\n\n    Uses fcntl.flock on Unix. On Windows, SQLite's built-in locking provides\n    sufficient protection for typical CLI usage patterns.\n    \"\"\"\n    if os.name == \"nt\":\n        # On Windows, rely on SQLite's own file locking\n        yield\n        return\n\n    lock_path.parent.mkdir(parents=True, exist_ok=True)\n    lock_file = open(lock_path, \"w\")  # noqa: SIM115\n    try:\n        _lock_file_unix(lock_file.fileno())\n        yield\n    finally:\n        _unlock_file_unix(lock_file.fileno())\n        lock_file.close()\n\n\ndef _iter_migration_files() -> list[Any]:\n    \"\"\"Yield packaged SQL migration files in lexicographic order.\"\"\"\n    pkg = import_module(_MIGRATIONS_PKG)\n    root = resources.files(pkg)\n    files = [p for p in root.iterdir() if p.name.endswith(\".sql\")]\n    if not files:\n        raise ValueError(\"No migration files found\")\n    return sorted(files, key=lambda p: p.name)\n\n\ndef _parse_target_version(sql_text: str) -> int | None:\n    \"\"\"Return target schema version declared in the first PRAGMA line, if any.\"\"\"\n    first_line = sql_text.splitlines()[0] if sql_text else \"\"\n    match = _USER_VERSION_PATTERN.search(first_line)\n    return int(match.group(1)) if match else None\n\n\ndef _apply_pending_migrations(conn: sqlite3.Connection) -> None:\n    \"\"\"Apply pending migrations (internal, assumes lock is held).\"\"\"\n    cur = conn.cursor()\n    current_version_row = cur.execute(\"PRAGMA user_version\").fetchone()\n    current_version = int(current_version_row[0]) if current_version_row else 0\n\n    for path in _iter_migration_files():\n        sql_text = path.read_text()\n        target_version = _parse_target_version(sql_text) or 0\n        if target_version <= current_version:\n            continue\n\n        try:\n            logger.debug(\n                \"Applying migration %s → target version %s\", path.name, target_version\n            )\n            cur.executescript(\"BEGIN;\\n\" + sql_text)\n        except Exception as exc:  # noqa: BLE001 – we surface the exact error\n            logger.error(\"Failed migration %s: %s\", path.name, exc)\n            cur.execute(\"ROLLBACK\")\n            raise\n        else:\n            cur.execute(\"COMMIT\")\n            current_version = target_version\n\n\ndef run_migrations(conn: sqlite3.Connection, db_path: Path | None = None) -> None:\n    \"\"\"Apply pending migrations found under the migrations package.\n\n    Each migration file should start with a `PRAGMA user_version=N;` line.\n    Files are applied in lexicographic order and only when N > current_version.\n\n    Uses a file lock to prevent concurrent migrations across processes when\n    db_path is provided.\n    \"\"\"\n    if db_path is not None:\n        lock_path = db_path.with_suffix(\".db.lock\")\n        with _file_lock(lock_path):\n            _apply_pending_migrations(conn)\n    else:\n        _apply_pending_migrations(conn)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/auth_service.py",
    "content": "import asyncio\n\nfrom llama_agents.cli.auth.client import PlatformAuthClient, RefreshMiddleware\nfrom llama_agents.cli.config._config import Auth, ConfigManager, Environment\nfrom llama_agents.cli.config.schema import DeviceOIDC\nfrom llama_agents.cli.utils.redact import redact_api_key\nfrom llama_agents.core.client.manage_client import ControlPlaneClient, httpx\nfrom llama_agents.core.schema import VersionResponse\nfrom llama_agents.core.schema.projects import ProjectSummary\n\n\nclass AuthService:\n    def __init__(self, config_manager: ConfigManager, env: Environment):\n        self.config_manager = config_manager\n        self.env = env\n\n    def list_profiles(self) -> list[Auth]:\n        return self.config_manager.list_profiles(self.env.api_url)\n\n    def get_profile(self, name: str) -> Auth | None:\n        return self.config_manager.get_profile(name, self.env.api_url)\n\n    def get_profile_by_id(self, id: str) -> Auth | None:\n        return self.config_manager.get_profile_by_id(id)\n\n    def set_current_profile(self, name: str) -> None:\n        self.config_manager.set_settings_current_profile(name)\n\n    def select_any_profile(self) -> None:\n        # best effort to select a profile within the environment\n        profiles = self.list_profiles()\n        if profiles:\n            self.set_current_profile(profiles[0].name)\n\n    def get_current_profile(self) -> Auth | None:\n        return self.config_manager.get_current_profile(self.env.api_url)\n\n    def create_profile_from_token(self, project_id: str, api_key: str | None) -> Auth:\n        base = _auto_profile_name_from_token(api_key or \"\") if api_key else \"default\"\n        auth = self.config_manager.create_profile(\n            base, self.env.api_url, project_id, api_key\n        )\n        self.config_manager.set_settings_current_profile(auth.name)\n        return auth\n\n    def create_or_update_profile_from_oidc(\n        self, project_id: str, device_oidc: DeviceOIDC\n    ) -> Auth:\n        base = device_oidc.email\n        existing = self.config_manager.get_profile_by_device_user_id(\n            self.env.api_url, device_oidc.user_id\n        )\n        if existing:\n            existing.device_oidc = device_oidc\n            self.config_manager.update_profile(existing)\n            auth = existing\n        else:\n            auth = self.config_manager.create_profile(\n                base, self.env.api_url, project_id, device_oidc=device_oidc\n            )\n        self.config_manager.set_settings_current_profile(auth.name)\n        return auth\n\n    def update_profile(self, profile: Auth) -> None:\n        self.config_manager.update_profile(profile)\n\n    async def delete_profile(self, name: str) -> bool:\n        profile = self.get_profile(name)\n        if profile and profile.api_key_id:\n            async with self.profile_client(profile) as client:\n                try:\n                    await client.delete_api_key(profile.api_key_id)\n                except Exception:\n                    pass\n        return self.config_manager.delete_profile(name, self.env.api_url)\n\n    def set_project(self, name: str, project_id: str) -> None:\n        self.config_manager.set_project(name, self.env.api_url, project_id)\n\n    def fetch_server_version(self) -> VersionResponse:\n        async def _fetch_server_version() -> VersionResponse:\n            async with ControlPlaneClient.ctx(self.env.api_url) as client:\n                version = await client.server_version()\n                return version\n\n        return asyncio.run(_fetch_server_version())\n\n    def _validate_token_and_list_projects(self, api_key: str) -> list[ProjectSummary]:\n        async def _run() -> list[ProjectSummary]:\n            async with ControlPlaneClient.ctx(self.env.api_url, api_key) as client:\n                return await client.list_projects()\n\n        return asyncio.run(_run())\n\n    def auth_middleware(self, profile: Auth | None = None) -> httpx.Auth | None:\n        profile = profile or self.get_current_profile()\n        if profile and profile.device_oidc:\n            _profile = profile  # copy to assist type checker being inflexible\n\n            async def _on_refresh(updated: DeviceOIDC) -> None:\n                # Persist refreshed tokens to the database synchronously within async wrapper\n                self.refresh_to_db(_profile.id, updated)\n\n            return RefreshMiddleware(\n                profile.device_oidc,\n                _on_refresh,\n            )\n        return None\n\n    def refresh_to_db(self, profile_id: str, device_oidc: DeviceOIDC) -> None:\n        profile = self.get_profile_by_id(profile_id)\n        if profile:\n            profile.device_oidc = device_oidc\n            self.config_manager.update_profile(profile)\n\n    def profile_client(self, profile: Auth | None = None) -> PlatformAuthClient:\n        profile = profile or self.get_current_profile()\n        if not profile:\n            raise ValueError(\"No active profile\")\n        return PlatformAuthClient(\n            profile.api_url, profile.api_key, self.auth_middleware(profile)\n        )\n\n\ndef _auto_profile_name_from_token(api_key: str) -> str:\n    token = api_key or \"token\"\n    return redact_api_key(token)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/env_service.py",
    "content": "from dataclasses import replace\nfrom typing import Callable\n\nfrom llama_agents.cli.config.schema import Environment\n\nfrom ._config import ConfigManager, config_manager\nfrom .auth_service import AuthService\n\n\nclass EnvService:\n    def __init__(self, config_manager: Callable[[], ConfigManager]):\n        self.config_manager = config_manager\n\n    def list_environments(self) -> list[Environment]:\n        return self.config_manager().list_environments()\n\n    def get_current_environment(self) -> Environment:\n        return self.config_manager().get_current_environment()\n\n    def switch_environment(self, api_url: str) -> Environment:\n        env = self.config_manager().get_environment(api_url)\n        if not env:\n            raise ValueError(\n                f\"Environment '{api_url}' not found. Add it with 'llamactl environments add <API_URL>'\"\n            )\n        self.config_manager().set_settings_current_environment(api_url)\n        self.config_manager().set_settings_current_profile(None)\n        return env\n\n    def create_or_update_environment(self, env: Environment) -> None:\n        self.config_manager().create_or_update_environment(\n            env.api_url, env.requires_auth, env.min_llamactl_version\n        )\n        self.config_manager().set_settings_current_environment(env.api_url)\n        self.config_manager().set_settings_current_profile(None)\n\n    def delete_environment(self, api_url: str) -> bool:\n        return self.config_manager().delete_environment(api_url)\n\n    def current_auth_service(self) -> AuthService:\n        return AuthService(self.config_manager(), self.get_current_environment())\n\n    def auto_update_env(self, env: Environment) -> Environment:\n        svc = AuthService(self.config_manager(), env)\n        version = svc.fetch_server_version()\n        update = replace(env)\n        update.requires_auth = version.requires_auth\n        update.min_llamactl_version = version.min_llamactl_version\n        update.capabilities = list(version.capabilities)\n        # Persist only the SQLite-backed fields (not capabilities)\n        persisted_changed = (\n            update.requires_auth != env.requires_auth\n            or update.min_llamactl_version != env.min_llamactl_version\n        )\n        if persisted_changed:\n            self.config_manager().create_or_update_environment(\n                update.api_url, update.requires_auth, update.min_llamactl_version\n            )\n        return update\n\n    def probe_environment(self, api_url: str) -> Environment:\n        clean = api_url.rstrip(\"/\")\n        base_env = Environment(\n            api_url=clean, requires_auth=False, min_llamactl_version=None\n        )\n        svc = AuthService(self.config_manager(), base_env)\n        version = svc.fetch_server_version()\n        base_env.requires_auth = version.requires_auth\n        base_env.min_llamactl_version = version.min_llamactl_version\n        base_env.capabilities = list(version.capabilities)\n        return base_env\n\n\nservice = EnvService(config_manager)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/migrations/0001_init.sql",
    "content": "PRAGMA user_version=1;\n\n-- Initial schema for llamactl config database\n\nCREATE TABLE IF NOT EXISTS profiles (\n    name TEXT NOT NULL,\n    api_url TEXT NOT NULL,\n    project_id TEXT NOT NULL,\n    api_key TEXT,\n    PRIMARY KEY (name, api_url)\n);\n\nCREATE TABLE IF NOT EXISTS settings (\n    key TEXT PRIMARY KEY,\n    value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS environments (\n    api_url TEXT PRIMARY KEY,\n    requires_auth INTEGER NOT NULL,\n    min_llamactl_version TEXT\n);\n\n-- Seed defaults (idempotent)\n-- 1) Ensure current environment setting exists (do not overwrite if already set)\nINSERT OR IGNORE INTO settings (key, value)\nVALUES ('current_environment_api_url', 'https://api.cloud.llamaindex.ai');\n\n-- 2) Backfill environments from any existing profiles (avoid duplicates)\nINSERT OR IGNORE INTO environments (api_url, requires_auth)\nSELECT DISTINCT api_url, 0 FROM profiles;\n\n-- 3) Ensure the default cloud environment exists with auth required\nINSERT OR IGNORE INTO environments (api_url, requires_auth, min_llamactl_version)\nVALUES ('https://api.cloud.llamaindex.ai', 1, NULL);\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/migrations/0002_add_auth_fields.sql",
    "content": "PRAGMA user_version=2;\n\n-- Add new fields to profiles: api_key_id and device_oidc (stored as JSON string)\nALTER TABLE profiles ADD COLUMN api_key_id TEXT;\nALTER TABLE profiles ADD COLUMN device_oidc TEXT;\n\n-- Add synthetic identifier for profiles\nALTER TABLE profiles ADD COLUMN id TEXT;\n\n-- Populate existing rows with random UUIDv4 values\nUPDATE profiles\nSET id = lower(\n    hex(randomblob(4)) || '-' ||\n    hex(randomblob(2)) || '-' ||\n    '4' || substr(hex(randomblob(2)), 2) || '-' ||\n    substr('89ab', 1 + (abs(random()) % 4), 1) || substr(hex(randomblob(2)), 2) || '-' ||\n    hex(randomblob(6))\n)\nWHERE id IS NULL;\n\n-- Ensure id values are unique\nCREATE UNIQUE INDEX IF NOT EXISTS idx_profiles_id ON profiles(id);\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/migrations/__init__.py",
    "content": "\"\"\"SQL migrations for llamactl configuration database.\n\nFiles are applied in lexicographic order and must start with:\n\n    PRAGMA user_version=N;\n\n\"\"\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/config/schema.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass, field\n\nfrom pydantic import BaseModel\n\n\n@dataclass\nclass Auth:\n    \"\"\"Auth Profile configuration\"\"\"\n\n    id: str\n    name: str\n    api_url: str\n    project_id: str\n    api_key: str | None = None\n    # reference to the API key if we created it from device oauth, to be cleaned up\n    # once de-authenticated\n    api_key_id: str | None = None\n    device_oidc: DeviceOIDC | None = None\n\n\nclass DeviceOIDC(BaseModel):\n    \"\"\"Device OIDC configuration\"\"\"\n\n    # A name for this device, derived from the host. Used in API key name.\n    device_name: str\n    # A unique user ID to identify the user in the API. Prevents duplicate logins.\n    user_id: str\n    # email of the user\n    email: str\n    # OIDC client ID\n    client_id: str\n    # OIDC discovery URL\n    discovery_url: str\n    # usually 5m long JWT. For calling APIs.\n    device_access_token: str\n    # usually opaque, used to get new access tokens\n    device_refresh_token: str | None = None\n    # usually 1h long JWT. Contains user info (email, name, etc.)\n    device_id_token: str | None = None\n\n\n@dataclass\nclass Environment:\n    \"\"\"Environment configuration stored in SQLite.\n\n    Note: `api_url`, `requires_auth`, and `min_llamactl_version` are persisted\n    in the environments table.\n    \"\"\"\n\n    api_url: str\n    requires_auth: bool\n    min_llamactl_version: str | None = None\n    capabilities: list[str] = field(default_factory=list)\n\n\nDEFAULT_ENVIRONMENT = Environment(\n    api_url=\"https://api.cloud.llamaindex.ai\",\n    requires_auth=True,\n    min_llamactl_version=None,\n)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/debug.py",
    "content": "import logging\n\n\ndef setup_file_logging(\n    log_file: str = \"llamactl.log\", level: int = logging.DEBUG\n) -> None:\n    \"\"\"Set up global file logging for debugging CLI commands.\"\"\"\n    # Configure the root logger\n    logging.basicConfig(\n        level=level,\n        format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\n        handlers=[\n            logging.FileHandler(log_file, mode=\"a\"),\n        ],\n        force=True,  # Override any existing configuration\n    )\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/display.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Declarative column framework for tabular CLI read commands.\n\nA \"tabular read command\" emits a CLI-side display model whose fields carry\n:class:`Column` markers in their ``Annotated[]`` metadata. A small walker\n(``resolve_columns``) reads the markers; ``render_columns`` derives a\nplain-whitespace table; consumers compose this with ``render_output`` (in\n``cli.options``) to dispatch text/json/yaml/wide.\n\nThe ``Annotated[]`` channel is intentionally open: future markers\n(``YamlComment`` for pedagogical comments in template output, ``Alias`` for\nlegacy-name input tolerance on ``apply``, etc.) live alongside ``Column`` on\nthe same fields. Each consumer reads ``field.metadata`` and filters on its own\nmarker class via ``isinstance`` — markers do not register with the framework.\n\nPer-command display models (``DeploymentDisplay``, ``ReleaseDisplay``,\n``AuthProfileDisplay``, …) live with the commands that consume them and\nimport the primitives from this module.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport types\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any, Callable, Literal, Union, get_args, get_origin\n\nfrom llama_agents.cli.render import format_iso_z, gh_short, short_sha\nfrom llama_agents.core.schema.deployments import (\n    DeploymentCreate,\n    DeploymentResponse,\n    DeploymentUpdate,\n    LlamaDeploymentPhase,\n    ReleaseHistoryItem,\n)\nfrom llama_agents.core.schema.projects import OrgSummary, ProjectSummary\nfrom pydantic import BaseModel, ConfigDict\nfrom typing_extensions import Annotated\n\nSECRET_MASK = \"********\"\n\n# Sentinel value of ``DeploymentSpec.repo_url`` indicating push-mode (the CLI\n# pushes the local working tree on apply rather than pointing at a remote).\nPUSH_MODE_REPO_URL = \"\"\n\n\ndef strip_masks(spec_data: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Remove :data:`SECRET_MASK` sentinels from a serialized spec dict.\n\n    - ``secrets``: drop entries whose value equals the mask; drop the key\n      entirely if no entries remain.\n    - ``personal_access_token``: drop the key if its value equals the mask.\n\n    The filter runs at the emit boundary so masked values never leak back\n    into apply input via ``get | edit | apply`` round-trips.\n    \"\"\"\n    out = dict(spec_data)\n    secrets = out.get(\"secrets\")\n    if isinstance(secrets, dict):\n        filtered = {k: v for k, v in secrets.items() if v != SECRET_MASK}\n        if filtered:\n            out[\"secrets\"] = filtered\n        else:\n            out.pop(\"secrets\", None)\n    if out.get(\"personal_access_token\") == SECRET_MASK:\n        out.pop(\"personal_access_token\", None)\n    return out\n\n\n@dataclass(frozen=True)\nclass Doc:\n    \"\"\"Marker placed in a field's ``Annotated[]`` metadata to attach a doc comment.\n\n    Consumed by the YAML template renderer (``cli.yaml_template.render``):\n    each ``Doc(text)`` becomes one ``## <text>`` line per ``\\\\n``-separated\n    chunk above the field's key in the rendered output. ``Doc`` coexists with\n    :class:`Column` on the same field and is read independently via\n    ``isinstance`` filtering of ``field.metadata``.\n\n    Args:\n        text: The comment body. Rendered verbatim, prefixed with ``## ``. May\n            contain ``\\\\n`` for multi-line guidance — each line emits as its\n            own ``##`` comment in the output.\n    \"\"\"\n\n    text: str\n\n\n@dataclass(frozen=True)\nclass TrailingDoc:\n    \"\"\"Marker for a short YAML note rendered after a field's scalar value.\"\"\"\n\n    text: str\n\n\n@dataclass(frozen=True)\nclass Column:\n    \"\"\"Marker placed in a field's ``Annotated[]`` metadata to declare a column.\n\n    The marker is a pure data class; it carries no behaviour. The walker\n    (:func:`resolve_columns`) discovers ``Column`` instances and the renderer\n    (:func:`render_columns`) consumes them.\n\n    Args:\n        header: Column header rendered verbatim in the table.\n        format: Optional cell formatter. Called with the raw field value\n            (only when non-None). Must return a string.\n        default: Cell text when the value (or any nested-model parent on the\n            field path) is ``None``.\n        wide: When ``True``, the column appears only under ``-o wide``.\n    \"\"\"\n\n    header: str\n    format: Callable[[Any], str] | None = None\n    default: str = \"\"\n    wide: bool = False\n\n\n@dataclass(frozen=True)\nclass ResolvedColumn:\n    \"\"\"A walker-derived column: its declaration path plus the marker.\"\"\"\n\n    path: tuple[str, ...]\n    column: Column\n\n\ndef _is_basemodel(tp: Any) -> bool:\n    return isinstance(tp, type) and issubclass(tp, BaseModel)\n\n\ndef _unwrap_optional_model(annotation: Any) -> type[BaseModel] | None:\n    \"\"\"If ``annotation`` is ``BaseModel``, ``Optional[BaseModel]`` or\n    ``BaseModel | None``, return the model class. Otherwise ``None``.\"\"\"\n\n    if _is_basemodel(annotation):\n        return annotation  # type: ignore[return-value]\n    origin = get_origin(annotation)\n    if origin is Union or origin is types.UnionType:\n        non_none = [a for a in get_args(annotation) if a is not type(None)]\n        if len(non_none) == 1 and _is_basemodel(non_none[0]):\n            return non_none[0]\n    return None\n\n\n@functools.cache\ndef resolve_columns(model_cls: type[BaseModel]) -> tuple[ResolvedColumn, ...]:\n    \"\"\"Walk a display model in declaration order and return its columns.\n\n    Field annotations carrying a ``Column`` marker yield a leaf column at the\n    field's path. Fields whose annotation is a ``BaseModel`` (or\n    ``Optional[BaseModel]``) are descended into. All other fields are skipped.\n\n    A field carrying multiple ``Column`` markers is a typo: the walker raises\n    ``ValueError`` rather than silently picking one.\n\n    Display models are assumed to form a tree, not a graph — circular\n    references are not supported.\n    \"\"\"\n\n    return tuple(_walk(model_cls, ()))\n\n\ndef _walk(model_cls: type[BaseModel], prefix: tuple[str, ...]) -> list[ResolvedColumn]:\n    out: list[ResolvedColumn] = []\n    for name, info in model_cls.model_fields.items():\n        path = prefix + (name,)\n        cols = [m for m in info.metadata if isinstance(m, Column)]\n        if len(cols) > 1:\n            raise ValueError(\n                f\"{model_cls.__name__}.{name}: multiple Column annotations on a single field\"\n            )\n        if cols:\n            out.append(ResolvedColumn(path=path, column=cols[0]))\n            continue\n        nested = _unwrap_optional_model(info.annotation)\n        if nested is not None:\n            out.extend(_walk(nested, path))\n    return out\n\n\ndef _extract_cell(row: BaseModel, column: ResolvedColumn) -> str:\n    value: Any = row\n    for part in column.path:\n        if value is None:\n            return column.column.default\n        value = getattr(value, part)\n    if value is None:\n        return column.column.default\n    if column.column.format is not None:\n        return column.column.format(value)\n    return str(value)\n\n\ndef render_columns(\n    rows: list[BaseModel] | list[Any],\n    *,\n    wide: bool = False,\n) -> None:\n    \"\"\"Render ``rows`` as a plain-whitespace table using ``Column`` metadata.\n\n    ``rows`` must be homogeneous; the row class is read from the first\n    element. An empty list still emits headers (matches ``render_table``'s\n    empty-row behaviour). Columns marked ``wide=True`` are filtered out unless\n    ``wide`` is ``True``.\n    \"\"\"\n\n    from llama_agents.cli.render import render_table  # local: avoid cycle\n\n    if not rows:\n        # No row to derive a class from. Caller is expected to have emitted a\n        # status message (\"No X found\") before reaching here — but if not,\n        # we silently emit nothing rather than guessing the column layout.\n        return\n\n    row_cls = type(rows[0])\n    if not isinstance(rows[0], BaseModel):\n        raise TypeError(\n            f\"render_columns expects BaseModel rows; got {row_cls.__name__}\"\n        )\n\n    cols = [c for c in resolve_columns(row_cls) if wide or not c.column.wide]\n    columns = [(c.column.header, c.column.header) for c in cols]\n    table_rows: list[dict[str, str]] = []\n    for row in rows:\n        table_rows.append({c.column.header: _extract_cell(row, c) for c in cols})\n    render_table(table_rows, columns)\n\n\nclass DeploymentSpec(BaseModel):\n    \"\"\"Editable deployment fields.\n\n    Every editable field is ``Optional`` (or has a default of ``None``) so the\n    same model serves three roles: (1) projection of a server-known deployment\n    (``DeploymentDisplay.from_response`` populates everything explicitly),\n    (2) input shape for ``apply`` (any subset is valid; create-time required\n    fields are enforced by the apply translator, not this model), (3) input\n    shape for partial updates (``model_dump(exclude_unset=True)`` produces a\n    clean patch payload).\n\n    ``personal_access_token`` is a leaky abstraction over the server-side\n    ``GITHUB_PAT`` secret — it is surfaced here as a dedicated field rather\n    than mixed into ``secrets`` so the apply input shape is explicit.\n    \"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    repo_url: Annotated[\n        str | None,\n        Column(\"REPO\", format=gh_short, default=\"-\"),\n        Doc(\n            '\"\" = push your local working tree (use for new deployments).\\n'\n            '\"internal://\" = push your local working tree (use for existing deployments).\\n'\n            \"https://… = remote git URL (GitHub, GitLab, etc.).\\n\"\n            \"Omit to keep the current value without pushing.\"\n        ),\n    ] = None\n    deployment_file_path: Annotated[\n        str | None,\n        TrailingDoc(\"pyproject.toml or llama_deploy.yaml\"),\n    ] = None\n    git_ref: Annotated[\n        str | None,\n        Column(\"GIT_REF\", default=\"-\"),\n    ] = None\n    appserver_version: Annotated[\n        str | None,\n        Column(\"APPSERVER\", default=\"-\", wide=True),\n        TrailingDoc(\"auto-pinned from local install\"),\n    ] = None\n    # No Column: suspended state is already visible via status.phase.\n    suspended: Annotated[\n        bool | None,\n        TrailingDoc(\"scale to zero without deleting\"),\n    ] = None\n    # ``str | None`` value type matches ``DeploymentUpdate.secrets`` on the wire:\n    # null values delete on apply.\n    secrets: Annotated[\n        dict[str, str | None] | None,\n        Doc(\n            \"Secret env vars. ${VAR} reads from your local environment at apply time.\\n\"\n            \"Values are masked after apply — set them, don't expect to read back.\"\n        ),\n    ] = None\n    personal_access_token: Annotated[\n        str | None,\n        TrailingDoc(\"private-repo access (GitHub PAT, GitLab access token)\"),\n    ] = None\n\n    def as_redacted(self) -> DeploymentSpec:\n        \"\"\"Return a copy with secret values masked for display-only output.\"\"\"\n        updates: dict[str, Any] = {}\n        if self.secrets is not None:\n            updates[\"secrets\"] = {\n                name: None if value is None else SECRET_MASK\n                for name, value in self.secrets.items()\n            }\n        if self.personal_access_token is not None:\n            updates[\"personal_access_token\"] = SECRET_MASK\n        return self.model_copy(update=updates)\n\n\nclass DeploymentStatus(BaseModel):\n    \"\"\"Read-only / system-set deployment status block.\n\n    These fields reflect runtime state and are not editable via ``apply``.\n    ``warning`` is intentionally always serialized (explicit ``null`` when no\n    warning is present) — absence vs. explicit-null carries meaning for the\n    future ``describe`` command.\n    \"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    # `LlamaDeploymentPhase | str` tolerates unknown phase values from a newer\n    # server; the column renders whatever string the server sent.\n    phase: Annotated[LlamaDeploymentPhase | str, Column(\"PHASE\")]\n    git_sha: Annotated[\n        str | None, Column(\"GIT_SHA\", format=short_sha, default=\"-\", wide=True)\n    ] = None\n    apiserver_url: Annotated[\n        str | None, Column(\"APISERVER_URL\", default=\"-\", wide=True)\n    ] = None\n    project_id: Annotated[str, Column(\"PROJECT\", wide=True)]\n    warning: str | None = None\n\n\nclass PayloadError(ValueError):\n    \"\"\"Validation error from payload construction, with a YAML field path.\"\"\"\n\n    def __init__(self, message: str, path: tuple[str | int, ...]) -> None:\n        self.path = path\n        super().__init__(message)\n\n\nclass DeploymentDisplay(BaseModel):\n    \"\"\"CLI projection of a deployment.\n\n    Top-level ``name`` is the stable id (immutable on update). ``spec``\n    carries the editable surface. ``status`` carries everything set by the\n    server. Use :meth:`from_response` to translate a ``DeploymentResponse``\n    into this shape; use :meth:`to_output_dict` to obtain a dict suitable for\n    JSON/YAML emission with the omit-when-empty rules applied.\n    \"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    # ``None`` is used by the ``deployments template`` command to render the\n    # top-level key as a commented-out example; ``from_response`` always\n    # populates it from the wire id.\n    name: Annotated[\n        str | None,\n        Column(\"NAME\", default=\"-\"),\n        Doc(\"Stable id for the deployment. Immutable on update.\"),\n    ] = None\n    # Slug seed used by the server when top-level ``name`` is unset. Surfaced\n    # at the identity tier (sibling to ``name``) since it answers a\n    # what-id-do-I-get question, not an editable-spec question. Wire-side\n    # the field is still ``display_name`` on ``DeploymentResponse``; the CLI\n    # flattens at :meth:`from_response`.\n    generate_name: str | None = None\n    spec: DeploymentSpec\n    status: DeploymentStatus | None = None\n\n    @classmethod\n    def from_response(cls, r: DeploymentResponse) -> DeploymentDisplay:\n        \"\"\"Project a wire ``DeploymentResponse`` into the CLI display shape.\"\"\"\n        secret_names = r.secret_names or []\n        secrets: dict[str, str | None] | None = (\n            {name: SECRET_MASK for name in secret_names} if secret_names else None\n        )\n        pat = SECRET_MASK if r.has_personal_access_token else None\n        spec = DeploymentSpec(\n            repo_url=r.repo_url,\n            deployment_file_path=r.deployment_file_path,\n            git_ref=r.git_ref,\n            appserver_version=r.appserver_version,\n            suspended=r.suspended,\n            secrets=secrets,\n            personal_access_token=pat,\n        )\n        status = DeploymentStatus(\n            phase=r.status,\n            git_sha=r.git_sha,\n            apiserver_url=str(r.apiserver_url) if r.apiserver_url else None,\n            project_id=r.project_id,\n            warning=r.warning,\n        )\n        return cls(name=r.id, generate_name=r.display_name, spec=spec, status=status)\n\n    def to_output_dict(self) -> dict[str, Any]:\n        \"\"\"Return the dict shape used for JSON/YAML rendering.\n\n        Omits fields inside ``spec`` whose value is None.  Masked secret\n        placeholders (``********``) are preserved so the output shows which\n        secrets exist.  ``generate_name`` is emitted at the top level only\n        when set. The nested ``status`` block is preserved verbatim so its\n        ``warning`` key remains explicit even when ``null``.\n        \"\"\"\n        spec_data = self.spec.model_dump(mode=\"json\")\n        spec_data = {k: v for k, v in spec_data.items() if v is not None}\n        data: dict[str, Any] = {\"name\": self.name}\n        if self.generate_name is not None:\n            data[\"generate_name\"] = self.generate_name\n        data[\"spec\"] = spec_data\n        if self.status is not None:\n            data[\"status\"] = self.status.model_dump(mode=\"json\")\n        return data\n\n    def without_mask_sentinels(self) -> DeploymentDisplay:\n        \"\"\"Return a copy with apply-unsafe mask sentinel values removed.\"\"\"\n        spec_data = self.spec.model_dump(mode=\"json\", exclude_unset=True)\n        stripped_spec = strip_masks(spec_data)\n        if stripped_spec == spec_data:\n            return self\n\n        data = self.model_dump(mode=\"json\", exclude_unset=True)\n        data[\"spec\"] = stripped_spec\n        return DeploymentDisplay.model_validate(data)\n\n    def to_create_payload(self) -> DeploymentCreate:\n        \"\"\"Translate this display model into the create wire model.\"\"\"\n        spec = self.spec\n\n        if spec.suspended is not None:\n            raise PayloadError(\n                \"cannot create a deployment as suspended; \"\n                \"create it first, then update with --suspended\",\n                (\"spec\", \"suspended\"),\n            )\n\n        if spec.secrets is not None:\n            none_keys = [k for k, v in spec.secrets.items() if v is None]\n            if none_keys:\n                raise PayloadError(\n                    f\"cannot delete secrets on create \"\n                    f\"(null values for: {', '.join(sorted(none_keys))})\",\n                    (\"spec\", \"secrets\", none_keys[0]),\n                )\n            secrets = {k: v for k, v in spec.secrets.items() if v is not None}\n        else:\n            secrets = None\n\n        if self.generate_name is None:\n            raise PayloadError(\n                \"generate_name is required on create\", (\"generate_name\",)\n            )\n\n        return DeploymentCreate(\n            id=self.name,\n            display_name=self.generate_name,\n            repo_url=spec.repo_url if spec.repo_url is not None else \"\",\n            deployment_file_path=spec.deployment_file_path,\n            git_ref=spec.git_ref,\n            appserver_version=spec.appserver_version,\n            personal_access_token=spec.personal_access_token,\n            secrets=secrets,\n        )\n\n    def to_update_payload(self) -> DeploymentUpdate:\n        \"\"\"Translate this display model into the patch-style update wire model.\"\"\"\n        spec = self.spec\n\n        # PAT semantics:\n        #   explicit null in YAML  -> \"\" on wire (delete sentinel)\n        #   string value           -> set as given\n        #   not present            -> None (unchanged by the patch model)\n        pat = spec.personal_access_token\n        if \"personal_access_token\" in spec.model_fields_set and pat is None:\n            pat = \"\"\n\n        return DeploymentUpdate(\n            display_name=self.generate_name,\n            repo_url=spec.repo_url,\n            deployment_file_path=spec.deployment_file_path,\n            git_ref=spec.git_ref,\n            personal_access_token=pat,\n            secrets=spec.secrets,\n            appserver_version=spec.appserver_version,\n            suspended=spec.suspended,\n        )\n\n\nclass ReleaseDisplay(BaseModel):\n    \"\"\"A single release-history entry, projected for table output.\"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    released_at: Annotated[datetime, Column(\"RELEASED_AT\", format=format_iso_z)]\n    git_sha: Annotated[str, Column(\"GIT_SHA\", format=short_sha)]\n    image_tag: Annotated[str | None, Column(\"IMAGE_TAG\", default=\"-\")] = None\n\n    @classmethod\n    def from_response(cls, item: ReleaseHistoryItem) -> ReleaseDisplay:\n        return cls(\n            released_at=item.released_at,\n            git_sha=item.git_sha,\n            image_tag=item.image_tag,\n        )\n\n\ndef _yes_no(value: bool) -> str:\n    return \"yes\" if value else \"no\"\n\n\nclass AuthProfileDisplay(BaseModel):\n    \"\"\"A locally-stored auth profile, projected for ``auth get``.\n\n    Secret material (``api_key``, OIDC tokens) is intentionally not surfaced.\n    \"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    name: Annotated[str, Column(\"NAME\")]\n    api_url: Annotated[str, Column(\"API_URL\")]\n    project_id: Annotated[str | None, Column(\"PROJECT_ID\", default=\"-\")] = None\n    active: Annotated[bool, Column(\"ACTIVE\", format=_yes_no)] = False\n    auth_type: Annotated[Literal[\"none\", \"token\", \"oidc\"], Column(\"AUTH\")] = \"none\"\n\n    @classmethod\n    def from_profile(\n        cls, profile: Any, *, current_name: str | None\n    ) -> AuthProfileDisplay:\n        if profile.device_oidc:\n            auth_type: Literal[\"none\", \"token\", \"oidc\"] = \"oidc\"\n        elif profile.api_key:\n            auth_type = \"token\"\n        else:\n            auth_type = \"none\"\n        return cls(\n            name=profile.name,\n            api_url=profile.api_url,\n            project_id=profile.project_id,\n            active=profile.name == current_name,\n            auth_type=auth_type,\n        )\n\n\nclass EnvDisplay(BaseModel):\n    \"\"\"A configured environment, projected for ``environments get``.\n\n    ``min_llamactl_version`` is intentionally omitted — it isn't part of the\n    public env-list contract.\n    \"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    api_url: Annotated[str, Column(\"API_URL\")]\n    requires_auth: Annotated[bool, Column(\"REQUIRES_AUTH\", format=_yes_no)]\n    active: Annotated[bool, Column(\"ACTIVE\", format=_yes_no)] = False\n\n    @classmethod\n    def from_environment(cls, env: Any, *, current_url: str | None) -> EnvDisplay:\n        return cls(\n            api_url=env.api_url,\n            requires_auth=env.requires_auth,\n            active=env.api_url == current_url,\n        )\n\n\nclass OrgDisplay(BaseModel):\n    \"\"\"An organization summary, projected for ``organizations get``.\"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    org_name: Annotated[str, Column(\"NAME\")]\n    org_id: Annotated[str, Column(\"ORG_ID\")]\n    is_default: Annotated[bool, Column(\"DEFAULT\", format=_yes_no)]\n    active: Annotated[bool, Column(\"ACTIVE\", format=_yes_no)] = False\n\n    @classmethod\n    def from_org_summary(\n        cls, org: OrgSummary, *, current_org_id: str | None = None\n    ) -> OrgDisplay:\n        return cls(\n            org_id=org.org_id,\n            org_name=org.org_name,\n            is_default=org.is_default,\n            active=current_org_id is not None and org.org_id == current_org_id,\n        )\n\n    def to_output_dict(self) -> dict[str, Any]:\n        return {\n            \"org_id\": self.org_id,\n            \"org_name\": self.org_name,\n            \"is_default\": self.is_default,\n            \"active\": self.active,\n        }\n\n\nclass ProjectDisplay(BaseModel):\n    \"\"\"A project summary, projected for ``projects get``.\"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    project_name: Annotated[str, Column(\"NAME\")]\n    project_id: Annotated[str, Column(\"PROJECT_ID\")]\n    deployment_count: Annotated[int, Column(\"DEPLOYMENTS\")]\n    active: Annotated[bool, Column(\"ACTIVE\", format=_yes_no)] = False\n\n    @classmethod\n    def from_project_summary(\n        cls, project: ProjectSummary, *, current_project_id: str | None = None\n    ) -> ProjectDisplay:\n        return cls(\n            project_id=project.project_id,\n            project_name=project.project_name,\n            deployment_count=project.deployment_count,\n            active=project.project_id == current_project_id,\n        )\n\n    def to_output_dict(self) -> dict[str, Any]:\n        return {\n            \"project_id\": self.project_id,\n            \"project_name\": self.project_name,\n            \"deployment_count\": self.deployment_count,\n            \"active\": self.active,\n        }\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/env.py",
    "content": "\"\"\"Environment variable handling utilities for llamactl\"\"\"\n\nfrom io import StringIO\nfrom typing import Dict\n\nfrom dotenv import dotenv_values\nfrom llama_agents.cli.output import warning\n\n\ndef load_env_secrets_from_string(env_content: str) -> Dict[str, str]:\n    \"\"\"\n    Load environment variables from string content to use as secrets.\n\n    Args:\n        env_content: String content containing environment variables in .env format\n\n    Returns:\n        Dictionary of environment variable names and values\n    \"\"\"\n    try:\n        # Use StringIO to create a file-like object from the string\n        # dotenv_values can parse from a stream\n        env_stream = StringIO(env_content)\n        secrets = dotenv_values(stream=env_stream)\n        # Filter out None values and convert to strings\n        return {k: str(v) for k, v in secrets.items() if v is not None}\n    except Exception as e:\n        warning(f\"could not parse environment variables from string: {e}\")\n        return {}\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/env_settings.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom llama_agents.cli.config.schema import DEFAULT_ENVIRONMENT\nfrom pydantic import Field, field_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass LlamactlEnvSettings(BaseSettings):\n    model_config = SettingsConfigDict(extra=\"ignore\")\n\n    llama_cloud_api_key: str | None = None\n    llama_cloud_base_url: str = DEFAULT_ENVIRONMENT.api_url\n    llama_agents_project_id: str | None = None\n    llama_cloud_use_profile: bool = False\n    llamactl_complete: str | None = Field(\n        default=None,\n        validation_alias=\"_LLAMACTL_COMPLETE\",\n    )\n\n    @field_validator(\"llama_cloud_api_key\", \"llama_agents_project_id\", mode=\"before\")\n    @classmethod\n    def _empty_string_is_unset(cls, value: Any) -> Any:\n        if value == \"\":\n            return None\n        return value\n\n    @field_validator(\"llama_cloud_base_url\", mode=\"before\")\n    @classmethod\n    def _empty_base_url_uses_default(cls, value: Any) -> Any:\n        if value == \"\":\n            return DEFAULT_ENVIRONMENT.api_url\n        return value\n\n    @property\n    def normalized_base_url(self) -> str:\n        return self.llama_cloud_base_url.rstrip(\"/\")\n\n    @property\n    def has_complete_cloud_auth(self) -> bool:\n        return bool(self.llama_cloud_api_key and self.llama_agents_project_id)\n\n    @property\n    def has_cloud_connection_summary(self) -> bool:\n        return bool(\n            self.llama_cloud_api_key\n            or self.llama_agents_project_id\n            or \"llama_cloud_base_url\" in self.model_fields_set\n        )\n\n    @property\n    def completion_active(self) -> bool:\n        return self.llamactl_complete is not None\n\n    @property\n    def cloud_auth_disabled(self) -> bool:\n        return self.llama_cloud_use_profile\n\n\ndef read_env_settings() -> LlamactlEnvSettings:\n    return LlamactlEnvSettings()\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/interactive.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom collections.abc import Sequence\nfrom typing import NoReturn, TypeVar\n\nimport click\n\nT = TypeVar(\"T\")\n\n_TERM_MENU_UNSUPPORTED = sys.platform == \"win32\"\n\n\ndef is_interactive_session() -> bool:\n    \"\"\"Return whether the current CLI session can prompt the user.\"\"\"\n    if os.environ.get(\"CI\"):\n        return False\n    if not (sys.stdin.isatty() and sys.stdout.isatty()):\n        return False\n    if os.environ.get(\"TERM\") == \"dumb\":\n        return False\n    return True\n\n\ndef _raise_non_interactive(\n    entries: list[tuple[T, str]],\n    title: str,\n    hint_flag: str,\n    hint_command: str | None,\n) -> T:\n    \"\"\"Print available choices to stderr and raise with a hint.\"\"\"\n    if title:\n        click.echo(title, err=True)\n    for _, label in entries:\n        if label:\n            click.echo(f\"- {label}\", err=True)\n    hint = f\"Pass {hint_flag} to choose one.\"\n    if hint_command is not None:\n        hint += f\" To inspect choices, run: {hint_command}\"\n    raise click.ClickException(hint)\n\n\ndef require_or_list_choices(\n    items: Sequence[tuple[str, str]],\n    hint_command: str,\n    empty_message: str | None = None,\n) -> NoReturn:\n    \"\"\"Print available choices to stderr and raise with an actionable hint.\n\n    Unlike ``select_or_exit`` this never shows a picker — it always lists\n    the available items and tells the user what command to run.  Use this\n    for action commands where the user should already know the target.\n    \"\"\"\n    if not items:\n        raise click.ClickException(empty_message or \"No items available\")\n    click.echo(\"Available:\", err=True)\n    for _, label in items:\n        if label:\n            click.echo(f\"  {label}\", err=True)\n    raise click.ClickException(f\"Run: {hint_command}\")\n\n\ndef _blessed_select(labels: list[str], title: str, selected: int = 0) -> int | None:\n    \"\"\"Show an interactive menu with type-to-filter. Returns selected index or None.\"\"\"\n    # Deferred for CLI startup: only commands that actually show a menu pay the cost.\n    from blessed import Terminal\n\n    term = Terminal()\n    query = \"\"\n    cursor = \"> \"\n    max_visible = min(15, term.height - 4)\n    selected = max(0, min(selected, len(labels) - 1))\n    scroll_offset = max(0, selected - max_visible + 1) if selected >= max_visible else 0\n    out = sys.stdout\n\n    def filtered() -> list[tuple[int, str]]:\n        if not query:\n            return list(enumerate(labels))\n        q = query.lower()\n        return [(i, label) for i, label in enumerate(labels) if q in label.lower()]\n\n    def highlight_matches(text: str) -> str:\n        if not query:\n            return text\n        q = query.lower()\n        pos = text.lower().find(q)\n        if pos == -1:\n            return text\n        before = text[:pos]\n        match = text[pos : pos + len(query)]\n        after = text[pos + len(query) :]\n        return before + term.yellow + term.bold + match + term.normal + after\n\n    def writeln(text: str) -> None:\n        # Truncate to terminal width so wrapped lines don't break cursor-up math.\n        col0 = term.move_x(0)  # type: ignore[reportArgumentType]  # ty: ignore[invalid-argument-type]\n        if term.length(text) > term.width:\n            text = term.truncate(text, term.width - 1) + \"…\"\n        out.write(col0 + term.clear_eol + text + \"\\r\\n\")\n\n    def render() -> int:\n        matches = filtered()\n        visible = matches[scroll_offset : scroll_offset + max_visible]\n        lines = 0\n\n        if title:\n            writeln(term.bold + title + term.normal)\n            lines += 1\n\n        count = term.dim + f\" [{len(matches)}/{len(labels)}]\" + term.normal\n        if query:\n            writeln(\"/ \" + query + count)\n        else:\n            writeln(term.dim + \"type to filter...\" + term.normal + count)\n        lines += 1\n\n        for i, (_orig_idx, label) in enumerate(visible):\n            pos = i + scroll_offset\n            if pos == selected:\n                writeln(term.bold + cursor + highlight_matches(label) + term.normal)\n            else:\n                writeln(\" \" * len(cursor) + highlight_matches(label))\n            lines += 1\n\n        out.write(term.clear_eos)\n        out.flush()\n        return lines\n\n    with term.cbreak(), term.hidden_cursor():\n        lines_drawn = render()\n\n        while True:\n            key = term.inkey()\n\n            if key.name == \"KEY_ESCAPE\" or key == \"\\x03\":\n                out.write(\"\\n\")\n                out.flush()\n                return None\n\n            if key.name == \"KEY_ENTER\":\n                matches = filtered()\n                if matches and 0 <= selected < len(matches):\n                    out.write(\"\\n\")\n                    out.flush()\n                    return matches[selected][0]\n                continue\n\n            if key.name == \"KEY_UP\":\n                if selected > 0:\n                    selected -= 1\n                    if selected < scroll_offset:\n                        scroll_offset = selected\n            elif key.name == \"KEY_DOWN\":\n                matches = filtered()\n                if selected < len(matches) - 1:\n                    selected += 1\n                    if selected >= scroll_offset + max_visible:\n                        scroll_offset = selected - max_visible + 1\n            elif key.name == \"KEY_BACKSPACE\" or key == \"\\x7f\":\n                if query:\n                    query = query[:-1]\n                    selected = 0\n                    scroll_offset = 0\n            elif key and not key.is_sequence and key.isprintable():\n                query += str(key)\n                selected = 0\n                scroll_offset = 0\n            else:\n                continue\n\n            if lines_drawn > 0:\n                out.write(f\"\\x1b[{lines_drawn}A\")\n            out.write(term.move_x(0))  # type: ignore[reportArgumentType]  # ty: ignore[invalid-argument-type]\n            lines_drawn = render()\n\n\ndef select_or_exit(\n    items: Sequence[tuple[T, str]],\n    title: str,\n    hint_flag: str,\n    hint_command: str | None = None,\n    empty_message: str | None = None,\n    interactive: bool | None = None,\n    selected: int = 0,\n) -> T:\n    entries = list(items)\n    if not entries:\n        raise click.ClickException(empty_message or \"No items to select\")\n\n    should_prompt = is_interactive_session() if interactive is None else interactive\n\n    if not should_prompt or _TERM_MENU_UNSUPPORTED:\n        return _raise_non_interactive(entries, title, hint_flag, hint_command)\n\n    try:\n        selected_index = _blessed_select(\n            [label for _, label in entries],\n            title,\n            selected=selected,\n        )\n    except ImportError:\n        return _raise_non_interactive(entries, title, hint_flag, hint_command)\n\n    if selected_index is None:\n        raise click.ClickException(\"Cancelled\")\n    return entries[selected_index][0]\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/local_context.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Gather local context for deployment scaffolding.\n\nReads the current working directory: git remote / branch, deployment config,\n``.env`` secrets, and the installed appserver version. Produces a\n:class:`LocalContext` consumable by ``deployments template`` and the editor\nworkflow.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom urllib.parse import urlsplit\n\nfrom llama_agents.cli.env import load_env_secrets_from_string\nfrom llama_agents.cli.utils.version import get_installed_appserver_version\nfrom llama_agents.core.deployment_config import (\n    DEFAULT_DEPLOYMENT_NAME,\n    read_deployment_config,\n)\nfrom llama_agents.core.git.git_util import (\n    get_current_branch,\n    get_git_root,\n    is_git_repo,\n    list_remotes,\n)\n\n# Module-level — reused per remote in :func:`normalize_git_url_to_http`.\n_SCP_URL_RE = re.compile(r\"^(?:(?P<user>[^@]+)@)?(?P<host>[^:/\\s]+):(?P<path>[^/].+)$\")\n\n\n@dataclass(frozen=True)\nclass LocalContext:\n    \"\"\"Local-machine signals used to scaffold a deployment YAML.\n\n    All fields are passive: this is a snapshot of what we observed, not a\n    decision about what the resulting deployment should look like.\n    \"\"\"\n\n    is_git_repo: bool = False\n    repo_url: str | None = None\n    git_ref: str | None = None\n    generate_name: str | None = None\n    deployment_file_path: str | None = None\n    available_secrets: dict[str, str] = field(default_factory=dict)\n    required_secret_names: list[str] = field(default_factory=list)\n    installed_appserver_version: str | None = None\n    warnings: list[str] = field(default_factory=list)\n\n\ndef gather_local_context() -> LocalContext:\n    \"\"\"Read cwd-rooted signals into a :class:`LocalContext`.\n\n    Failures while parsing the deployment config become a warning rather than\n    an exception so the scaffolding command stays useful in broken trees.\n    \"\"\"\n\n    warnings: list[str] = []\n    generate_name: str | None = None\n    deployment_file_path: str | None = None\n    required_secret_names: list[str] = []\n\n    has_git = is_git_repo()\n    try:\n        config = read_deployment_config(Path(\".\"), Path(\".\"))\n        if config.name != DEFAULT_DEPLOYMENT_NAME:\n            generate_name = config.name\n        required_secret_names = list(config.required_env_vars)\n    except Exception:\n        warnings.append(\"Could not parse local deployment config. It may be invalid.\")\n\n    repo_url: str | None = None\n    git_ref: str | None = None\n    if has_git:\n        repo_url = pick_preferred_remote(list_remotes())\n        git_ref = get_current_branch()\n        try:\n            root = get_git_root()\n            if root != Path.cwd():\n                deployment_file_path = str(Path.cwd().relative_to(root))\n        except Exception:\n            pass\n\n    available_secrets: dict[str, str] = {}\n    try:\n        available_secrets = load_env_secrets_from_string(Path(\".env\").read_text())\n    except FileNotFoundError:\n        # No `.env` is the common offline path — no warning.\n        pass\n    except OSError as exc:\n        warnings.append(f\"Could not read .env: {exc.strerror or exc}\")\n\n    return LocalContext(\n        is_git_repo=has_git,\n        repo_url=repo_url,\n        git_ref=git_ref,\n        generate_name=generate_name,\n        deployment_file_path=deployment_file_path,\n        available_secrets=available_secrets,\n        required_secret_names=required_secret_names,\n        installed_appserver_version=get_installed_appserver_version(),\n        warnings=warnings,\n    )\n\n\ndef pick_preferred_remote(remotes: list[str]) -> str | None:\n    \"\"\"Normalize and dedupe remotes, preferring github.com over others.\"\"\"\n    seen: set[str] = set()\n    best: str | None = None\n    for remote in remotes:\n        normalized = normalize_git_url_to_http(remote)\n        if normalized in seen:\n            continue\n        seen.add(normalized)\n        if best is None or (\"github.com\" in normalized and \"github.com\" not in best):\n            best = normalized\n    return best\n\n\ndef normalize_git_url_to_http(url: str) -> str:\n    \"\"\"Best-effort normalize a git URL to an https URL.\n\n    Handles the common SSH/SCP shapes (``git@host:path``,\n    ``ssh://git@host:port/path``) and bare ``host/path`` strings. Strips\n    credentials and any explicit port.\n    \"\"\"\n    candidate = (url or \"\").strip()\n\n    has_scheme = \"://\" in candidate\n    if not has_scheme:\n        scp_match = _SCP_URL_RE.match(candidate)\n        if scp_match:\n            host = scp_match.group(\"host\")\n            path = scp_match.group(\"path\").lstrip(\"/\")\n            if path.endswith(\".git\"):\n                path = path[:-4]\n            return f\"https://{host}/{path}\"\n\n    parsed = urlsplit(candidate if has_scheme else f\"https://{candidate}\")\n    netloc = parsed.netloc.split(\"@\", 1)[-1]\n    scheme = parsed.scheme.lower()\n    # SSH transport ports (e.g. :7999) are meaningless over HTTPS — drop them.\n    # HTTP/HTTPS ports are intentional (self-hosted on a non-standard port).\n    if scheme not in (\"http\", \"https\") and \":\" in netloc:\n        netloc = netloc.split(\":\", 1)[0]\n    path = parsed.path.lstrip(\"/\")\n    if path.endswith(\".git\"):\n        path = path[:-4]\n    if path:\n        return f\"https://{netloc}/{path}\"\n    return f\"https://{netloc}\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/log_format.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Pure body parser for structlog-formatted log lines.\n\nThe control plane's log SSE endpoint emits ``LogEvent`` envelopes whose\n``text`` field is a single log line — typically a structlog JSON record like\n``{\"event\": \"...\", \"level\": \"info\", \"timestamp\": \"...\", \"logger\": \"...\"}``,\nbut sometimes a plain string (e.g. application stdout). This module owns the\nparsing of that body and the plain-text renderer used by the\n``deployments logs`` CLI command.\n\nThis module knows nothing about ``LogEvent.pod`` / ``LogEvent.container`` /\n``LogEvent.timestamp`` — those live on the envelope and are formatted by\ncallers. The K8s leading timestamp is already stripped before bodies reach\nthis parser (see ``k8s_client._parse_raw_log_lines``).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass, field\n\n# structlog keys handled inline; everything else goes into ``extras``.\n_KNOWN_KEYS = frozenset({\"event\", \"level\", \"timestamp\", \"logger\", \"request_id\"})\n\n\n@dataclass\nclass ParsedLogBody:\n    \"\"\"Structured form of one structlog body line.\n\n    ``raw`` holds the original input. If the line wasn't structlog JSON,\n    ``event`` carries the raw text and ``structured`` is False.\n    \"\"\"\n\n    raw: str\n    structured: bool\n    timestamp: str = \"\"\n    level: str = \"\"\n    logger: str = \"\"\n    event: str = \"\"\n    request_id: str = \"\"\n    extras: dict[str, object] = field(default_factory=dict)\n\n\ndef parse_log_body(line: str) -> ParsedLogBody:\n    \"\"\"Parse a structlog JSON body into a ``ParsedLogBody``.\n\n    Falls back to a non-structured ``ParsedLogBody`` (with ``event=line``)\n    when the input is not a structlog dict — which is fine, downstream\n    renderers just emit it as-is.\n    \"\"\"\n    raw = line\n    stripped = line.strip()\n    if not stripped.startswith(\"{\"):\n        return ParsedLogBody(raw=raw, structured=False, event=line)\n    try:\n        data = json.loads(stripped)\n    except (json.JSONDecodeError, ValueError):\n        return ParsedLogBody(raw=raw, structured=False, event=line)\n\n    if not isinstance(data, dict) or \"event\" not in data:\n        return ParsedLogBody(raw=raw, structured=False, event=line)\n\n    return ParsedLogBody(\n        raw=raw,\n        structured=True,\n        timestamp=str(data.get(\"timestamp\", \"\")),\n        level=str(data.get(\"level\", \"\")).lower(),\n        logger=str(data.get(\"logger\", \"\")),\n        event=str(data.get(\"event\", \"\")),\n        request_id=str(data.get(\"request_id\", \"\")),\n        extras={k: v for k, v in data.items() if k not in _KNOWN_KEYS},\n    )\n\n\ndef trim_timestamp(ts: str) -> str:\n    \"\"\"Trim ISO timestamp to its time portion.\"\"\"\n    if \"T\" in ts:\n        ts = ts.split(\"T\", 1)[1]\n        for suffix in (\"Z\", \"+00:00\"):\n            ts = ts.removesuffix(suffix)\n    return ts\n\n\ndef render_plain(parsed: ParsedLogBody) -> str:\n    \"\"\"Render ``parsed`` as a plain string (no Rich/ANSI).\n\n    Used by ``llamactl deployments logs`` text mode.\n    \"\"\"\n    if not parsed.structured:\n        return parsed.event or parsed.raw\n\n    parts: list[str] = []\n    ts = trim_timestamp(parsed.timestamp)\n    if ts:\n        parts.append(ts)\n    if parsed.level:\n        parts.append(f\"{parsed.level.upper():8s}\")\n    if parsed.logger:\n        parts.append(parsed.logger)\n    parts.append(parsed.event)\n\n    line = \" \".join(p for p in parts if p)\n    if parsed.request_id:\n        line += f\" req={parsed.request_id}\"\n    if parsed.extras:\n        line += \" \" + \" \".join(f\"{k}={v}\" for k, v in parsed.extras.items())\n    return line\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/options.py",
    "content": "import json\nimport logging\nimport os\nfrom typing import Any, Callable, ParamSpec, TypeVar\n\nimport click\nimport yaml\nfrom llama_agents.cli.param_types import ProjectType\nfrom pydantic import BaseModel\n\nfrom .debug import setup_file_logging\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\n\n\ndef global_options(f: Callable[P, R]) -> Callable[P, R]:\n    \"\"\"Common decorator to add global options to command groups\"\"\"\n\n    return native_tls_option(file_logging(f))\n\n\n_BASE_OUTPUT_CHOICES = (\"text\", \"json\", \"yaml\", \"wide\")\n_BASE_OUTPUT_HELP = (\n    \"Output format. 'json'/'yaml' for machine-readable output; \"\n    \"'wide' for the text table with extra columns.\"\n)\n_SIMPLE_OUTPUT_CHOICES = (\"text\", \"json\", \"yaml\")\n_SIMPLE_OUTPUT_HELP = \"Output format. 'json'/'yaml' for machine-readable output.\"\n\n\ndef _output_option(\n    *extra_choices: str, help_text: str = _BASE_OUTPUT_HELP\n) -> Callable[[Callable[P, R]], Callable[P, R]]:\n    choices = list(_BASE_OUTPUT_CHOICES) + list(extra_choices)\n    return click.option(\n        \"-o\",\n        \"--output\",\n        \"output\",\n        type=click.Choice(choices, case_sensitive=False),\n        default=\"text\",\n        show_default=True,\n        help=help_text,\n    )\n\n\ndef output_option(f: Callable[P, R]) -> Callable[P, R]:\n    \"\"\"Add a `-o/--output` option for read commands.\n\n    Choices: ``text`` (default), ``json``, ``yaml``, ``wide``. ``wide`` is\n    text mode with the less-common columns interleaved into their natural\n    positions (kubectl-style).\n    \"\"\"\n\n    return _output_option()(f)\n\n\ndef simple_output_option(f: Callable[P, R]) -> Callable[P, R]:\n    \"\"\"Add ``-o/--output`` for commands without a wide text variant.\"\"\"\n\n    return click.option(\n        \"-o\",\n        \"--output\",\n        \"output\",\n        type=click.Choice(_SIMPLE_OUTPUT_CHOICES, case_sensitive=False),\n        default=\"text\",\n        show_default=True,\n        help=_SIMPLE_OUTPUT_HELP,\n    )(f)\n\n\ndef output_option_with_template(f: Callable[P, R]) -> Callable[P, R]:\n    \"\"\"Variant of :func:`output_option` that adds the ``template`` choice.\n\n    ``template`` emits the apply-shaped YAML scaffold (annotated with ``##``\n    docs, suitable for ``llamactl deployments apply -f``). It only makes sense\n    on a single-row read (``deployments get <name>``) and is opted-in per\n    command rather than advertised on every ``-o``-bearing command.\n    \"\"\"\n\n    return _output_option(\n        \"template\",\n        help_text=(\n            f\"{_BASE_OUTPUT_HELP[:-1]}; \"\n            \"'template' for an annotated YAML scaffold suitable for `apply`.\"\n        ),\n    )(f)\n\n\ndef project_option(f: Callable[P, R]) -> Callable[P, R]:\n    \"\"\"Add a ``--project`` option to override the active profile's project for a command.\n\n    The value is exposed as the ``project`` keyword argument and should be\n    threaded into ``get_project_client()`` or ``project_client_context()`` via\n    the ``project_id_override`` parameter.\n    \"\"\"\n\n    return click.option(\n        \"--project\",\n        \"project\",\n        type=ProjectType(),\n        default=None,\n        help=\"Project ID to use for this command (overrides active profile).\",\n    )(f)\n\n\ndef render_output(\n    payload: BaseModel | list[BaseModel] | Any,\n    output: str,\n    text_renderer: Callable[[], None] | None = None,\n) -> None:\n    \"\"\"Render a payload according to ``output`` mode.\n\n    - ``text`` / ``wide``: if ``payload`` is a ``BaseModel`` (or list of\n      them) whose class declares ``Column`` annotations, the framework\n      derives the table directly. Otherwise (or if ``text_renderer`` is\n      provided as an explicit override) the supplied text renderer runs.\n      ``wide`` includes ``wide=True`` columns; ``text`` excludes them.\n    - ``json``: emit canonical JSON via ``click.echo`` (no Rich markup).\n    - ``yaml``: emit YAML via ``click.echo`` (the naive Pydantic shape).\n\n    ``payload`` may be a Pydantic model, a list of Pydantic models, or any\n    JSON-serializable value. Structured outputs go through ``click.echo`` so\n    they pipe cleanly even when Rich would otherwise insert markup.\n    \"\"\"\n\n    # Defer the import: the framework lives in ``cli.display`` which has\n    # heavier transitive imports than ``options.py`` itself.\n    from llama_agents.cli.display import render_columns, resolve_columns\n\n    mode = output.lower()\n    if mode == \"template\":\n        # ``-o template`` is the apply-shaped scaffold output and only makes\n        # sense for ``deployments get <name>``. Each command that supports it\n        # short-circuits before calling ``render_output``; reaching here with\n        # ``template`` means the command does not.\n        raise click.ClickException(\n            \"-o template is only supported for `llamactl deployments get <name>`\"\n        )\n    if mode in {\"text\", \"wide\"}:\n        rows: list[BaseModel] | None = None\n        if isinstance(payload, BaseModel):\n            rows = [payload]\n        elif (\n            isinstance(payload, list) and payload and isinstance(payload[0], BaseModel)\n        ):\n            rows = list(payload)\n        if rows is not None and resolve_columns(type(rows[0])):\n            render_columns(rows, wide=(mode == \"wide\"))\n            return\n        if isinstance(payload, list) and not payload:\n            # Empty list in text/wide mode: caller is expected to have\n            # emitted a status line (\"No X found\"). Silently no-op rather\n            # than demanding a text_renderer for the degenerate case.\n            return\n        if text_renderer is None:\n            raise click.ClickException(\"No text renderer available for this payload.\")\n        text_renderer()\n        return\n\n    def _to_json_safe(value: Any) -> Any:\n        # Models with a custom ``to_output_dict`` (e.g. ``DeploymentDisplay``)\n        # own their on-the-wire shape — including which null keys to keep —\n        # so prefer that over a raw ``model_dump``.\n        to_output = getattr(value, \"to_output_dict\", None)\n        if callable(to_output):\n            return to_output()\n        if isinstance(value, BaseModel):\n            return value.model_dump(mode=\"json\")\n        if isinstance(value, list):\n            return [_to_json_safe(item) for item in value]\n        if isinstance(value, dict):\n            return {k: _to_json_safe(v) for k, v in value.items()}\n        return value\n\n    if mode == \"json\":\n        click.echo(json.dumps(_to_json_safe(payload), indent=2))\n        return\n    if mode == \"yaml\":\n        click.echo(yaml.safe_dump(_to_json_safe(payload), sort_keys=False))\n        return\n\n    raise click.ClickException(f\"Unknown output mode: {output}\")\n\n\ndef native_tls_option(f: Callable[P, R]) -> Callable[P, R]:\n    \"\"\"Enable native TLS to trust system configured trust store rather than python bundled trust stores.\n\n    When enabled, we set:\n    - UV_NATIVE_TLS=1 to instruct uv to use the platform trust store\n    - LLAMA_DEPLOY_USE_TRUSTSTORE=1 to use system certificate store for Python httpx clients\n    \"\"\"\n\n    def _enable_native_tls(\n        ctx: click.Context, param: click.Parameter, value: bool\n    ) -> bool:\n        if value:\n            # Don't override if user explicitly set a value\n            os.environ.setdefault(\"UV_NATIVE_TLS\", \"1\")\n            os.environ.setdefault(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"1\")\n        return value\n\n    return click.option(\n        \"--native-tls\",\n        is_flag=True,\n        help=(\n            \"Enable native TLS mode to use system certificate store rather than runtime defaults. Can be set via LLAMACTL_NATIVE_TLS=1\"\n        ),\n        callback=_enable_native_tls,\n        expose_value=False,\n        is_eager=True,\n        envvar=[\"LLAMACTL_NATIVE_TLS\"],\n    )(f)\n\n\ndef file_logging(f: Callable[P, R]) -> Callable[P, R]:\n    def debug_callback(ctx: click.Context, param: click.Parameter, value: str) -> str:\n        if value:\n            setup_file_logging(level=logging._nameToLevel.get(value, logging.INFO))\n        return value\n\n    return click.option(\n        \"--log-level\",\n        type=click.Choice(\n            [\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"], case_sensitive=False\n        ),\n        help=\"Enable debug logging to file\",\n        callback=debug_callback,\n        expose_value=False,\n        is_eager=True,\n        hidden=True,\n    )(f)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/output.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport click\n\n\ndef status(message: Any = \"\", *, nl: bool = True) -> None:\n    \"\"\"Write human-facing CLI status text to stderr.\"\"\"\n    click.echo(str(message), err=True, nl=nl)\n\n\ndef warning(message: Any, *, nl: bool = True) -> None:\n    \"\"\"Write a human-facing CLI warning to stderr.\"\"\"\n    status(f\"warning: {message}\", nl=nl)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/param_types.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nfrom typing import Any\n\nimport click\nfrom click.shell_completion import CompletionItem\nfrom llama_agents.cli.templates import ALL_TEMPLATES\n\n\ndef _safe_fetch(fn: Any, timeout: float = 2.0) -> list[Any]:\n    \"\"\"Run a fetch function in a thread with a timeout. Returns [] on failure.\"\"\"\n    pool = ThreadPoolExecutor(max_workers=1)\n    try:\n        future = pool.submit(fn)\n        return future.result(timeout=timeout)\n    except (Exception, SystemExit):\n        return []\n    finally:\n        pool.shutdown(wait=False)\n\n\ndef _fetch_deployments(\n    project_id_override: str | None = None,\n) -> list[CompletionItem]:\n    from llama_agents.cli.client import get_project_client\n\n    client = get_project_client(project_id_override=project_id_override)\n    deployments = asyncio.run(client.list_deployments())\n    return [CompletionItem(d.id) for d in deployments]\n\n\ndef _fetch_projects() -> list[CompletionItem]:\n    from llama_agents.cli.client import get_control_plane_client\n\n    client = get_control_plane_client()\n    projects = asyncio.run(client.list_projects())\n    return [\n        CompletionItem(\n            p.project_id,\n            help=f\"{p.project_name} ({p.deployment_count} deployments)\",\n        )\n        for p in projects\n    ]\n\n\ndef _fetch_organizations() -> list[CompletionItem]:\n    from llama_agents.cli.client import get_control_plane_client\n\n    client = get_control_plane_client()\n    organizations = asyncio.run(client.list_organizations())\n    return [\n        CompletionItem(\n            o.org_id,\n            help=f\"{o.org_name}{' (default)' if o.is_default else ''}\",\n        )\n        for o in organizations\n    ]\n\n\ndef _fetch_deployment_history(\n    deployment_id: str, project_id_override: str | None = None\n) -> list[CompletionItem]:\n    from llama_agents.cli.client import get_project_client\n\n    client = get_project_client(project_id_override=project_id_override)\n\n    async def _fetch() -> Any:\n        return await client.get_deployment_history(deployment_id)\n\n    history = asyncio.run(_fetch())\n    return [\n        CompletionItem(item.git_sha, help=item.released_at.isoformat())\n        for item in history.history\n    ]\n\n\ndef _filter(items: list[CompletionItem], incomplete: str) -> list[CompletionItem]:\n    lower = incomplete.lower()\n    return [item for item in items if item.value.lower().startswith(lower)]\n\n\nclass DeploymentType(click.ParamType):\n    name = \"deployment\"\n\n    def shell_complete(\n        self, ctx: click.Context, param: click.Parameter, incomplete: str\n    ) -> list[CompletionItem]:\n        project_id_override = ctx.params.get(\"project\")\n        return _filter(\n            _safe_fetch(lambda: _fetch_deployments(project_id_override)),\n            incomplete,\n        )\n\n\nclass ProfileType(click.ParamType):\n    name = \"profile\"\n\n    def shell_complete(\n        self, ctx: click.Context, param: click.Parameter, incomplete: str\n    ) -> list[CompletionItem]:\n        def _fetch() -> list[CompletionItem]:\n            from llama_agents.cli.config.env_service import service\n\n            auth_svc = service.current_auth_service()\n            profiles = auth_svc.list_profiles()\n            return [CompletionItem(p.name, help=p.api_url) for p in profiles]\n\n        return _filter(_safe_fetch(_fetch), incomplete)\n\n\nclass ProjectType(click.ParamType):\n    name = \"project\"\n\n    def shell_complete(\n        self, ctx: click.Context, param: click.Parameter, incomplete: str\n    ) -> list[CompletionItem]:\n        return _filter(_safe_fetch(_fetch_projects), incomplete)\n\n\nclass OrgType(click.ParamType):\n    name = \"org\"\n\n    def shell_complete(\n        self, ctx: click.Context, param: click.Parameter, incomplete: str\n    ) -> list[CompletionItem]:\n        return _filter(_safe_fetch(_fetch_organizations), incomplete)\n\n\nclass EnvironmentType(click.ParamType):\n    name = \"environment\"\n\n    def shell_complete(\n        self, ctx: click.Context, param: click.Parameter, incomplete: str\n    ) -> list[CompletionItem]:\n        def _fetch() -> list[CompletionItem]:\n            from llama_agents.cli.config.env_service import service\n\n            envs = service.list_environments()\n            current = service.get_current_environment()\n            return [\n                CompletionItem(\n                    e.api_url,\n                    help=\"(current)\" if e.api_url == current.api_url else \"\",\n                )\n                for e in envs\n            ]\n\n        return _filter(_safe_fetch(_fetch), incomplete)\n\n\nclass TemplateType(click.ParamType):\n    name = \"template\"\n\n    def shell_complete(\n        self, ctx: click.Context, param: click.Parameter, incomplete: str\n    ) -> list[CompletionItem]:\n        return _filter(\n            [CompletionItem(t.id, help=t.description) for t in ALL_TEMPLATES],\n            incomplete,\n        )\n\n\nclass GitShaType(click.ParamType):\n    name = \"git_sha\"\n\n    def shell_complete(\n        self, ctx: click.Context, param: click.Parameter, incomplete: str\n    ) -> list[CompletionItem]:\n        deployment_id = ctx.params.get(\"deployment_id\")\n        if not deployment_id:\n            return []\n        project_id_override = ctx.params.get(\"project\")\n        return _filter(\n            _safe_fetch(\n                lambda: _fetch_deployment_history(deployment_id, project_id_override)\n            ),\n            incomplete,\n        )\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/paths.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\n\ndef resolve_llamactl_config_dir() -> Path:\n    \"\"\"Resolve the config directory from the override/env/platform policy.\"\"\"\n    override = os.environ.get(\"LLAMACTL_CONFIG_DIR\")\n    if override:\n        return Path(override).expanduser()\n\n    if os.name == \"nt\":\n        return (Path(os.environ.get(\"APPDATA\", \"~\")) / \"llamactl\").expanduser()\n\n    xdg_config_home = os.environ.get(\"XDG_CONFIG_HOME\")\n    if xdg_config_home:\n        return (Path(xdg_config_home).expanduser() / \"llamactl\").expanduser()\n    return (Path.home() / \".config\" / \"llamactl\").expanduser()\n\n\ndef bash_completion_dir(home: Path | None = None) -> Path:\n    \"\"\"Return the preferred per-user bash completion directory.\"\"\"\n    resolved_home = home or Path.home()\n    preferred = resolved_home / \".local\" / \"share\" / \"bash-completion\" / \"completions\"\n    if preferred.exists():\n        return preferred\n    return resolved_home / \".bash_completion.d\"\n\n\ndef bash_rc_path(home: Path | None = None) -> Path:\n    return (home or Path.home()) / \".bashrc\"\n\n\ndef zsh_completion_dir(home: Path | None = None) -> Path:\n    return (home or Path.home()) / \".zfunc\"\n\n\ndef zsh_rc_path(home: Path | None = None) -> Path:\n    return (home or Path.home()) / \".zshrc\"\n\n\ndef fish_completion_dir(home: Path | None = None) -> Path:\n    return (home or Path.home()) / \".config\" / \"fish\" / \"completions\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/pkg/__init__.py",
    "content": "from .defaults import DEFAULT_DOCKER_IGNORE\nfrom .options import pkg_container_options\nfrom .utils import build_dockerfile_content, infer_python_version\n\n__all__ = [\n    \"infer_python_version\",\n    \"build_dockerfile_content\",\n    \"DEFAULT_DOCKER_IGNORE\",\n    \"pkg_container_options\",\n]\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/pkg/defaults.py",
    "content": "DEFAULT_DOCKER_IGNORE = \"\"\"\n.venv/\n.git/\n__pycache__/\n*.py[oc]\nbuild/\ndist/\nwheels/\n*.egg-info\n.env\n\"\"\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/pkg/options.py",
    "content": "from pathlib import Path\nfrom typing import Callable, ParamSpec, TypeVar\n\nimport click\nfrom llama_agents.core.config import DEFAULT_DEPLOYMENT_FILE_PATH\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\n\n# hack around for mypy not letting you set path_type=Path on click.Path\n_ClickPath = getattr(click, \"Path\")\n\n\ndef _deployment_file_option(f: Callable[P, R]) -> Callable[P, R]:\n    return click.argument(\n        \"deployment_file\",\n        required=False,\n        default=DEFAULT_DEPLOYMENT_FILE_PATH,\n        type=_ClickPath(dir_okay=True, resolve_path=True, path_type=Path),\n    )(f)\n\n\ndef _python_version_option(f: Callable[P, R]) -> Callable[P, R]:\n    return click.option(\n        \"--python-version\",\n        help=\"Python version for the base image. Default is inferred from the uv project configuration (.python-version or pyproject.toml). If no version can be inferred, python 3.12 is used.\",\n        required=False,\n        default=None,\n    )(f)\n\n\ndef _port_option(f: Callable[P, R]) -> Callable[P, R]:\n    return click.option(\n        \"--port\",\n        help=\"The port to run the API server on. Defaults to 4501.\",\n        required=False,\n        default=4501,\n        type=int,\n    )(f)\n\n\ndef _dockerignore_path_option(f: Callable[P, R]) -> Callable[P, R]:\n    return click.option(\n        \"--dockerignore-path\",\n        help=\"Path for the output .dockerignore file. Defaults to .dockerignore\",\n        required=False,\n        default=\".dockerignore\",\n    )(f)\n\n\ndef _output_file_option(f: Callable[P, R]) -> Callable[P, R]:\n    return click.option(\n        \"--output-file\",\n        help=\"Path for the output file to build the image. Defaults to Dockerfile\",\n        required=False,\n        default=\"Dockerfile\",\n    )(f)\n\n\ndef _overwrite_option(f: Callable[P, R]) -> Callable[P, R]:\n    return click.option(\n        \"--overwrite\",\n        help=\"Overwrite output files\",\n        is_flag=True,\n    )(f)\n\n\ndef _exclude_option(f: Callable[P, R]) -> Callable[P, R]:\n    return click.option(\n        \"--exclude\",\n        help=\"Path to exclude from the build (will be appended to .dockerignore). Can be used multiple times.\",\n        multiple=True,\n        required=False,\n        default=None,\n    )(f)\n\n\ndef pkg_container_options(f: Callable[P, R]) -> Callable[P, R]:\n    return _deployment_file_option(\n        _python_version_option(\n            _port_option(\n                _dockerignore_path_option(\n                    _overwrite_option(_exclude_option(_output_file_option(f)))\n                )\n            )\n        )\n    )\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/pkg/utils.py",
    "content": "from pathlib import Path\n\nfrom llama_agents.core._compat import load_toml_file\n\n\ndef _get_min_py_version(requires_python: str) -> str:\n    min_v = requires_python.split(\",\")[0].strip()\n    return (\n        min_v.replace(\"=\", \"\")\n        .replace(\">\", \"\")\n        .replace(\"<\", \"\")\n        .replace(\"~\", \"\")\n        .strip()\n    )\n\n\ndef infer_python_version(config_dir: Path) -> str:\n    if (config_dir / \".python-version\").exists():\n        with open(config_dir / \".python-version\", \"r\") as f:\n            content = f.read()\n            if content.strip():\n                py_version = content.strip()\n                return py_version\n    with open(config_dir / \"pyproject.toml\", \"rb\") as f:\n        data = load_toml_file(f)\n    return _get_min_py_version(data.get(\"project\", {}).get(\"requires-python\", \"3.12\"))\n\n\ndef build_dockerfile_content(\n    python_version: str | None = None, port: int = 4501\n) -> str:\n    return f\"\"\"\nFROM python:{python_version}-slim-trixie\n\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/\n\nWORKDIR /app\n\nCOPY . /app/\n\nENV PATH=/root/.local/bin:$PATH\n\nRUN uv sync --locked\nRUN uv tool install llamactl\n\nEXPOSE {port}\n\nENTRYPOINT [ \"uv\", \"run\", \"llamactl\", \"serve\", \"--host\", \"0.0.0.0\", \"--port\", \"{port}\" ]\n\"\"\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/py.typed",
    "content": ""
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/render.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Plain-whitespace table renderer for llamactl.\n\nNo colors, no truncation, no Rich. Long values let the terminal wrap. The\nhelper exists so every read command emits the same simple, grep-friendly\nshape.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nimport click\n\nGH_PREFIX = \"https://github.com/\"\n\n\ndef gh_short(repo_url: str) -> str:\n    \"\"\"Return ``gh:org/repo`` for github URLs, otherwise the input unchanged.\n\n    Used in table cells only; YAML/JSON keep the full URL.\n    \"\"\"\n    if repo_url.startswith(GH_PREFIX):\n        return \"gh:\" + repo_url.removeprefix(GH_PREFIX)\n    return repo_url\n\n\ndef short_sha(sha: str) -> str:\n    \"\"\"Return the first 7 characters of ``sha``, or the input unchanged.\n\n    Used in table cells; JSON/YAML keep the full SHA. Safe on shorter input.\n    \"\"\"\n    return sha[:7] if len(sha) > 7 else sha\n\n\ndef star_marker(active: bool) -> str:\n    \"\"\"Render a boolean as ``\"*\"`` (true) or ``\"\"`` (false).\n\n    Used by the ``ACTIVE`` column on profile/env/org tables.\n    \"\"\"\n    return \"*\" if active else \"\"\n\n\ndef format_iso_z(dt: datetime) -> str:\n    \"\"\"Format ``dt`` as a UTC ISO 8601 string with a ``Z`` suffix.\n\n    Tz-aware datetimes are converted to UTC. Naive datetimes are assumed to\n    already be UTC (the server emits UTC; this is a safety fallback rather\n    than an invitation to pass local time). Fractional seconds are dropped:\n    current data doesn't carry them and the table cell stays narrow.\n\n    The output (``YYYY-MM-DDTHH:MM:SSZ``) matches what Pydantic emits in\n    JSON mode, so text and JSON encodings of the same instant agree.\n    \"\"\"\n    if dt.tzinfo is not None:\n        dt = dt.astimezone(timezone.utc)\n    return dt.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\n\ndef render_table(\n    rows: list[dict[str, str]],\n    columns: list[tuple[str, str]],\n) -> None:\n    \"\"\"Render a plain whitespace table to stdout.\n\n    Args:\n        rows: Each row is a ``{column_key: cell_value}`` mapping. Cells must\n            already be strings (callers handle ``None`` → ``\"-\"``).\n        columns: Ordered ``(header, key)`` pairs. Headers are rendered\n            verbatim — callers pass uppercase headers if they want them.\n\n    Column widths are sized to the widest cell (header included) plus a\n    two-space gutter on the right except the last column. No truncation.\n    \"\"\"\n    widths: list[int] = [\n        max(len(header), *(len(row.get(key, \"\")) for row in rows))\n        if rows\n        else len(header)\n        for header, key in columns\n    ]\n\n    def _format_line(values: list[str]) -> str:\n        parts: list[str] = []\n        last = len(values) - 1\n        for i, value in enumerate(values):\n            if i == last:\n                parts.append(value)\n            else:\n                parts.append(value.ljust(widths[i] + 2))\n        return \"\".join(parts).rstrip()\n\n    headers = [header for header, _ in columns]\n    click.echo(_format_line(headers))\n    for row in rows:\n        click.echo(_format_line([row.get(key, \"\") for _, key in columns]))\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/scaffold/.codex/config.toml",
    "content": "[mcp_servers.llama-index-docs]\nurl = \"https://developers.llamaindex.ai/mcp\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/scaffold/.cursor/mcp.json",
    "content": "{\n  \"mcpServers\": {\n    \"llama-index-docs\": {\n      \"url\": \"https://developers.llamaindex.ai/mcp\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/scaffold/.mcp.json",
    "content": "{\n  \"mcpServers\": {\n    \"llama-index-docs\": {\n      \"type\": \"http\",\n      \"url\": \"https://developers.llamaindex.ai/mcp\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/scaffold/AGENTS.md",
    "content": "# LlamaIndex Agent Instructions\n\n## Using LlamaIndex APIs\n\nThis project uses LlamaIndex. Use the MCP server for documentation lookup\nwhen developing agent apps or working with LlamaIndex APIs:\n\n**MCP endpoint:** `https://developers.llamaindex.ai/mcp`\n\nThe server provides these tools:\n- **search_docs** — lexical search over LlamaIndex documentation\n- **grep_docs** — exact pattern matching with regex\n- **read_doc** — retrieve full page contents by path\n\nUse these tools to look up API usage, workflow patterns, and integration\nguides before writing or modifying code.\n\n## Fallback\n\nIf MCP is unavailable, fetch documentation from:\n`https://developers.llamaindex.ai/llms.txt`\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/styles.py",
    "content": "# A place to centralize design tokens to simplify tweaking the appearance of the CLI\n# See https://rich.readthedocs.io/en/stable/appendix/colors.html\n\n\nHEADER_COLOR = \"cornflower_blue\"\nHEADER_COLOR_HEX = \"#5f87ff\"\nPRIMARY_COL = \"default\"\nMUTED_COL = \"grey46\"\nWARNING = \"yellow\"\nACTIVE_INDICATOR = \"magenta\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/templates/agentcore_entrypoint.py.template",
    "content": "# SPDX-License-Identifier: MIT\n\"\"\"\nAgentCore entrypoint for LlamaIndex Workflows.\nAuto-generated by llamactl — wraps workflows for AWS Bedrock AgentCore Runtime.\n\nUses BedrockAgentCoreApp with @app.entrypoint decorator for native AgentCore integration.\n\"\"\"\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport sys\n\n# Add paths for imports (handles both deployed and local dev scenarios)\n_current_dir = os.path.dirname(os.path.abspath(__file__))\n_parent_dir = os.path.dirname(_current_dir)\nsys.path.insert(0, _current_dir)\nsys.path.insert(0, os.path.join(_current_dir, \"src\"))\n# For local dev when running from .agentcore/ folder\nsys.path.insert(0, _parent_dir)\nsys.path.insert(0, os.path.join(_parent_dir, \"src\"))\n\n# Try loading .env (optional - not needed in production where env vars are set via runtime config)\ntry:\n    from dotenv import load_dotenv\n    load_dotenv(os.path.join(_current_dir, \".env\"), override=True)\n    load_dotenv(os.path.join(_parent_dir, \".env\"), override=True)\nexcept ImportError:\n    pass  # dotenv not available, env vars should be set via AgentCore runtime config\n\nfrom bedrock_agentcore import BedrockAgentCoreApp  # noqa: E402\n\n{{workflow_imports}}\n\nlogger = logging.getLogger(__name__)\napp = BedrockAgentCoreApp()\n\nWORKFLOWS = {\n{{workflow_entries}}\n}\n\n_metadata_cache = None\n\n\nasync def _get_metadata():\n    \"\"\"Get cached metadata (avoids re-running expensive metadata workflow).\"\"\"\n    global _metadata_cache\n    if _metadata_cache is not None:\n        return _metadata_cache\n{{metadata_workflow_call}}\n    return _metadata_cache\n\n\ndef _serialize(result):\n    \"\"\"Serialize workflow result to JSON-compatible format.\"\"\"\n    if hasattr(result, \"model_dump\"):\n        return result.model_dump()\n    if isinstance(result, (str, int, float, bool, dict, list, type(None))):\n        return result\n    return str(result)\n\n\nasync def _run_workflow(workflow_name: str, event_data: dict):\n    \"\"\"Run a workflow by name with the given event data.\"\"\"\n    if workflow_name not in WORKFLOWS:\n        return None, {\n            \"error\": f\"Unknown workflow '{workflow_name}'\",\n            \"available\": list(WORKFLOWS.keys()),\n        }\n\n    # Special handling for metadata workflow (cached)\n    if workflow_name == \"metadata\":\n        metadata = await _get_metadata()\n        return {\"workflow\": \"metadata\", \"status\": \"completed\", \"result\": metadata}, None\n\n    entry = WORKFLOWS[workflow_name]\n    wf = entry[\"workflow\"]\n    start_cls = entry[\"start_event_class\"]\n\n    try:\n        start_event = start_cls(**event_data)\n    except Exception as e:\n        return None, {\"error\": f\"Invalid start_event: {e}\"}\n\n    try:\n        result = await wf.run(start_event=start_event)\n    except Exception as e:\n        logger.error(\"Workflow '%s' failed: %s\", workflow_name, e, exc_info=True)\n        return None, {\"error\": f\"Workflow failed: {e}\"}\n\n    return {\n        \"workflow\": workflow_name,\n        \"status\": \"completed\",\n        \"result\": _serialize(result),\n    }, None\n\n\ndef _parse_payload(payload: dict) -> tuple[str, dict]:\n    \"\"\"Parse incoming payload to determine workflow and event data.\n\n    Supports:\n        - Explicit: {\"workflow\": \"process-file\", \"start_event\": {\"file_id\": \"123\"}}\n        - Shorthand: {\"file_id\": \"123\"} -> routes to process-file workflow\n        - Default: {} -> routes to metadata workflow\n    \"\"\"\n    workflow_name = payload.get(\"workflow\")\n    event_data = payload.get(\"start_event\", {})\n\n    if not workflow_name:\n        # Shorthand detection based on payload keys\n{{payload_routing}}\n    return workflow_name, event_data\n\n\n@app.entrypoint\nasync def invoke(payload: dict, context):\n    \"\"\"Main AgentCore entrypoint — routes to appropriate workflow.\"\"\"\n    workflow_name, event_data = _parse_payload(payload)\n    result, error = await _run_workflow(workflow_name, event_data)\n    if error:\n        return error\n    result[\"session_id\"] = context.session_id\n    return result\n\n\nif __name__ == \"__main__\":\n    app.run()\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/templates.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass GithubTemplateRepo:\n    url: str\n\n\n@dataclass\nclass TemplateOption:\n    id: str\n    name: str\n    description: str\n    source: GithubTemplateRepo\n    llama_cloud: bool\n\n\nUI_TEMPLATES = [\n    TemplateOption(\n        id=\"basic-ui\",\n        name=\"Basic UI\",\n        description=\"Starter workflow with React Vite UI\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-basic-ui\"\n        ),\n        llama_cloud=False,\n    ),\n    TemplateOption(\n        id=\"showcase\",\n        name=\"Showcase\",\n        description=\"Workflow and UI pattern examples for LlamaAgents apps\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-showcase\"\n        ),\n        llama_cloud=False,\n    ),\n    TemplateOption(\n        id=\"document-qa\",\n        name=\"Document Question & Answer\",\n        description=\"Document upload and Q&A with React UI\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-document-qa\"\n        ),\n        llama_cloud=True,\n    ),\n    TemplateOption(\n        id=\"extraction-review\",\n        name=\"Extraction Agent with Review UI\",\n        description=\"Schema-based document extraction with review UI (LlamaExtract)\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-data-extraction\"\n        ),\n        llama_cloud=True,\n    ),\n    TemplateOption(\n        id=\"classify-extract-sec\",\n        name=\"SEC Insights\",\n        description=\"SEC filing classification and key info extraction\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-classify-extract-sec\"\n        ),\n        llama_cloud=True,\n    ),\n    TemplateOption(\n        id=\"extract-reconcile-invoice\",\n        name=\"Invoice Extraction & Reconciliation\",\n        description=\"Invoice extraction and reconciliation against contracts\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-extract-reconcile-invoice\"\n        ),\n        llama_cloud=True,\n    ),\n]\n\nHEADLESS_TEMPLATES = [\n    TemplateOption(\n        id=\"basic\",\n        name=\"Basic Workflow\",\n        description=\"Starter workflow usage patterns (API only, no UI)\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-basic\"\n        ),\n        llama_cloud=False,\n    ),\n    TemplateOption(\n        id=\"document_parsing\",\n        name=\"Document Parser\",\n        description=\"Parse unstructured documents to text via LlamaParse\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-document-parsing\"\n        ),\n        llama_cloud=True,\n    ),\n    TemplateOption(\n        id=\"human_in_the_loop\",\n        name=\"Human in the Loop\",\n        description=\"Human-in-the-loop workflow pattern\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-human-in-the-loop\"\n        ),\n        llama_cloud=False,\n    ),\n    TemplateOption(\n        id=\"invoice_extraction\",\n        name=\"Invoice Extraction\",\n        description=\"Extract invoice details via LlamaExtract\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-invoice-extraction\"\n        ),\n        llama_cloud=True,\n    ),\n    TemplateOption(\n        id=\"rag\",\n        name=\"RAG\",\n        description=\"Embed, index, and query documents (RAG pipeline)\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-rag\"\n        ),\n        llama_cloud=False,\n    ),\n    TemplateOption(\n        id=\"web_scraping\",\n        name=\"Web Scraping\",\n        description=\"Scrape and summarize URLs via Gemini API\",\n        source=GithubTemplateRepo(\n            url=\"https://github.com/run-llama/template-workflow-web-scraping\"\n        ),\n        llama_cloud=False,\n    ),\n]\n\nALL_TEMPLATES = UI_TEMPLATES + HEADLESS_TEMPLATES\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/utils/capabilities.py",
    "content": "\"\"\"Helpers for probing server capabilities.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nfrom llama_agents.core.schema.public import Capabilities\n\nlogger = logging.getLogger(__name__)\n\n\ndef _probe_capability(capability: str) -> bool | None:\n    \"\"\"Probe whether the current environment's server advertises a capability.\n\n    Returns True/False if the probe succeeds, or None if it fails (fail open).\n    \"\"\"\n    from llama_agents.cli.config.env_service import service\n\n    try:\n        env = service.get_current_environment()\n        if not env.capabilities:\n            env = service.auto_update_env(env)\n        return capability in env.capabilities\n    except Exception:\n        logger.debug(\"Failed to probe server capabilities\", exc_info=True)\n        return None\n\n\ndef probe_code_push_support() -> bool | None:\n    \"\"\"Probe the current environment's server for code_push capability.\n\n    Returns True/False if the probe succeeds, or None if it fails (fail open).\n    \"\"\"\n    return _probe_capability(Capabilities.CODE_PUSH)\n\n\ndef probe_organizations_support() -> bool | None:\n    \"\"\"Probe the current environment's server for organizations capability.\n\n    Returns True/False if the probe succeeds, or None if it fails (fail open).\n    \"\"\"\n    return _probe_capability(Capabilities.ORGANIZATIONS)\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/utils/env_inject.py",
    "content": "from __future__ import annotations\n\nfrom typing import Dict\n\nfrom llama_agents.cli.config.schema import Auth\n\n\ndef env_vars_from_profile(profile: Auth) -> Dict[str, str]:\n    \"\"\"Return env var values derived strictly from the given profile.\n\n    Produces the three keys expected by CLI commands:\n    - LLAMA_CLOUD_API_KEY\n    - LLAMA_CLOUD_BASE_URL\n    - LLAMA_AGENTS_PROJECT_ID\n    \"\"\"\n    values: Dict[str, str] = {}\n    if profile.api_key:\n        values[\"LLAMA_CLOUD_API_KEY\"] = profile.api_key\n    if profile.api_url:\n        values[\"LLAMA_CLOUD_BASE_URL\"] = profile.api_url\n    if profile.project_id:\n        values[\"LLAMA_AGENTS_PROJECT_ID\"] = profile.project_id\n    return values\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/utils/git_push.py",
    "content": "\"\"\"Git push utilities for deployment code pushing.\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\n\nfrom llama_agents.core.git.git_util import FULL_SHA_RE\n\n\ndef _git_remote_name(deployment_id: str) -> str:\n    return f\"llamaagents-{deployment_id}\"\n\n\ndef has_deployment_git_remote(deployment_id: str) -> bool:\n    \"\"\"Return whether the standard deployment git remote exists.\"\"\"\n    result = subprocess.run(\n        [\"git\", \"remote\", \"get-url\", _git_remote_name(deployment_id)],\n        capture_output=True,\n    )\n    return result.returncode == 0\n\n\ndef get_deployment_git_url(base_url: str, deployment_id: str) -> str:\n    \"\"\"Build the git endpoint URL for a deployment.\"\"\"\n    api_url = base_url.rstrip(\"/\")\n    return f\"{api_url}/api/v1beta1/deployments/{deployment_id}/git\"\n\n\ndef get_api_key() -> str | None:\n    \"\"\"Get the API key from the current profile.\n\n    Returns None when the backend does not require auth and no key is configured.\n    Raises RuntimeError when auth is required but no valid profile/key exists.\n    \"\"\"\n    from llama_agents.cli.config.env_service import service\n\n    auth_svc = service.current_auth_service()\n    profile = auth_svc.get_current_profile()\n    if profile is not None and profile.api_key is not None:\n        return profile.api_key\n    if auth_svc.env.requires_auth:\n        raise RuntimeError(\"Not authenticated. Run `llamactl auth login` first.\")\n    return None\n\n\ndef _set_extra_headers(git_url: str, api_key: str | None, project_id: str) -> None:\n    \"\"\"Configure git http.extraHeader entries for auth and project-id.\n\n    Clears existing headers first to avoid duplicates on repeated calls.\n    \"\"\"\n    config_key = f\"http.{git_url}.extraHeader\"\n    subprocess.run(\n        [\"git\", \"config\", \"--local\", \"--unset-all\", config_key],\n        capture_output=True,\n    )\n    headers = [f\"project-id: {project_id}\"]\n    if api_key:\n        headers.append(f\"Authorization: Bearer {api_key}\")\n    for header in headers:\n        subprocess.run(\n            [\"git\", \"config\", \"--local\", \"--add\", config_key, header],\n            check=True,\n            capture_output=True,\n        )\n\n\ndef configure_git_remote(\n    git_url: str, api_key: str | None, project_id: str, deployment_id: str\n) -> str:\n    \"\"\"Configure a deployment-scoped git remote and extraHeaders.\n\n    Returns the remote name (e.g. 'llamaagents-my-deploy').\n    \"\"\"\n    _set_extra_headers(git_url, api_key, project_id)\n\n    remote_name = _git_remote_name(deployment_id)\n    result = subprocess.run(\n        [\"git\", \"remote\", \"get-url\", remote_name],\n        capture_output=True,\n        text=True,\n    )\n    if result.returncode == 0:\n        subprocess.run(\n            [\"git\", \"remote\", \"set-url\", remote_name, git_url],\n            check=True,\n            capture_output=True,\n        )\n    else:\n        subprocess.run(\n            [\"git\", \"remote\", \"add\", remote_name, git_url],\n            check=True,\n            capture_output=True,\n        )\n    return remote_name\n\n\ndef push_to_remote(\n    remote_name: str,\n    local_ref: str = \"HEAD\",\n    target_ref: str = \"refs/heads/main\",\n) -> subprocess.CompletedProcess[bytes]:\n    \"\"\"Push to an already-configured remote.\n\n    Call ``configure_git_remote`` first to set up auth headers and the remote.\n    Returns the CompletedProcess from git push. Caller should check returncode.\n    \"\"\"\n    return subprocess.run(\n        [\"git\", \"push\", remote_name, f\"{local_ref}:{target_ref}\"],\n        capture_output=True,\n    )\n\n\ndef git_ref_exists(ref_name: str) -> bool:\n    result = subprocess.run(\n        [\"git\", \"show-ref\", \"--verify\", \"--quiet\", ref_name],\n        capture_output=True,\n    )\n    return result.returncode == 0\n\n\ndef internal_push_refspec(git_ref: str | None) -> tuple[str, str]:\n    \"\"\"Compute (local_ref, target_ref) for pushing to an internal code repo.\n\n    Handles branches, tags, full refs, and pinned SHAs.\n    \"\"\"\n    if git_ref is None:\n        return \"main\", \"refs/heads/main\"\n\n    if FULL_SHA_RE.fullmatch(git_ref):\n        return git_ref, f\"refs/llamactl/pins/{git_ref}\"\n\n    if git_ref.startswith(\"refs/\"):\n        return git_ref, git_ref\n\n    branch_ref = f\"refs/heads/{git_ref}\"\n    if git_ref_exists(branch_ref):\n        return branch_ref, branch_ref\n\n    tag_ref = f\"refs/tags/{git_ref}\"\n    if git_ref_exists(tag_ref):\n        return tag_ref, tag_ref\n\n    return git_ref, branch_ref\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/utils/redact.py",
    "content": "from __future__ import annotations\n\n\ndef redact_api_key(\n    token: str | None,\n    visible_prefix: int = 6,\n    visible_suffix_long: int = 4,\n    visible_suffix_short: int = 2,\n    long_threshold: int = 10,\n    mask: str = \"****\",\n) -> str:\n    \"\"\"Redact an API key for display.\n\n    Shows a prefix and suffix with a mask in the middle. If token is short,\n    reduces the suffix length to keep at least two trailing characters visible.\n\n    This mirrors the masking behavior used for profile names.\n    \"\"\"\n    if not token:\n        return \"-\"\n    cleaned = token.replace(\" \", \"\")\n    if len(cleaned) <= 0:\n        return \"-\"\n    first = cleaned[:visible_prefix]\n    last_len = (\n        visible_suffix_long if len(cleaned) > long_threshold else visible_suffix_short\n    )\n    last = cleaned[-last_len:] if last_len > 0 else \"\"\n    return f\"{first}{mask}{last}\"\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/utils/retry.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Awaitable, Callable\nfrom typing import TypeVar\n\nimport httpx\nfrom tenacity import (\n    AsyncRetrying,\n    retry_if_exception,\n    stop_after_attempt,\n    wait_exponential,\n)\n\n_T = TypeVar(\"_T\")\n\n\ndef _is_transient_httpx_error(exc: BaseException) -> bool:\n    \"\"\"Return True for network-level httpx errors that are safe to retry\n    for idempotent operations.\n\n    - Retries on httpx.RequestError (connection errors, timeouts, etc.)\n    - Never retries httpx.HTTPStatusError (4xx/5xx responses from the server)\n    \"\"\"\n    return isinstance(exc, httpx.RequestError) and not isinstance(\n        exc, httpx.HTTPStatusError\n    )\n\n\ndef _is_connect_phase_error(exc: BaseException) -> bool:\n    \"\"\"Return True for httpx errors that are guaranteed to have occurred\n    before the request reached the server — safe to retry even for\n    non-idempotent operations.\n\n    Excludes read/write errors and RemoteProtocolError, any of which may\n    have happened after the server accepted (and possibly processed) the\n    request.\n    \"\"\"\n    return isinstance(\n        exc,\n        (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout),\n    )\n\n\nasync def run_with_network_retries(\n    operation: Callable[[], Awaitable[_T]],\n    *,\n    max_attempts: int = 3,\n    idempotent: bool = True,\n) -> _T:\n    \"\"\"Run an async operation with standard network retry semantics.\n\n    Retries transient httpx network errors with exponential backoff, but does not retry\n    HTTP status errors. After the final attempt, the last exception is re-raised.\n\n    When ``idempotent`` is False, retries are restricted to connect-phase\n    errors (ConnectError, ConnectTimeout, PoolTimeout) — i.e. failures where\n    the request is guaranteed to have never reached the server. This lets\n    callers stay resilient to initial-connectivity blips without risking\n    duplicate server-side effects from a read-timeout retry.\n    \"\"\"\n    classifier = _is_transient_httpx_error if idempotent else _is_connect_phase_error\n    async for attempt in AsyncRetrying(\n        retry=retry_if_exception(classifier),\n        stop=stop_after_attempt(max_attempts),\n        wait=wait_exponential(multiplier=1, min=1),\n        reraise=True,\n    ):\n        with attempt:\n            return await operation()\n\n    # This line should be unreachable because AsyncRetrying either yields an\n    # attempt or re-raises the last exception.\n    raise RuntimeError(\"run_with_network_retries reached an unexpected state\")\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/utils/version.py",
    "content": "\"\"\"Version utilities shared across CLI components.\"\"\"\n\nfrom importlib import metadata as importlib_metadata\n\n\ndef get_installed_appserver_version() -> str | None:\n    \"\"\"Return the installed version of `llama-agents-appserver`, if available.\"\"\"\n    try:\n        return importlib_metadata.version(\"llama-agents-appserver\")\n    except Exception:\n        return None\n"
  },
  {
    "path": "packages/llamactl/src/llama_agents/cli/yaml_template.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Render a :class:`DeploymentDisplay` as commented apply-shaped YAML.\n\nOne ``render()`` walks ``DeploymentDisplay``'s top-level identity fields\n(``name``, optionally ``generate_name``) and then ``DeploymentSpec.model_fields``\nin declaration order, appending lines to a list. Each spec field renders in\none of three states:\n\n* **set** — ``<key>: <scalar>`` (uncommented), with the field's\n  :class:`~llama_agents.cli.display.Doc` text as ``## …`` lines above.\n  ``secrets`` is the only nested shape; its child keys render at indent 4.\n* **required-but-unset** — ``<key>: ~`` with a ``## Required …`` marker so a\n  ``grep`` or eyeball pass finds the gaps that block ``apply``.\n* **unset** — ``# <key>: <example>`` (commented out one-liner) with the doc\n  above. The schema-fixed ``_EXAMPLES`` table supplies the example value.\n\nTop-level ``name`` follows the set / unset split (no required path — the\nserver slugifies an id when ``name`` is omitted, so a missing top-level\n``name`` is never an apply blocker). ``generate_name`` is special-cased: the\ncaller opts in via ``scaffold_generate_name`` (only the offline ``deployments\ntemplate`` flow does), and when emitted it always renders commented-out under\nthe identity-tier comment block.\n\nA small ``field_alternatives`` mapping lets the caller surface a\ncommented-out alternative under a *set* field (used to suggest the detected\ngit remote under an empty ``repo_url``). Scalar quoting delegates to PyYAML;\nSingle-character strings are force-quoted so values like ``\".\"`` read\nunambiguously rather than hitting YAML plain-scalar edge cases.\n\nMask sentinels (``SECRET_MASK``) inside ``secrets`` and on\n``personal_access_token`` are preserved in rendered output so the user can see\nwhich secrets exist.  Stripping happens on the apply/parse side\n(:meth:`~llama_agents.cli.display.DeploymentDisplay.without_mask_sentinels`)\nso a ``get -o template | apply`` round-trip can't push a literal ``********``\nback as the value.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable, Mapping, Sequence\nfrom typing import Any\n\nimport yaml\nfrom llama_agents.cli.display import (\n    DeploymentDisplay,\n    DeploymentSpec,\n    Doc,\n    TrailingDoc,\n    strip_masks,\n)\nfrom pydantic.fields import FieldInfo\n\n_INDENT = \"  \"\n_MARKER = \"## \"\n_TRAILING_COLUMN = 45\n_SCAFFOLD_IDENTITY_DOCS = (\n    \"Set 'name' for a stable id, or 'generate_name' to let the server pick a\",\n    \"unique id from this slug. Create needs one of these fields.\",\n)\n\n# Per-field example values shown when a spec field is unset (rendered commented).\n_EXAMPLES: dict[str, Any] = {\n    # The push-mode sentinel (``\"\"``) is documented separately via\n    # :data:`~llama_agents.cli.display.PUSH_MODE_REPO_URL`; the example here\n    # shows a real URL so a user uncommenting the line gets a working shape.\n    \"repo_url\": \"https://github.com/owner/repo\",\n    \"deployment_file_path\": \".\",\n    \"git_ref\": \"main\",\n    \"appserver_version\": \"0.5.0\",\n    \"suspended\": False,\n    \"secrets\": {\"MY_SECRET\": \"${MY_SECRET}\"},\n    \"personal_access_token\": \"${GITHUB_TOKEN}\",\n}\n\n\ndef render(\n    display: DeploymentDisplay,\n    *,\n    head: Sequence[str] = (),\n    secret_comments: Mapping[str, str] = {},\n    field_alternatives: Mapping[str, tuple[str, str]] = {},\n    required: Iterable[str] = (),\n    name_example: str = \"my-app\",\n    scaffold_generate_name: bool = False,\n    strip_mask_sentinels: bool = False,\n) -> str:\n    \"\"\"Render ``display`` as a commented apply-shaped YAML string.\n\n    Args:\n        display: The deployment to render. ``status`` is unconditionally\n            omitted; only ``name``, ``generate_name``, and ``spec`` reach the\n            output.\n        head: Lines emitted at the top of the output as ``## `` comments,\n            before ``name:``. An empty-string entry becomes a bare ``##``.\n        secret_comments: Map of secret name → comment text. Each becomes a\n            trailing ``## `` note on the matching key inside the ``secrets:``\n            block. Ignored when ``secrets`` is unset.\n        field_alternatives: Map of field name → ``(suggestion, annotation)``.\n            For each *set* field listed, a commented-out\n            ``# <field>: <suggestion>  ## <annotation>`` line is emitted\n            directly under the field. Silently ignored for fields in the\n            ``required`` or ``unset`` states.\n        required: Field names (python attribute names on ``DeploymentSpec``)\n            to force-emit as ``<key>: ~`` with a ``## Required …`` marker\n            even when unset. The top-level ``name`` is *not* supported here —\n            it has no required path.\n        name_example: Example value rendered for an unset top-level ``name``\n            (and ``generate_name`` when it has no model value). Defaults to\n            ``\"my-app\"``; the offline template command passes the cwd name.\n        scaffold_generate_name: When ``True``, emit a commented-out\n            ``# generate_name: <value>`` line at the identity tier. When\n            ``False`` (the default), the\n            field is omitted entirely. Only the offline ``deployments\n            template`` flow opts in; ``get -o template`` does not.\n        strip_mask_sentinels: When ``True``, omit masked secret values\n            from output so they don't round-trip into apply input.  The\n            default is ``False`` — masked placeholders are preserved so\n            ``get -o template`` and the editor both show which secrets\n            exist.  Stripping happens on the apply/parse side instead.\n    \"\"\"\n    required_set = set(required)\n    out: list[str] = []\n\n    for line in head:\n        out.append(\"##\" if line == \"\" else f\"{_MARKER}{line}\")\n    if head:\n        out.append(\"\")\n\n    if scaffold_generate_name:\n        out.extend(_doc_lines(_SCAFFOLD_IDENTITY_DOCS, indent=\"\"))\n    else:\n        name_docs = _docs(DeploymentDisplay.model_fields[\"name\"])\n        out.extend(_doc_lines(name_docs, indent=\"\"))\n    if display.name is None:\n        out.append(f\"# name: {name_example}\")\n    else:\n        out.append(f\"name: {_scalar(display.name)}\")\n\n    if scaffold_generate_name:\n        gn_value = display.generate_name or name_example\n        out.append(f\"# generate_name: {_scalar(gn_value)}\")\n\n    out.append(\"\")\n    out.append(\"spec:\")\n    spec_dump = display.spec.model_dump(mode=\"json\", exclude_none=True)\n    spec_set = strip_masks(spec_dump) if strip_mask_sentinels else spec_dump\n\n    for idx, (fname, finfo) in enumerate(DeploymentSpec.model_fields.items()):\n        if idx > 0 and fname in {\n            \"deployment_file_path\",\n            \"secrets\",\n            \"personal_access_token\",\n        }:\n            out.append(\"\")\n\n        docs = _docs(finfo)\n        if fname in required_set and fname not in spec_set:\n            docs = _required_docs(docs)\n        out.extend(_doc_lines(docs, indent=_INDENT))\n\n        if fname in spec_set:\n            _emit_set_field(\n                out,\n                fname,\n                spec_set[fname],\n                finfo,\n                secret_comments,\n                field_alternatives,\n            )\n        elif fname in required_set:\n            out.append(_with_trailing(f\"{_INDENT}{fname}: ~\", _trailing_doc(finfo)))\n        else:\n            _emit_unset_field(out, fname, finfo)\n\n    return \"\\n\".join(out) + \"\\n\"\n\n\ndef _emit_set_field(\n    out: list[str],\n    fname: str,\n    value: Any,\n    finfo: FieldInfo,\n    secret_comments: Mapping[str, str],\n    field_alternatives: Mapping[str, tuple[str, str]],\n) -> None:\n    \"\"\"Append lines for a spec field that has a value.\"\"\"\n    if fname == \"secrets\" and isinstance(value, dict):\n        out.append(f\"{_INDENT}{fname}:\")\n        for sname, sval in value.items():\n            out.append(\n                _with_trailing(\n                    f\"{_INDENT * 2}{sname}: {_scalar(sval)}\",\n                    _one_line(secret_comments.get(sname)),\n                    align=False,\n                )\n            )\n        return\n    out.append(\n        _with_trailing(f\"{_INDENT}{fname}: {_scalar(value)}\", _trailing_doc(finfo))\n    )\n    alt = field_alternatives.get(fname)\n    if alt is not None:\n        alt_value, alt_note = alt\n        out.append(_with_trailing(f\"{_INDENT}# {fname}: {alt_value}\", alt_note))\n\n\ndef _emit_unset_field(out: list[str], fname: str, finfo: FieldInfo) -> None:\n    \"\"\"Append a commented-out example line for an unset spec field.\"\"\"\n    example = _EXAMPLES.get(fname, \"\")\n    if isinstance(example, dict):\n        out.append(f\"{_INDENT}# {fname}:\")\n        for k, v in example.items():\n            out.append(f\"{_INDENT * 2}# {k}: {_scalar(v)}\")\n    else:\n        out.append(\n            _with_trailing(\n                f\"{_INDENT}# {fname}: {_scalar(example)}\",\n                _trailing_doc(finfo),\n            )\n        )\n\n\ndef _doc_lines(docs: Iterable[str], *, indent: str) -> list[str]:\n    \"\"\"Return ``## <doc>`` lines at the given indent, trailing-stripped.\"\"\"\n    return [f\"{indent}{_MARKER}{d}\".rstrip() for d in docs]\n\n\ndef _docs(info: FieldInfo | None) -> tuple[str, ...]:\n    \"\"\"Return the first ``Doc`` marker text on ``info``, split into lines.\"\"\"\n    if info is None:\n        return ()\n    for marker in info.metadata:\n        if isinstance(marker, Doc):\n            return tuple(marker.text.split(\"\\n\"))\n    return ()\n\n\ndef _trailing_doc(info: FieldInfo | None) -> str | None:\n    \"\"\"Return the first ``TrailingDoc`` marker text on ``info``.\"\"\"\n    if info is None:\n        return None\n    for marker in info.metadata:\n        if isinstance(marker, TrailingDoc):\n            return marker.text\n    return None\n\n\ndef _required_docs(docs: tuple[str, ...]) -> tuple[str, ...]:\n    \"\"\"Insert a REQUIRED marker above the existing doc lines.\"\"\"\n    return (\"REQUIRED.\", *docs)\n\n\ndef _one_line(value: str | None) -> str | None:\n    if value is None:\n        return None\n    return \" \".join(value.split())\n\n\ndef _with_trailing(line: str, note: str | None, *, align: bool = True) -> str:\n    \"\"\"Append an aligned trailing ``##`` note when ``note`` is present.\"\"\"\n    if not note:\n        return line\n    gap = max(2, _TRAILING_COLUMN - len(line)) if align else 2\n    return f\"{line}{' ' * gap}{_MARKER}{note}\"\n\n\ndef _scalar(value: Any) -> str:\n    \"\"\"Render ``value`` as a YAML scalar suitable for the right-hand side of\n    a key line.\n\n    Delegates to PyYAML in mapping context (``{\"_\": value}``) so its\n    emitter applies block-context rules — plain for ``${VAR}``, URLs,\n    versions; quoted for reserved words / flow chars / values containing\n    ``: `` or `` #``. Carve-outs: ``None`` emits as ``~`` for parity with\n    the required-tilde rendering, ``\"\"`` emits as ``\"\"`` so the push-mode\n    signal isn't hidden as PyYAML's default ``''``, and single-character\n    strings (except ``\"``) are force-quoted so values like ``\".\"`` read\n    unambiguously rather than hitting YAML plain-scalar edge cases.\n    \"\"\"\n    if value is None:\n        return \"~\"\n    if value == \"\":\n        return '\"\"'\n    if isinstance(value, str) and len(value) == 1 and value != '\"':\n        return f'\"{value}\"'\n    return yaml.safe_dump(\n        {\"_\": value}, default_flow_style=False, width=1 << 30, allow_unicode=True\n    ).rstrip()[3:]\n"
  },
  {
    "path": "packages/llamactl/src/llama_deploy/cli/__init__.py",
    "content": "# Backwards-compatibility shim: llama_deploy.cli -> llama_agents.cli\nfrom llama_agents.core._alias import install_alias_finder\n\ninstall_alias_finder()\n\nfrom llama_agents.cli import *  # noqa: E402, F403\nfrom llama_agents.cli import __all__  # noqa: E402, F401\n"
  },
  {
    "path": "packages/llamactl/tests/conftest.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Test session configuration for isolating per-worker state.\n\nEach xdist worker gets a unique HOME and LLAMACTL_CONFIG_DIR so migrations and\nSQLite DBs do not clash across processes.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport os\nimport shutil\nimport tempfile\nfrom types import SimpleNamespace\nfrom typing import Any\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport httpx\nimport pytest\nfrom llama_agents.core.schema.deployments import DeploymentResponse\nfrom llama_agents.core.schema.git_validation import RepositoryValidationResponse\n\n# Base temp dir for the whole session\n_TEST_HOME = tempfile.mkdtemp(prefix=\"llamactl_test_home_\")\n\n# Worker-specific subdir (e.g. gw0, gw1). Falls back to 'main' without xdist.\n_WORKER_ID = os.environ.get(\"PYTEST_XDIST_WORKER\", \"main\")\n_WORKER_HOME = os.path.join(_TEST_HOME, _WORKER_ID)\n_WORKER_CONFIG = os.path.join(_WORKER_HOME, \".config\", \"llamactl\")\n\n# Ensure directories exist\nos.makedirs(_WORKER_CONFIG, exist_ok=True)\n\n# Isolate HOME and config per worker\nos.environ[\"HOME\"] = _WORKER_HOME\nos.environ.setdefault(\"TERM\", \"xterm\")\nos.environ[\"LLAMACTL_CONFIG_DIR\"] = _WORKER_CONFIG\n\nLLAMA_CLOUD_ENV_VARS = (\n    \"LLAMA_CLOUD_API_KEY\",\n    \"LLAMA_CLOUD_BASE_URL\",\n    \"LLAMA_CLOUD_USE_PROFILE\",\n    \"LLAMA_AGENTS_PROJECT_ID\",\n    \"LLAMA_DEPLOY_PROJECT_ID\",\n    \"_LLAMACTL_COMPLETE\",\n    \"llama_cloud_api_key\",\n    \"llama_cloud_base_url\",\n    \"llama_cloud_use_profile\",\n    \"llama_agents_project_id\",\n    \"llama_deploy_project_id\",\n)\n\n\ndef pytest_sessionfinish(session: pytest.Session, exitstatus: pytest.ExitCode) -> None:\n    try:\n        shutil.rmtree(_TEST_HOME, ignore_errors=True)\n    except Exception:\n        pass\n\n\ndef make_deployment(\n    deployment_id: str = \"my-app\", **overrides: Any\n) -> DeploymentResponse:\n    \"\"\"Build a DeploymentResponse with sensible defaults for command tests.\"\"\"\n    base: dict[str, Any] = {\n        \"id\": deployment_id,\n        \"display_name\": deployment_id,\n        \"repo_url\": \"https://github.com/example/repo\",\n        \"deployment_file_path\": \"llama_deploy.yaml\",\n        \"git_ref\": \"main\",\n        \"git_sha\": \"abc1234567890\",\n        \"project_id\": \"proj_default\",\n        \"secret_names\": [],\n        \"apiserver_url\": None,\n        \"status\": \"Running\",\n    }\n    base.update(overrides)\n    return DeploymentResponse.model_validate(base)\n\n\ndef make_loop_bound_project_client(\n    *,\n    existing: DeploymentResponse | None = None,\n    created: DeploymentResponse | None = None,\n    updated: DeploymentResponse | None = None,\n    validate_accessible: bool = True,\n) -> MagicMock:\n    loop: asyncio.AbstractEventLoop | None = None\n\n    def check_loop() -> None:\n        nonlocal loop\n        running = asyncio.get_running_loop()\n        if loop is None:\n            loop = running\n        elif running is not loop:\n            raise RuntimeError(\"Event loop is closed\")\n\n    async def get_deployment(\n        deployment_id: str, include_events: bool = False\n    ) -> DeploymentResponse:\n        check_loop()\n        if existing is not None and deployment_id == existing.id:\n            return existing\n        request = httpx.Request(\n            \"GET\", f\"http://test/api/v1beta1/deployments/{deployment_id}\"\n        )\n        response = httpx.Response(404, request=request, text='{\"detail\":\"not found\"}')\n        raise httpx.HTTPStatusError(\"HTTP 404\", request=request, response=response)\n\n    async def validate_repository(\n        repo_url: str,\n        deployment_id: str | None = None,\n        pat: str | None = None,\n    ) -> RepositoryValidationResponse:\n        check_loop()\n        return RepositoryValidationResponse(\n            accessible=validate_accessible,\n            message=\"ok\" if validate_accessible else \"repo not found\",\n        )\n\n    async def create_deployment(payload: Any) -> DeploymentResponse:\n        check_loop()\n        return created or make_deployment(\"new-app\")\n\n    async def update_deployment(deployment_id: str, payload: Any) -> DeploymentResponse:\n        check_loop()\n        return updated or existing or make_deployment(deployment_id)\n\n    async def aclose() -> None:\n        check_loop()\n\n    client = MagicMock()\n    client.project_id = \"proj_default\"\n    client.base_url = \"http://test:8011\"\n    client.api_key = \"profile-client-key\"\n    client.get_deployment = AsyncMock(side_effect=get_deployment)\n    client.validate_repository = AsyncMock(side_effect=validate_repository)\n    client.create_deployment = AsyncMock(side_effect=create_deployment)\n    client.update_deployment = AsyncMock(side_effect=update_deployment)\n    client.aclose = AsyncMock(side_effect=aclose)\n    return client\n\n\ndef clear_llama_cloud_env(monkeypatch: pytest.MonkeyPatch) -> None:\n    for name in LLAMA_CLOUD_ENV_VARS:\n        monkeypatch.delenv(name, raising=False)\n\n\ndef set_llama_cloud_env(\n    monkeypatch: pytest.MonkeyPatch,\n    *,\n    api_key: str | None = None,\n    project_id: str | None = None,\n    base_url: str | None = None,\n    use_profile: bool | None = None,\n    completion: str | None = None,\n) -> None:\n    clear_llama_cloud_env(monkeypatch)\n    if api_key is not None:\n        monkeypatch.setenv(\"LLAMA_CLOUD_API_KEY\", api_key)\n    if project_id is not None:\n        monkeypatch.setenv(\"LLAMA_AGENTS_PROJECT_ID\", project_id)\n    if base_url is not None:\n        monkeypatch.setenv(\"LLAMA_CLOUD_BASE_URL\", base_url)\n    if use_profile is not None:\n        monkeypatch.setenv(\"LLAMA_CLOUD_USE_PROFILE\", \"1\" if use_profile else \"0\")\n    if completion is not None:\n        monkeypatch.setenv(\"_LLAMACTL_COMPLETE\", completion)\n\n\ndef patch_project_client(client_mock: MagicMock) -> Any:\n    \"\"\"Patch ProjectClient construction inside ``cli.client.get_project_client``.\"\"\"\n    return patch(\n        \"llama_agents.core.client.manage_client.ProjectClient\",\n        return_value=client_mock,\n    )\n\n\n@pytest.fixture\ndef fake_profile() -> SimpleNamespace:\n    return SimpleNamespace(\n        api_url=\"http://test:8011\",\n        project_id=\"proj_default\",\n        api_key=\"key\",\n        device_oidc=None,\n        name=\"prof\",\n    )\n\n\n@pytest.fixture\ndef patched_auth(fake_profile: SimpleNamespace) -> Any:\n    \"\"\"Patch the env service so commands use a fake authenticated profile.\"\"\"\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = fake_profile\n        mock_auth_svc.list_profiles.return_value = [fake_profile]\n        mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n        mock_auth_svc.auth_middleware.return_value = None\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        yield mock_service\n"
  },
  {
    "path": "packages/llamactl/tests/test_apply_yaml.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``cli.apply_yaml`` — YAML parsing for ``deployments apply -f``.\"\"\"\n\nfrom __future__ import annotations\n\nimport textwrap\n\nimport pytest\nfrom llama_agents.cli.apply_yaml import (\n    SPEC_FIELDS,\n    ApplyYamlError,\n    FieldError,\n    UnresolvedEnvVarsError,\n    annotate_yaml_with_errors,\n    parse_apply_yaml,\n    parse_delete_yaml_name,\n)\nfrom llama_agents.cli.display import DeploymentSpec\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\nMINIMAL_YAML = textwrap.dedent(\"\"\"\\\n    name: my-app\n    spec:\n      repo_url: https://github.com/example/repo\n\"\"\")\n\n\ndef _yaml_with_spec(**spec_fields: object) -> str:\n    \"\"\"Build a minimal YAML doc with arbitrary spec fields.\"\"\"\n    lines = [\"name: my-app\", \"spec:\"]\n    for k, v in spec_fields.items():\n        lines.append(f\"  {k}: {v}\")\n    return \"\\n\".join(lines) + \"\\n\"\n\n\n# ---------------------------------------------------------------------------\n# SPEC_FIELDS sync guard\n# ---------------------------------------------------------------------------\n\n# Fields on DeploymentSpec that are not wire-level spec fields for annotation\n# purposes (e.g. secrets is handled separately via nested key indexing).\n_SPEC_FIELDS_EXCLUDED = {\"secrets\"}\n\n\ndef test_spec_fields_covers_deployment_spec() -> None:\n    \"\"\"SPEC_FIELDS must stay in sync with DeploymentSpec's model fields.\"\"\"\n    model_fields = set(DeploymentSpec.model_fields.keys()) - _SPEC_FIELDS_EXCLUDED\n    assert model_fields == SPEC_FIELDS\n\n\n# ---------------------------------------------------------------------------\n# parse_apply_yaml — basics\n# ---------------------------------------------------------------------------\n\n\ndef test_parse_basic_name_and_repo() -> None:\n    display = parse_apply_yaml(MINIMAL_YAML)\n    assert display.name == \"my-app\"\n    assert display.spec.repo_url == \"https://github.com/example/repo\"\n\n\ndef test_parse_drops_status_key() -> None:\n    \"\"\"Round-trip from ``get -o yaml`` includes ``status``; parse strips it.\"\"\"\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n        status:\n          phase: Running\n          project_id: proj_default\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.name == \"my-app\"\n    assert display.status is None\n\n\ndef test_parse_unknown_spec_field_raises() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          image_tag: latest\n    \"\"\")\n    with pytest.raises(ApplyYamlError, match=\"image_tag\"):\n        parse_apply_yaml(doc)\n\n\ndef test_parse_unknown_spec_field_rebuild_raises() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          rebuild: true\n    \"\"\")\n    with pytest.raises(ApplyYamlError, match=\"rebuild\"):\n        parse_apply_yaml(doc)\n\n\n# ---------------------------------------------------------------------------\n# Environment variable resolution\n# ---------------------------------------------------------------------------\n\n\ndef test_env_var_resolves(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setenv(\"MY_REPO\", \"https://github.com/resolved/repo\")\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: ${MY_REPO}\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.spec.repo_url == \"https://github.com/resolved/repo\"\n\n\ndef test_env_var_multiple_in_one_string(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setenv(\"HOST\", \"example.com\")\n    monkeypatch.setenv(\"PORT\", \"8080\")\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://${HOST}:${PORT}/repo\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.spec.repo_url == \"https://example.com:8080/repo\"\n\n\ndef test_env_var_missing_raises(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.delenv(\"MISSING_VAR\", raising=False)\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: ${MISSING_VAR}\n    \"\"\")\n    with pytest.raises(UnresolvedEnvVarsError) as exc_info:\n        parse_apply_yaml(doc)\n    assert \"MISSING_VAR\" in exc_info.value.unresolved\n\n\ndef test_env_var_multiple_missing_listed(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.delenv(\"AAA\", raising=False)\n    monkeypatch.delenv(\"BBB\", raising=False)\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: ${AAA}/${BBB}\n    \"\"\")\n    with pytest.raises(UnresolvedEnvVarsError) as exc_info:\n        parse_apply_yaml(doc)\n    assert \"AAA\" in exc_info.value.unresolved\n    assert \"BBB\" in exc_info.value.unresolved\n\n\ndef test_env_var_missing_reports_all_paths(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.delenv(\"GITHUB_TOKEN\", raising=False)\n    monkeypatch.delenv(\"OPENAI_API_KEY\", raising=False)\n    doc = textwrap.dedent(\"\"\"\\\n        generate_name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          personal_access_token: ${GITHUB_TOKEN}\n          secrets:\n            OPENAI_API_KEY: ${OPENAI_API_KEY}\n    \"\"\")\n\n    with pytest.raises(UnresolvedEnvVarsError) as exc_info:\n        parse_apply_yaml(doc)\n\n    assert exc_info.value.unresolved == [\"GITHUB_TOKEN\", \"OPENAI_API_KEY\"]\n    assert [error.path for error in exc_info.value.errors] == [\n        (\"spec\", \"personal_access_token\"),\n        (\"spec\", \"secrets\", \"OPENAI_API_KEY\"),\n    ]\n\n\ndef test_env_var_in_unknown_field_is_not_resolved(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.delenv(\"MISSING_VAR\", raising=False)\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          image_tag: ${MISSING_VAR}\n    \"\"\")\n    with pytest.raises(ApplyYamlError, match=\"image_tag\"):\n        parse_apply_yaml(doc)\n\n\ndef test_env_var_in_non_string_field_is_not_resolved(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.delenv(\"MISSING_VAR\", raising=False)\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          suspended: ${MISSING_VAR}\n    \"\"\")\n    with pytest.raises(ApplyYamlError, match=\"suspended\"):\n        parse_apply_yaml(doc)\n\n\ndef test_env_var_in_secrets(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setenv(\"SECRET_VAL\", \"s3cret\")\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          secrets:\n            MY_SECRET: ${SECRET_VAL}\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.spec.secrets is not None\n    assert display.spec.secrets[\"MY_SECRET\"] == \"s3cret\"\n\n\n# ---------------------------------------------------------------------------\n# Mask passthrough (strip SECRET_MASK values)\n# ---------------------------------------------------------------------------\n\n\ndef test_mask_pat_stripped() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          personal_access_token: \"********\"\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    # Masked PAT is stripped — field not set on the model.\n    assert \"personal_access_token\" not in display.spec.model_fields_set\n\n\ndef test_mask_secret_entry_stripped() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          secrets:\n            FOO: \"********\"\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    # All entries masked → secrets key itself dropped.\n    assert display.spec.secrets is None or \"FOO\" not in display.spec.secrets\n\n\ndef test_mask_partial_secrets() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          secrets:\n            FOO: \"********\"\n            BAR: real-value\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.spec.secrets is not None\n    assert \"FOO\" not in display.spec.secrets\n    assert display.spec.secrets[\"BAR\"] == \"real-value\"\n\n\n# ---------------------------------------------------------------------------\n# Null handling\n# ---------------------------------------------------------------------------\n\n\ndef test_null_pat_preserved() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          personal_access_token: null\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.spec.personal_access_token is None\n    assert \"personal_access_token\" in display.spec.model_fields_set\n\n\ndef test_null_secret_value_preserved() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          secrets:\n            FOO: null\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.spec.secrets is not None\n    assert display.spec.secrets[\"FOO\"] is None\n\n\n# ---------------------------------------------------------------------------\n# generate_name\n# ---------------------------------------------------------------------------\n\n\ndef test_generate_name_snake_case() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        generate_name: my-slug\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    display = parse_apply_yaml(doc)\n    assert display.generate_name == \"my-slug\"\n\n\n# ---------------------------------------------------------------------------\n# parse_delete_yaml_name\n# ---------------------------------------------------------------------------\n\n\ndef test_delete_returns_name() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    assert parse_delete_yaml_name(doc) == \"my-app\"\n\n\ndef test_delete_missing_name_raises() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    with pytest.raises(ApplyYamlError):\n        parse_delete_yaml_name(doc)\n\n\ndef test_delete_non_string_name_raises() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: 42\n    \"\"\")\n    with pytest.raises(ApplyYamlError):\n        parse_delete_yaml_name(doc)\n\n\ndef test_delete_ignores_other_fields_no_env_resolution(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"No env resolution or model validation happens.\"\"\"\n    monkeypatch.delenv(\"MISSING\", raising=False)\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: ${MISSING}\n          bogus_field: whatever\n    \"\"\")\n    # Should succeed — only name is inspected.\n    assert parse_delete_yaml_name(doc) == \"my-app\"\n\n\n# ---------------------------------------------------------------------------\n# Schema validation error messages\n# ---------------------------------------------------------------------------\n\n\ndef test_validation_error_includes_spec_prefix() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          repo_url: https://github.com/example/repo\n          bogus: nope\n    \"\"\")\n    with pytest.raises(ApplyYamlError, match=\"spec\"):\n        parse_apply_yaml(doc)\n\n\n# ---------------------------------------------------------------------------\n# annotate_yaml_with_errors\n# ---------------------------------------------------------------------------\n\n\ndef _field_error(path: tuple[str | int, ...], message: str) -> FieldError:\n    return FieldError(path=path, message=message)\n\n\ndef test_annotate_top_level_field() -> None:\n    doc = \"name: my-app\\nspec:\\n  repo_url: https://github.com/example/repo\\n\"\n\n    annotated = annotate_yaml_with_errors(\n        doc, [_field_error((\"name\",), \"must be a valid DNS label\")]\n    )\n\n    assert annotated.startswith(\"## ERROR: must be a valid DNS label\\nname: my-app\")\n\n\ndef test_annotate_spec_field_above_doc_block() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          ## https://... = remote git URL.\n          ## Omit to keep the current value.\n          repo_url: https://github.com/example/repo\n    \"\"\")\n\n    annotated = annotate_yaml_with_errors(\n        doc, [_field_error((\"spec\", \"repo_url\"), \"repo not found\")]\n    )\n\n    assert \"  ## ERROR: repo not found\\n  ## https://... = remote git URL.\" in annotated\n    assert \"## Omit to keep the current value.\" in annotated\n\n\ndef test_annotate_secret_path_preserves_indentation() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          secrets:\n            API_KEY: null\n    \"\"\")\n\n    annotated = annotate_yaml_with_errors(\n        doc, [_field_error((\"spec\", \"secrets\", \"API_KEY\"), \"cannot delete on create\")]\n    )\n\n    assert \"    ## ERROR: cannot delete on create\\n    API_KEY: null\" in annotated\n\n\ndef test_annotate_multiple_errors_same_field_preserves_order() -> None:\n    doc = \"generate_name: My App\\nspec: {}\\n\"\n\n    annotated = annotate_yaml_with_errors(\n        doc,\n        [\n            _field_error((\"generate_name\",), \"first\"),\n            _field_error((\"generate_name\",), \"second\"),\n        ],\n    )\n\n    assert annotated.startswith(\n        \"## ERROR: first\\n## ERROR: second\\ngenerate_name: My App\"\n    )\n\n\ndef test_annotate_unresolved_path_prepends_with_path() -> None:\n    doc = \"name: my-app\\nspec: {}\\n\"\n\n    annotated = annotate_yaml_with_errors(\n        doc, [_field_error((\"spec\", \"missing\"), \"not valid\")]\n    )\n\n    assert annotated.startswith(\"## ERROR: spec.missing: not valid\\nname: my-app\")\n\n\ndef test_annotate_is_idempotent_for_existing_error_lines() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        ## ERROR: old file error\n        name: my-app\n        spec:\n          ## ERROR: old repo error\n          repo_url: https://github.com/example/repo\n    \"\"\")\n\n    once = annotate_yaml_with_errors(\n        doc, [_field_error((\"spec\", \"repo_url\"), \"new repo error\")]\n    )\n    twice = annotate_yaml_with_errors(\n        once, [_field_error((\"spec\", \"repo_url\"), \"new repo error\")]\n    )\n\n    assert once == twice\n    assert \"old file error\" not in once\n    assert once.count(\"## ERROR: new repo error\") == 1\n\n\ndef test_annotate_preserves_template_docs_that_are_not_errors() -> None:\n    doc = textwrap.dedent(\"\"\"\\\n        ## Edit, then run: llamactl deployments apply -f <file>\n        name: my-app\n        spec: {}\n    \"\"\")\n\n    annotated = annotate_yaml_with_errors(doc, [_field_error((), \"file problem\")])\n\n    assert \"## Edit, then run: llamactl deployments apply -f <file>\" in annotated\n\n\ndef test_annotate_syntax_error_falls_back_to_file_level() -> None:\n    doc = \"name: [\\n\"\n\n    annotated = annotate_yaml_with_errors(doc, [_field_error((\"name\",), \"bad name\")])\n\n    assert annotated == \"## ERROR: name: bad name\\nname: [\\n\"\n\n\ndef test_annotate_multiline_error_comments_every_line() -> None:\n    doc = \"name: [\\n\"\n\n    annotated = annotate_yaml_with_errors(\n        doc, [_field_error((), \"invalid YAML: first line\\n  second line\\nthird line\")]\n    )\n\n    assert annotated == (\n        \"## ERROR: invalid YAML: first line\\n\"\n        \"## ERROR:   second line\\n\"\n        \"## ERROR: third line\\n\"\n        \"name: [\\n\"\n    )\n\n\ndef test_annotate_multiline_error_is_idempotent() -> None:\n    doc = \"name: [\\n\"\n    error = _field_error((), \"invalid YAML: first line\\nsecond line\")\n\n    once = annotate_yaml_with_errors(doc, [error])\n    twice = annotate_yaml_with_errors(once, [error])\n\n    assert once == twice\n    assert once.count(\"## ERROR:\") == 2\n\n\ndef test_annotate_non_mapping_falls_back_to_file_level() -> None:\n    doc = \"- name: my-app\\n\"\n\n    annotated = annotate_yaml_with_errors(doc, [_field_error((\"name\",), \"bad name\")])\n\n    assert annotated == \"## ERROR: name: bad name\\n- name: my-app\\n\"\n"
  },
  {
    "path": "packages/llamactl/tests/test_auth_cli.py",
    "content": "from types import SimpleNamespace\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport httpx\nimport pytest\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.commands.auth import _create_or_update_agent_api_key\nfrom llama_agents.cli.config.schema import Auth\n\n\ndef test_auth_create_api_key_profile_non_interactive_validation() -> None:\n    runner = CliRunner()\n    with patch(\n        \"llama_agents.cli.commands.auth.is_interactive_session\", return_value=False\n    ):\n        result = runner.invoke(app, [\"auth\", \"token\"])\n    assert result.exit_code != 0\n    assert (\n        \"--api-key and --project are required in non-interactive mode\" in result.output\n    )\n\n\ndef test_auth_create_api_key_profile_non_interactive_success() -> None:\n    runner = CliRunner()\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\n            \"llama_agents.cli.commands.auth.is_interactive_session\", return_value=False\n        ),\n    ):\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.create_profile_from_token.return_value = SimpleNamespace(\n            name=\"prof\"\n        )\n        mock_service.current_auth_service.return_value = mock_auth_svc\n\n        result = runner.invoke(\n            app,\n            [\n                \"auth\",\n                \"token\",\n                \"--project\",\n                \"p\",\n                \"--api-key\",\n                \"key\",\n            ],\n        )\n        assert result.exit_code == 0\n        mock_auth_svc.create_profile_from_token.assert_called_once_with(\"p\", \"key\")\n\n\ndef test_auth_create_api_key_profile_project_id_alias() -> None:\n    runner = CliRunner()\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\n            \"llama_agents.cli.commands.auth.is_interactive_session\", return_value=False\n        ),\n    ):\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.create_profile_from_token.return_value = SimpleNamespace(\n            name=\"prof\"\n        )\n        mock_service.current_auth_service.return_value = mock_auth_svc\n\n        result = runner.invoke(\n            app,\n            [\n                \"auth\",\n                \"token\",\n                \"--project-id\",\n                \"p\",\n                \"--api-key\",\n                \"key\",\n            ],\n        )\n\n    assert result.exit_code == 0\n    mock_auth_svc.create_profile_from_token.assert_called_once_with(\"p\", \"key\")\n\n\ndef test_auth_get_profiles_no_profiles() -> None:\n    runner = CliRunner()\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.list_profiles.return_value = []\n        mock_auth_svc.get_current_profile.return_value = None\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        result = runner.invoke(app, [\"auth\", \"get\"])\n        assert result.exit_code == 0\n        assert \"no profiles found\" in result.output\n\n\ndef test_auth_use_profile_success_and_missing() -> None:\n    runner = CliRunner()\n    with (\n        patch(\"llama_agents.cli.commands.auth._select_profile\") as mock_select,\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_auth_svc = MagicMock()\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        mock_select.return_value = SimpleNamespace(name=\"p1\")\n        result = runner.invoke(app, [\"auth\", \"use\", \"p1\"])\n        assert result.exit_code == 0\n        mock_auth_svc.set_current_profile.assert_called_once_with(\"p1\")\n\n    with (\n        patch(\"llama_agents.cli.commands.auth._select_profile\", return_value=None),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service2,\n    ):\n        mock_service2.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"auth\", \"use\", \"doesnt-exist\"])\n        assert result.exit_code == 0\n        assert \"no profile selected\" in result.output\n\n\ndef test_auth_use_non_interactive_lists_profiles_and_hints() -> None:\n    runner = CliRunner()\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        profile = SimpleNamespace(name=\"p1\", api_url=\"https://api\")\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_profile.return_value = None\n        mock_auth_svc.list_profiles.return_value = [profile]\n        mock_auth_svc.get_current_profile.return_value = profile\n        mock_service.current_auth_service.return_value = mock_auth_svc\n\n        result = runner.invoke(app, [\"auth\", \"use\"])\n\n    assert result.exit_code != 0\n    assert \"p1\" in result.output\n    assert \"Pass <name> to choose one\" in result.output\n    assert \"llamactl auth get\" in result.output\n\n\ndef test_removed_auth_commands_show_rename_hints() -> None:\n    runner = CliRunner()\n\n    result = runner.invoke(app, [\"auth\", \"list\"])\n    assert result.exit_code != 0\n    assert \"Use `llamactl auth get` instead.\" in result.output\n\n    result = runner.invoke(app, [\"auth\", \"env\", \"list\"])\n    assert result.exit_code != 0\n    assert \"Use `llamactl environments get` instead.\" in result.output\n\n    result = runner.invoke(app, [\"auth\", \"project\"])\n    assert result.exit_code != 0\n    assert \"Use `llamactl projects` instead.\" in result.output\n\n\ndef test_auth_get_does_not_offer_wide_output() -> None:\n    result = CliRunner().invoke(app, [\"auth\", \"get\", \"-o\", \"wide\"])\n    assert result.exit_code != 0\n    assert \"'wide' is not one of 'text', 'json', 'yaml'\" in result.output\n\n\ndef test_auth_logout_existing() -> None:\n    runner = CliRunner()\n    with (\n        patch(\"llama_agents.cli.commands.auth._select_profile\") as mock_select,\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_auth_svc = MagicMock()\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        mock_select.return_value = SimpleNamespace(name=\"p1\")\n        mock_auth_svc.delete_profile = AsyncMock(return_value=True)\n        result = runner.invoke(app, [\"auth\", \"logout\", \"p1\"])\n        assert result.exit_code == 0\n\n\ndef test_auth_logout_missing() -> None:\n    runner = CliRunner()\n    with (\n        patch(\"llama_agents.cli.commands.auth._select_profile\", return_value=None),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service2,\n    ):\n        mock_service2.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"auth\", \"logout\", \"missing\"])\n        assert result.exit_code != 0\n        assert \"Profile 'missing' not found\" in result.output\n\n\ndef test_projects_use_validates_across_all_orgs() -> None:\n    \"\"\"projects use <id> should not scope validation to the default org.\n\n    Tab completion lists projects from all orgs, so the validation path\n    must also search all orgs — otherwise a project from a non-default org\n    completes but then fails with 'Project not found'.\n    \"\"\"\n    runner = CliRunner()\n    cross_org_project = MagicMock(project_id=\"cross-org-proj-id\")\n    with (\n        patch(\n            \"llama_agents.cli.commands.projects.validate_authenticated_profile\",\n            return_value=MagicMock(name=\"p\", project_id=\"current-proj\"),\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\n            \"llama_agents.cli.commands.projects._list_projects\",\n            return_value=[cross_org_project],\n        ) as mock_list,\n        patch(\n            \"llama_agents.cli.commands.projects._discover_organization\",\n            return_value=MagicMock(org_id=\"default-org-id\"),\n        ),\n    ):\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.env.requires_auth = True\n        mock_service.current_auth_service.return_value = mock_auth_svc\n\n        result = runner.invoke(app, [\"projects\", \"use\", \"cross-org-proj-id\"])\n        assert result.exit_code == 0\n        # Must call _list_projects without org scoping (org_id=None),\n        # not with the auto-discovered default org.\n        mock_list.assert_called_once_with(mock_auth_svc, org_id=None)\n        mock_auth_svc.set_project.assert_called_once()\n\n\ndef test_projects_use_non_interactive_lists_options_and_hints() -> None:\n    runner = CliRunner()\n    with (\n        patch(\n            \"llama_agents.cli.commands.projects.validate_authenticated_profile\",\n            return_value=MagicMock(name=\"p\", project_id=\"x\"),\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._discover_organization\",\n            return_value=None,\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._list_projects\",\n            return_value=[\n                MagicMock(\n                    project_id=\"abc-123\", project_name=\"My Project\", deployment_count=2\n                ),\n            ],\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects.is_interactive_session\",\n            return_value=False,\n        ),\n    ):\n        result = runner.invoke(app, [\"projects\", \"use\"])\n        assert result.exit_code != 0\n        assert \"abc-123\" in result.output\n        assert \"Pass <project_id> to choose one\" in result.output\n        assert \"llamactl projects get\" in result.output\n\n\ndef test_projects_use_interactive_sets_selected() -> None:\n    runner = CliRunner()\n    with (\n        patch(\n            \"llama_agents.cli.commands.projects.validate_authenticated_profile\",\n            return_value=MagicMock(name=\"p\"),\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._list_projects\",\n            return_value=[\n                MagicMock(project_id=\"proj\", project_name=\"Proj\", deployment_count=1)\n            ],\n        ),\n        patch(\"llama_agents.cli.commands.projects.select_or_exit\") as mock_select,\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\n            \"llama_agents.cli.commands.projects.is_interactive_session\",\n            return_value=True,\n        ),\n    ):\n        mock_auth_svc = MagicMock()\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        mock_select.return_value = \"proj\"\n        result = runner.invoke(app, [\"projects\", \"use\"])\n        assert result.exit_code == 0\n        mock_auth_svc.set_project.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_create_or_update_agent_api_key_does_not_retry_read_timeout() -> None:\n    \"\"\"Read-phase errors must not trigger retries.\n\n    create_agent_api_key is a non-idempotent POST: a ReadTimeout after the\n    request left the client could mean the server already created a key, so\n    retrying would duplicate it.\n    \"\"\"\n    profile = Auth(\n        id=\"id-1\",\n        name=\"test\",\n        api_url=\"https://example.com\",\n        project_id=\"proj\",\n        api_key=None,\n        api_key_id=None,\n        device_oidc=None,\n    )\n\n    mock_auth_svc = MagicMock()\n    mock_client_cm = AsyncMock()\n    mock_client = MagicMock()\n    mock_client_cm.__aenter__.return_value = mock_client\n    mock_auth_svc.profile_client.return_value = mock_client_cm\n\n    mock_client.create_agent_api_key = AsyncMock(\n        side_effect=httpx.ReadTimeout(\"server took too long\")\n    )\n\n    with pytest.raises(Exception) as exc_info:\n        await _create_or_update_agent_api_key(mock_auth_svc, profile)\n\n    assert \"Network error while provisioning an API token\" in str(exc_info.value)\n    assert mock_client.create_agent_api_key.await_count == 1\n\n\n@pytest.mark.asyncio\nasync def test_create_or_update_agent_api_key_retries_connect_error() -> None:\n    \"\"\"Connect-phase errors (DNS, connection refused, connect timeout) happen\n    before the request is sent, so retrying is safe even for a non-idempotent\n    POST. The CLI should absorb a brief connectivity blip.\n    \"\"\"\n    profile = Auth(\n        id=\"id-1\",\n        name=\"test\",\n        api_url=\"https://example.com\",\n        project_id=\"proj\",\n        api_key=None,\n        api_key_id=None,\n        device_oidc=None,\n    )\n\n    mock_auth_svc = MagicMock()\n    mock_client_cm = AsyncMock()\n    mock_client = MagicMock()\n    mock_client_cm.__aenter__.return_value = mock_client\n    mock_auth_svc.profile_client.return_value = mock_client_cm\n\n    mock_client.create_agent_api_key = AsyncMock(\n        side_effect=httpx.ConnectError(\"connection refused\")\n    )\n\n    # Patch tenacity's sleep so the test doesn't pay real back-off.\n    with patch(\"tenacity.nap.time.sleep\"), patch(\"asyncio.sleep\", AsyncMock()):\n        with pytest.raises(Exception) as exc_info:\n            await _create_or_update_agent_api_key(mock_auth_svc, profile)\n\n    assert \"Network error while provisioning an API token\" in str(exc_info.value)\n    # Default max_attempts=3 — every attempt should have run the operation.\n    assert mock_client.create_agent_api_key.await_count == 3\n"
  },
  {
    "path": "packages/llamactl/tests/test_auth_inject.py",
    "content": "from pathlib import Path\n\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.config._config import config_manager\nfrom llama_agents.cli.config.schema import Environment\nfrom pytest import MonkeyPatch\n\n\ndef test_auth_inject_writes_env_file(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:\n    # Isolate config dir to temp\n    cfg_dir = tmp_path / \".config\" / \"llamactl\"\n    cfg_dir.mkdir(parents=True, exist_ok=True)\n    monkeypatch.setenv(\"LLAMACTL_CONFIG_DIR\", str(cfg_dir))\n\n    # Create environment and profile\n    # Ensure the global ConfigManager used by the CLI resolves to this temp dir\n    config_manager.cache_clear()  # reset cached instance to honor env var\n    cm = config_manager()\n    env = Environment(\n        api_url=\"https://api.example.com\", requires_auth=True, min_llamactl_version=None\n    )\n    cm.create_or_update_environment(\n        env.api_url, env.requires_auth, env.min_llamactl_version\n    )\n\n    created = cm.create_profile(\n        name=\"default\",\n        api_url=env.api_url,\n        project_id=\"proj-123\",\n        api_key=\"sk-test-abc\",\n    )\n    cm.set_settings_current_environment(env.api_url)\n    cm.set_settings_current_profile(created.name)\n\n    # Run command\n    runner = CliRunner()\n    env_file = tmp_path / \".env\"\n    result = runner.invoke(app, [\"auth\", \"inject\", \"--env-file\", str(env_file)])\n\n    assert result.exit_code == 0, result.output\n    contents = env_file.read_text()\n    assert \"LLAMA_CLOUD_API_KEY='sk-test-abc'\" in contents\n    assert \"LLAMA_CLOUD_BASE_URL='https://api.example.com'\" in contents\n    assert \"LLAMA_AGENTS_PROJECT_ID='proj-123'\" in contents\n"
  },
  {
    "path": "packages/llamactl/tests/test_auth_login.py",
    "content": "from __future__ import annotations\n\nfrom types import SimpleNamespace\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport click\nimport httpx\nimport pytest\nfrom click.testing import CliRunner\nfrom llama_agents.cli.auth.client import (\n    OIDCNotEnabledError,\n    PlatformAuthDiscoveryClient,\n)\nfrom llama_agents.cli.commands.auth import _create_device_profile, device_login\n\n\ndef _make_fake_device_oidc() -> SimpleNamespace:\n    return SimpleNamespace(\n        device_name=\"dev\",\n        user_id=\"user-1\",\n        email=\"user@example.com\",\n        client_id=\"cli\",\n        discovery_url=\"https://example.com/.well-known/openid-configuration\",\n        device_access_token=\"access-token\",\n        device_refresh_token=\"refresh-token\",\n        device_id_token=\"id-token\",\n    )\n\n\ndef _make_fake_auth_profile() -> SimpleNamespace:\n    return SimpleNamespace(\n        id=\"id-1\",\n        name=\"test-profile\",\n        api_url=\"https://api.example.com\",\n        project_id=\"proj-1\",\n        api_key=None,\n        api_key_id=None,\n        device_oidc=None,\n    )\n\n\ndef test_create_device_profile_cleans_up_on_api_key_failure() -> None:\n    \"\"\"If API key provisioning fails, the partially created profile should be deleted.\"\"\"\n\n    fake_profile = _make_fake_auth_profile()\n\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.create_or_update_profile_from_oidc.return_value = fake_profile\n    mock_auth_svc.delete_profile = AsyncMock(return_value=True)\n\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\n            \"llama_agents.cli.commands.auth._run_device_authentication\",\n            return_value=_make_fake_device_oidc(),\n        ),\n        patch(\n            \"llama_agents.cli.commands.auth._discover_organization\",\n            return_value=None,\n        ),\n        patch(\n            \"llama_agents.cli.commands.auth._list_projects\",\n            return_value=[SimpleNamespace(project_id=\"proj-1\")],\n        ),\n        patch(\n            \"llama_agents.cli.commands.auth._select_or_enter_project\",\n            return_value=\"proj-1\",\n        ),\n        patch(\n            \"llama_agents.cli.commands.auth._create_or_update_agent_api_key\",\n            side_effect=click.ClickException(\"network error\"),\n        ),\n    ):\n        mock_service.current_auth_service.return_value = mock_auth_svc\n\n        with pytest.raises(click.ClickException, match=\"network error\"):\n            _create_device_profile()\n\n    mock_auth_svc.delete_profile.assert_awaited_once_with(fake_profile.name)\n\n\n@pytest.mark.asyncio\nasync def test_oidc_discovery_translates_disabled_400_to_typed_error() -> None:\n    \"\"\"A 400 from /auth/oidc/discovery should surface as OIDCNotEnabledError.\"\"\"\n\n    def handler(request: httpx.Request) -> httpx.Response:\n        return httpx.Response(400, json={\"detail\": \"Discovery URL is not enabled\"})\n\n    transport = httpx.MockTransport(handler)\n    client = PlatformAuthDiscoveryClient(\"http://example.test\")\n    client.client = httpx.AsyncClient(\n        base_url=\"http://example.test\", transport=transport\n    )\n    try:\n        with pytest.raises(OIDCNotEnabledError, match=\"Discovery URL is not enabled\"):\n            await client.oidc_discovery()\n    finally:\n        await client.close()\n\n\ndef test_device_login_suggests_token_when_oidc_disabled() -> None:\n    \"\"\"When OIDC is unavailable, login should print a friendly hint, not a stack trace.\"\"\"\n\n    with patch(\n        \"llama_agents.cli.commands.auth._create_device_profile\",\n        side_effect=OIDCNotEnabledError(\"Discovery URL is not enabled\"),\n    ):\n        result = CliRunner().invoke(device_login, [])\n\n    assert result.exit_code == 0, result.output\n    assert \"browser-based login\" in result.output\n    assert \"llamactl auth token\" in result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_auth_validate.py",
    "content": "from types import SimpleNamespace\nfrom unittest.mock import MagicMock, patch\n\nimport click\nimport pytest\nfrom llama_agents.cli.commands.auth import validate_authenticated_profile\n\n_INTERACTIVE_PATCH = \"llama_agents.cli.commands.auth.is_interactive_session\"\n\n\nclass DummyProfile:\n    def __init__(self, name: str):\n        self.name = name\n\n\ndef test_validate_authenticated_profile_returns_current_when_present() -> None:\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = DummyProfile(\"cur\")\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        prof = validate_authenticated_profile()\n        assert isinstance(prof, DummyProfile)\n        assert prof.name == \"cur\"\n\n\ndef test_validate_authenticated_profile_raises_when_non_interactive_and_missing() -> (\n    None\n):\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(_INTERACTIVE_PATCH, return_value=False),\n    ):\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = None\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        with pytest.raises(click.ClickException):\n            validate_authenticated_profile()\n\n\ndef test_validate_authenticated_profile_interactive_multiple_profiles_selects() -> None:\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.auth.select_or_exit\") as mock_select,\n        patch(_INTERACTIVE_PATCH, return_value=True),\n    ):\n        mock_auth_svc = MagicMock()\n        profiles = [DummyProfile(\"a\"), DummyProfile(\"b\")]\n        mock_auth_svc.get_current_profile.return_value = None\n        mock_auth_svc.list_profiles.return_value = profiles\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        mock_select.return_value = profiles[1]\n        prof = validate_authenticated_profile()\n        mock_auth_svc.set_current_profile.assert_called_once_with(\"b\")\n        assert prof.name == \"b\"\n\n\ndef test_validate_authenticated_profile_interactive_multiple_profiles_none_selected() -> (\n    None\n):\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.auth.select_or_exit\") as mock_select,\n        patch(_INTERACTIVE_PATCH, return_value=True),\n    ):\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = None\n        mock_auth_svc.list_profiles.return_value = [\n            DummyProfile(\"a\"),\n            DummyProfile(\"b\"),\n        ]\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        mock_select.side_effect = click.ClickException(\"Cancelled\")\n        with pytest.raises(click.ClickException):\n            validate_authenticated_profile()\n\n\ndef test_validate_authenticated_profile_interactive_single_profile_sets_current() -> (\n    None\n):\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(_INTERACTIVE_PATCH, return_value=True),\n    ):\n        mock_auth_svc = MagicMock()\n        only = DummyProfile(\"only-one\")\n        mock_auth_svc.get_current_profile.return_value = None\n        mock_auth_svc.list_profiles.return_value = [only]\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        prof = validate_authenticated_profile()\n        mock_auth_svc.set_current_profile.assert_called_once_with(\"only-one\")\n        assert prof.name == \"only-one\"\n\n\ndef test_validate_authenticated_profile_interactive_no_profiles_and_no_auth_cancel() -> (\n    None\n):\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.auth.click.prompt\") as mock_prompt,\n        patch(_INTERACTIVE_PATCH, return_value=True),\n    ):\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = None\n        mock_auth_svc.list_profiles.return_value = []\n        mock_auth_svc.env = SimpleNamespace(requires_auth=False)\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        mock_prompt.return_value = \"\"\n        with pytest.raises(click.ClickException):\n            validate_authenticated_profile()\n"
  },
  {
    "path": "packages/llamactl/tests/test_cli_imports.py",
    "content": "import subprocess\nimport sys\nfrom textwrap import dedent\n\n\ndef test_llamactl_help_does_not_import_heavy_modules() -> None:\n    \"\"\"Ensure `llamactl --help` does not require heavy, optional modules.\n\n    Runs the CLI help in a clean Python subprocess and inspects which modules\n    were imported, without mutating this test process's import state.\n    \"\"\"\n    forbidden_prefixes = (\n        \"llama_agents.appserver\",\n        \"aiohttp\",\n        \"httpx\",\n        \"llama_index\",\n    )\n\n    script = dedent(\n        \"\"\"\n        import sys\n\n        from click.testing import CliRunner\n        from llama_agents.cli.app import app\n\n        runner = CliRunner()\n        result = runner.invoke(app, [\"--help\"])\n        if result.exit_code != 0:\n            # Propagate the error code so the parent test can see the failure.\n            raise SystemExit(result.exit_code)\n\n        for name in sorted(sys.modules):\n            print(name)\n        \"\"\"\n    )\n\n    proc = subprocess.run(\n        [sys.executable, \"-c\", script],\n        check=False,\n        capture_output=True,\n        text=True,\n    )\n    assert proc.returncode == 0, proc.stderr\n\n    imported = proc.stdout.splitlines()\n    imported_heavy = [\n        name\n        for name in imported\n        if any(name == p or name.startswith(f\"{p}.\") for p in forbidden_prefixes)\n    ]\n    assert imported_heavy == []\n"
  },
  {
    "path": "packages/llamactl/tests/test_cli_options.py",
    "content": "\"\"\"Tests for CLI options and decorators.\"\"\"\n\nimport os\nfrom typing import Any, Protocol, runtime_checkable\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom click import Context, Parameter\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\n\n\n@runtime_checkable\nclass ClickDecorated(Protocol):\n    __click_params__: list[Any]\n\n\n@pytest.fixture(autouse=True)\ndef clean_env(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Clean TLS-related environment variables before each test.\"\"\"\n    monkeypatch.delenv(\"UV_NATIVE_TLS\", raising=False)\n    monkeypatch.delenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", raising=False)\n    monkeypatch.delenv(\"LLAMACTL_NATIVE_TLS\", raising=False)\n\n\ndef test_native_tls_option_sets_env_vars(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that --native-tls flag sets the expected environment variables.\"\"\"\n    # Clear any existing values\n    monkeypatch.delenv(\"UV_NATIVE_TLS\", raising=False)\n    monkeypatch.delenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", raising=False)\n\n    runner = CliRunner()\n    # Use serve command which has the native_tls_option applied via global_options\n    result = runner.invoke(app, [\"serve\", \"--native-tls\", \"--help\"])\n\n    # Command should succeed\n    assert result.exit_code == 0\n\n    # Check that env vars were set\n    # Note: Click callbacks run during parsing, so we need to check if the flag exists\n    assert \"--native-tls\" in result.output\n\n\ndef test_native_tls_option_preserves_existing_env_vars(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    \"\"\"Test that native TLS option doesn't override pre-existing env vars.\"\"\"\n    # Pre-set custom values\n    monkeypatch.setenv(\"UV_NATIVE_TLS\", \"custom\")\n    monkeypatch.setenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"custom\")\n\n    # Import the callback function after env is set\n    from llama_agents.cli.options import native_tls_option\n\n    # Find the callback by introspecting the decorator chain\n    # native_tls_option returns a function that applies click.option\n    def dummy_func() -> None:\n        pass\n\n    decorated = native_tls_option(dummy_func)\n\n    # Extract the click option params\n    if isinstance(decorated, ClickDecorated):\n        params = decorated.__click_params__\n        # Find the native-tls param\n        for param in params:\n            if hasattr(param, \"name\") and \"native_tls\" in str(param.name):\n                if hasattr(param, \"callback\") and param.callback:\n                    # Call the callback with True\n                    ctx = MagicMock(spec=Context)\n                    param_obj = MagicMock(spec=Parameter)\n                    param.callback(ctx, param_obj, True)\n                    break\n\n    # Values should remain as \"custom\"\n    assert os.environ.get(\"UV_NATIVE_TLS\") == \"custom\"\n    assert os.environ.get(\"LLAMA_DEPLOY_USE_TRUSTSTORE\") == \"custom\"\n\n\ndef test_native_tls_option_from_envvar(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that LLAMACTL_NATIVE_TLS env var enables native TLS.\"\"\"\n    monkeypatch.setenv(\"LLAMACTL_NATIVE_TLS\", \"1\")\n\n    runner = CliRunner()\n    # When running with LLAMACTL_NATIVE_TLS set, the callback should be triggered\n    result = runner.invoke(app, [\"serve\", \"--help\"])\n\n    # Command should succeed\n    assert result.exit_code == 0\n    # Help should show the native-tls option\n    assert \"--native-tls\" in result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_client_ssl_integration.py",
    "content": "\"\"\"Integration tests for SSL context usage in HTTP clients.\"\"\"\n\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nfrom llama_agents.cli.auth.client import ClientContextManager\nfrom llama_agents.core.client.manage_client import BaseClient\n\n\n@pytest.fixture(autouse=True)\ndef clean_env(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Clean SSL-related environment variables before each test.\"\"\"\n    monkeypatch.delenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", raising=False)\n\n\n@pytest.mark.asyncio\nasync def test_manage_client_uses_ssl_context(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that BaseClient passes verify parameter to httpx clients.\"\"\"\n    monkeypatch.setenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"1\")\n\n    mock_client = MagicMock()\n    mock_httpx_async_client = MagicMock(return_value=mock_client)\n\n    with patch(\"httpx.AsyncClient\", mock_httpx_async_client):\n        BaseClient(base_url=\"http://localhost:8000\")\n\n        # Verify httpx.AsyncClient was called twice (client and hookless_client)\n        assert mock_httpx_async_client.call_count == 2\n\n        # Check that both calls included the verify parameter\n        for call in mock_httpx_async_client.call_args_list:\n            kwargs = call[1]\n            assert \"verify\" in kwargs\n            # Should be an SSLContext when truststore is enabled\n            verify_param = kwargs[\"verify\"]\n            # Just verify it's not True (it should be an SSLContext)\n            assert verify_param is not True\n\n\n@pytest.mark.asyncio\nasync def test_manage_client_uses_default_verify() -> None:\n    \"\"\"Test that BaseClient uses default True verify when truststore disabled.\"\"\"\n    mock_client = MagicMock()\n    mock_httpx_async_client = MagicMock(return_value=mock_client)\n\n    with patch(\"httpx.AsyncClient\", mock_httpx_async_client):\n        BaseClient(base_url=\"http://localhost:8000\")\n\n        # Verify httpx.AsyncClient was called\n        assert mock_httpx_async_client.call_count == 2\n\n        # Check that verify=True was passed\n        for call in mock_httpx_async_client.call_args_list:\n            kwargs = call[1]\n            assert kwargs.get(\"verify\") is True\n\n\n@pytest.mark.asyncio\nasync def test_auth_client_uses_ssl_context(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that ClientContextManager passes verify parameter to httpx client.\"\"\"\n    monkeypatch.setenv(\"LLAMA_DEPLOY_USE_TRUSTSTORE\", \"1\")\n\n    mock_client = MagicMock()\n    mock_httpx_async_client = MagicMock(return_value=mock_client)\n\n    with patch(\"httpx.AsyncClient\", mock_httpx_async_client):\n        async with ClientContextManager(base_url=\"http://localhost:8000\"):\n            # Verify httpx.AsyncClient was called\n            assert mock_httpx_async_client.call_count == 1\n\n            # Check that verify parameter was passed\n            call_kwargs = mock_httpx_async_client.call_args[1]\n            assert \"verify\" in call_kwargs\n            # Should be an SSLContext when truststore is enabled\n            verify_param = call_kwargs[\"verify\"]\n            assert verify_param is not True\n\n\n@pytest.mark.asyncio\nasync def test_auth_client_uses_default_verify() -> None:\n    \"\"\"Test that ClientContextManager uses default True verify when disabled.\"\"\"\n    mock_client = MagicMock()\n    mock_httpx_async_client = MagicMock(return_value=mock_client)\n\n    with patch(\"httpx.AsyncClient\", mock_httpx_async_client):\n        async with ClientContextManager(base_url=\"http://localhost:8000\"):\n            # Check that verify=True was passed\n            call_kwargs = mock_httpx_async_client.call_args[1]\n            assert call_kwargs.get(\"verify\") is True\n"
  },
  {
    "path": "packages/llamactl/tests/test_commands_core.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport asyncio\nfrom types import SimpleNamespace\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nimport click\nimport llama_agents.cli.client as client_module\nimport llama_agents.cli.config.env_service as env_service\nimport pytest\nfrom conftest import clear_llama_cloud_env, set_llama_cloud_env\nfrom llama_agents.cli.client import get_control_plane_client, get_project_client\n\nDEFAULT_BASE_URL = \"https://api.cloud.llamaindex.ai\"\nOVERRIDE_WARNING = (\n    \"Using LLAMA_CLOUD_API_KEY from environment (overriding profile 'prof'). \"\n    \"Set LLAMA_CLOUD_USE_PROFILE=1 to use the profile instead.\"\n)\nPARTIAL_ENV_WARNING = (\n    \"LLAMA_CLOUD_API_KEY is set but LLAMA_AGENTS_PROJECT_ID is missing. \"\n    \"Set it or pass --project for env var auth.\"\n)\n\n\n@pytest.fixture(autouse=True)\ndef clean_env_var_auth_state(monkeypatch: pytest.MonkeyPatch) -> None:\n    clear_llama_cloud_env(monkeypatch)\n    monkeypatch.setattr(client_module, \"_ENV_AUTH_WARNING_EMITTED\", False)\n\n\ndef _profile(\n    *,\n    api_url: str = \"http://test:8011\",\n    project_id: str = \"default-project\",\n    api_key: str | None = None,\n    name: str = \"prof\",\n) -> SimpleNamespace:\n    return SimpleNamespace(\n        api_url=api_url,\n        project_id=project_id,\n        api_key=api_key,\n        device_oidc=None,\n        name=name,\n    )\n\n\ndef _set_current_profile(\n    monkeypatch: pytest.MonkeyPatch, profile: SimpleNamespace | None\n) -> MagicMock:\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.get_current_profile.return_value = profile\n    mock_auth_svc.list_profiles.return_value = [] if profile is None else [profile]\n    mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n    mock_auth_svc.auth_middleware.return_value = None\n\n    mock_service = MagicMock()\n    mock_service.current_auth_service.return_value = mock_auth_svc\n    mock_service.get_current_environment.return_value = SimpleNamespace(\n        api_url=DEFAULT_BASE_URL,\n        requires_auth=True,\n    )\n    monkeypatch.setattr(env_service, \"service\", mock_service)\n    return mock_auth_svc\n\n\ndef _close_client(client: Any) -> None:\n    asyncio.run(client.aclose())\n\n\ndef test_deployment_project_resolution() -> None:\n    \"\"\"Test that get_project_client uses profile's project by default\"\"\"\n    profile = _profile()\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = profile\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        client = get_project_client()\n        try:\n            assert client.base_url == \"http://test:8011\"\n            assert client.project_id == \"default-project\"\n        finally:\n            _close_client(client)\n\n\ndef test_client_requires_profile_with_project() -> None:\n    \"\"\"Test that client works when profile has a project (project_id is required)\"\"\"\n    profile = _profile(project_id=\"test-project\")\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = profile\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        client = get_project_client()\n        try:\n            assert client.project_id == \"test-project\"\n        finally:\n            _close_client(client)\n\n\ndef test_client_requires_valid_profile(monkeypatch: pytest.MonkeyPatch) -> None:\n    \"\"\"Test that client fails when no profile is configured\"\"\"\n    monkeypatch.setattr(client_module, \"is_interactive_session\", lambda: False)\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.return_value = None\n        mock_service.current_auth_service.return_value = mock_auth_svc\n        with pytest.raises(click.ClickException, match=\"No profile configured\"):\n            get_project_client()\n\n\ndef test_interactive_project_client_authenticates_and_retries(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    profile = _profile(project_id=\"authed-project\", api_key=\"authed-key\")\n    monkeypatch.setattr(client_module, \"is_interactive_session\", lambda: True)\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\n            \"llama_agents.cli.commands.auth.validate_authenticated_profile\",\n            return_value=profile,\n        ) as validate_authenticated_profile,\n    ):\n        mock_auth_svc = MagicMock()\n        mock_auth_svc.get_current_profile.side_effect = [None, profile]\n        mock_auth_svc.auth_middleware.return_value = None\n        mock_service.current_auth_service.return_value = mock_auth_svc\n\n        client = get_project_client()\n\n    try:\n        validate_authenticated_profile.assert_called_once_with()\n        assert client.base_url == \"http://test:8011\"\n        assert client.project_id == \"authed-project\"\n        assert client.api_key == \"authed-key\"\n    finally:\n        _close_client(client)\n\n\ndef test_env_var_project_client_uses_default_base_url_and_api_key(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _set_current_profile(monkeypatch, None)\n\n    client = get_project_client()\n    try:\n        assert client.base_url == DEFAULT_BASE_URL\n        assert client.api_key == \"env-api-key\"\n        assert client.project_id == \"env-project\"\n    finally:\n        _close_client(client)\n\n\ndef test_env_var_control_plane_client_strips_base_url_and_uses_api_key(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(\n        monkeypatch,\n        api_key=\"env-api-key\",\n        project_id=\"env-project\",\n        base_url=\"https://api.example.test/\",\n    )\n    _set_current_profile(monkeypatch, None)\n\n    client = get_control_plane_client()\n    try:\n        assert client.base_url == \"https://api.example.test\"\n        assert client.api_key == \"env-api-key\"\n    finally:\n        _close_client(client)\n\n\ndef test_incomplete_env_var_project_client_falls_back_to_profile_with_warning(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\")\n    auth_svc = _set_current_profile(\n        monkeypatch,\n        _profile(\n            api_url=\"https://profile.example.test\",\n            project_id=\"profile-project\",\n            api_key=\"profile-api-key\",\n        ),\n    )\n\n    client = get_project_client()\n    try:\n        captured = capsys.readouterr()\n        assert client.base_url == \"https://profile.example.test\"\n        assert client.api_key == \"profile-api-key\"\n        assert client.project_id == \"profile-project\"\n        assert PARTIAL_ENV_WARNING in captured.err\n        assert \"Using LLAMA_CLOUD_API_KEY from environment\" not in captured.err\n        auth_svc.auth_middleware.assert_called_once_with()\n    finally:\n        _close_client(client)\n\n\ndef test_incomplete_env_var_control_plane_client_falls_back_to_profile_with_warning(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\")\n    auth_svc = _set_current_profile(\n        monkeypatch,\n        _profile(\n            api_url=\"https://profile.example.test/\",\n            project_id=\"profile-project\",\n            api_key=\"profile-api-key\",\n        ),\n    )\n\n    client = get_control_plane_client()\n    try:\n        captured = capsys.readouterr()\n        assert client.base_url == \"https://profile.example.test\"\n        assert client.api_key == \"profile-api-key\"\n        assert PARTIAL_ENV_WARNING in captured.err\n        assert \"Using LLAMA_CLOUD_API_KEY from environment\" not in captured.err\n        auth_svc.auth_middleware.assert_called_once_with()\n    finally:\n        _close_client(client)\n\n\ndef test_incomplete_env_var_project_client_without_profile_warns_and_exits(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\")\n    monkeypatch.setattr(client_module, \"is_interactive_session\", lambda: False)\n    _set_current_profile(monkeypatch, None)\n\n    with pytest.raises(click.ClickException, match=\"No profile configured\"):\n        get_project_client()\n\n    captured = capsys.readouterr()\n    assert PARTIAL_ENV_WARNING in captured.err\n\n\ndef test_env_var_project_override_wins_over_env_project_id(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _set_current_profile(monkeypatch, None)\n\n    client = get_project_client(project_id_override=\"flag-project\")\n    try:\n        assert client.project_id == \"flag-project\"\n        assert client.api_key == \"env-api-key\"\n    finally:\n        _close_client(client)\n\n\ndef test_env_var_use_profile_falls_through_to_profile_path(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(\n        monkeypatch,\n        api_key=\"env-api-key\",\n        project_id=\"env-project\",\n        base_url=\"https://env.example.test\",\n        use_profile=True,\n    )\n    auth_svc = _set_current_profile(\n        monkeypatch,\n        _profile(\n            api_url=\"https://profile.example.test\",\n            project_id=\"profile-project\",\n            api_key=\"profile-api-key\",\n        ),\n    )\n\n    client = get_project_client()\n    try:\n        assert client.base_url == \"https://profile.example.test\"\n        assert client.api_key == \"profile-api-key\"\n        assert client.project_id == \"profile-project\"\n        auth_svc.auth_middleware.assert_called_once_with()\n    finally:\n        _close_client(client)\n\n\ndef test_env_var_override_warning_fires_once_with_active_profile(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _set_current_profile(monkeypatch, _profile(api_key=\"profile-api-key\"))\n\n    project_client = get_project_client()\n    control_plane_client = get_control_plane_client()\n    try:\n        captured = capsys.readouterr()\n        assert captured.err.count(OVERRIDE_WARNING) == 1\n    finally:\n        _close_client(project_client)\n        _close_client(control_plane_client)\n\n\ndef test_env_var_override_warning_does_not_fire_without_profile(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _set_current_profile(monkeypatch, None)\n\n    client = get_project_client()\n    try:\n        captured = capsys.readouterr()\n        assert \"Using LLAMA_CLOUD_API_KEY from environment\" not in captured.err\n    finally:\n        _close_client(client)\n\n\ndef test_partial_env_warning_fires_once_across_calls(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\")\n    _set_current_profile(\n        monkeypatch,\n        _profile(\n            api_url=\"https://profile.example.test\",\n            project_id=\"profile-project\",\n            api_key=\"profile-api-key\",\n        ),\n    )\n\n    project_client = get_project_client()\n    control_plane_client = get_control_plane_client()\n    try:\n        captured = capsys.readouterr()\n        assert captured.err.count(PARTIAL_ENV_WARNING) == 1\n    finally:\n        _close_client(project_client)\n        _close_client(control_plane_client)\n\n\ndef test_partial_env_warning_suppressed_under_completion(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", completion=\"zsh_source\")\n    _set_current_profile(\n        monkeypatch,\n        _profile(\n            api_url=\"https://profile.example.test\",\n            project_id=\"profile-project\",\n            api_key=\"profile-api-key\",\n        ),\n    )\n\n    client = get_project_client()\n    try:\n        captured = capsys.readouterr()\n        assert PARTIAL_ENV_WARNING not in captured.err\n    finally:\n        _close_client(client)\n\n\ndef test_env_var_override_warning_does_not_fire_under_completion(\n    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n) -> None:\n    set_llama_cloud_env(\n        monkeypatch,\n        api_key=\"env-api-key\",\n        project_id=\"env-project\",\n        completion=\"zsh_source\",\n    )\n    _set_current_profile(monkeypatch, _profile(api_key=\"profile-api-key\"))\n\n    client = get_project_client()\n    try:\n        captured = capsys.readouterr()\n        assert client.api_key == \"env-api-key\"\n        assert client.project_id == \"env-project\"\n        assert \"Using LLAMA_CLOUD_API_KEY from environment\" not in captured.err\n    finally:\n        _close_client(client)\n"
  },
  {
    "path": "packages/llamactl/tests/test_completion_commands.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom types import SimpleNamespace\nfrom typing import Any\nfrom unittest.mock import MagicMock\n\nimport llama_agents.cli.config.env_service as env_service\nimport llama_agents.cli.param_types as param_types\nimport pytest\nfrom click.testing import CliRunner\nfrom conftest import set_llama_cloud_env\nfrom llama_agents.cli.app import app\nfrom llama_agents.core.client.manage_client import ProjectClient\n\n\ndef _first_matching_line_index(lines: list[str], predicate: str) -> int:\n    for index, line in enumerate(lines):\n        if predicate in line and not line.lstrip().startswith(\"#\"):\n            return index\n    raise AssertionError(f\"Could not find live line containing {predicate!r}\")\n\n\ndef _invoke_zsh_install(home: Path, monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setattr(Path, \"home\", lambda: home)\n    runner = CliRunner()\n    result = runner.invoke(app, [\"completion\", \"install\", \"--shell\", \"zsh\"])\n    assert result.exit_code == 0, result.output\n\n\ndef test_completion_generate_zsh() -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"completion\", \"generate\", \"zsh\"])\n    assert result.exit_code == 0\n    assert \"_LLAMACTL_COMPLETE\" in result.output or \"compdef\" in result.output\n\n\ndef test_completion_generate_bash() -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"completion\", \"generate\", \"bash\"])\n    assert result.exit_code == 0\n    assert \"_LLAMACTL_COMPLETE\" in result.output or \"complete\" in result.output\n\n\ndef test_completion_generate_fish() -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"completion\", \"generate\", \"fish\"])\n    assert result.exit_code == 0\n    assert \"llamactl\" in result.output\n\n\ndef test_completion_generate_invalid_shell() -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"completion\", \"generate\", \"powershell\"])\n    assert result.exit_code != 0\n\n\ndef test_completion_install_dry_run() -> None:\n    runner = CliRunner()\n    result = runner.invoke(\n        app, [\"completion\", \"install\", \"--shell\", \"zsh\", \"--dry-run\"]\n    )\n    assert result.exit_code == 0\n    assert \"would write completion script\" in result.output\n\n\ndef test_completion_install_dry_run_bash() -> None:\n    runner = CliRunner()\n    result = runner.invoke(\n        app, [\"completion\", \"install\", \"--shell\", \"bash\", \"--dry-run\"]\n    )\n    assert result.exit_code == 0\n    assert \"would write completion script\" in result.output\n\n\ndef test_completion_install_dry_run_fish() -> None:\n    runner = CliRunner()\n    result = runner.invoke(\n        app, [\"completion\", \"install\", \"--shell\", \"fish\", \"--dry-run\"]\n    )\n    assert result.exit_code == 0\n    assert \"would write completion script\" in result.output\n\n\ndef test_completion_install_zsh_repairs_ordered_completion_block(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    home = tmp_path / \"home\"\n    home.mkdir()\n    zshrc = home / \".zshrc\"\n    zshrc.write_text(\n        \"autoload -Uz compinit && compinit\\n\"\n        \"fpath=(~/.zfunc $fpath)\\n\"\n        'echo \"custom shell setup\"\\n'\n    )\n\n    _invoke_zsh_install(home, monkeypatch)\n\n    lines = zshrc.read_text().splitlines()\n    fpath_index = _first_matching_line_index(lines, \"~/.zfunc\")\n    compinit_index = _first_matching_line_index(lines, \"compinit\")\n    assert fpath_index < compinit_index\n\n    first_pass = zshrc.read_text()\n    _invoke_zsh_install(home, monkeypatch)\n    assert zshrc.read_text() == first_pass\n\n\ndef test_completion_install_zsh_bootstraps_compinit_when_missing(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    home = tmp_path / \"home\"\n    home.mkdir()\n    zshrc = home / \".zshrc\"\n    zshrc.write_text('export PATH=\"$HOME/bin:$PATH\"\\n')\n\n    _invoke_zsh_install(home, monkeypatch)\n\n    lines = zshrc.read_text().splitlines()\n    fpath_index = _first_matching_line_index(lines, \"~/.zfunc\")\n    compinit_index = _first_matching_line_index(lines, \"compinit\")\n    assert fpath_index < compinit_index\n    live_compinit_lines = [\n        line\n        for line in lines\n        if \"compinit\" in line and not line.lstrip().startswith(\"#\")\n    ]\n    assert len(live_compinit_lines) == 1\n\n\ndef test_completion_group_help() -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"completion\", \"--help\"])\n    assert result.exit_code == 0\n    assert \"generate\" in result.output\n    assert \"install\" in result.output\n\n\ndef test_completion_safe_fetch_handles_env_api_key_without_project_id(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", completion=\"zsh_source\")\n\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.get_current_profile.return_value = None\n    mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n    mock_service = MagicMock()\n    mock_service.current_auth_service.return_value = mock_auth_svc\n    mock_service.get_current_environment.return_value = SimpleNamespace(\n        api_url=\"https://api.cloud.llamaindex.ai\",\n        requires_auth=True,\n    )\n    monkeypatch.setattr(env_service, \"service\", mock_service)\n\n    async def _empty_deployments(self: ProjectClient) -> list[Any]:\n        return []\n\n    monkeypatch.setattr(ProjectClient, \"list_deployments\", _empty_deployments)\n\n    assert param_types._safe_fetch(param_types._fetch_deployments, timeout=1.0) == []\n\n\ndef test_completion_safe_fetch_incomplete_env_auth_uses_active_profile(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", completion=\"zsh_source\")\n\n    profile = SimpleNamespace(\n        api_url=\"https://profile.example.test\",\n        project_id=\"profile-project\",\n        api_key=\"profile-api-key\",\n        device_oidc=None,\n        name=\"prof\",\n    )\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.get_current_profile.return_value = profile\n    mock_auth_svc.list_profiles.return_value = [profile]\n    mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n    mock_auth_svc.auth_middleware.return_value = None\n    mock_service = MagicMock()\n    mock_service.current_auth_service.return_value = mock_auth_svc\n    monkeypatch.setattr(env_service, \"service\", mock_service)\n\n    async def _profile_deployments(self: ProjectClient) -> list[Any]:\n        assert self.base_url == \"https://profile.example.test\"\n        assert self.project_id == \"profile-project\"\n        assert self.api_key == \"profile-api-key\"\n        return [SimpleNamespace(id=\"profile-app\")]\n\n    monkeypatch.setattr(ProjectClient, \"list_deployments\", _profile_deployments)\n\n    completions = param_types._safe_fetch(param_types._fetch_deployments, timeout=1.0)\n\n    assert [item.value for item in completions] == [\"profile-app\"]\n"
  },
  {
    "path": "packages/llamactl/tests/test_config.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for config.py - Database operations and profile management\"\"\"\n\nfrom __future__ import annotations\n\nimport sqlite3\nimport tempfile\nfrom collections.abc import Generator\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.cli.config._config import ConfigManager\n\n\n@pytest.fixture\ndef temp_config() -> Generator[ConfigManager, None, None]:\n    \"\"\"Create a temporary config manager for testing\"\"\"\n    with tempfile.TemporaryDirectory() as temp_dir:\n        config_manager = ConfigManager()\n        # Override the config directory to use temp directory\n        config_manager.config_dir = Path(temp_dir)\n        config_manager.db_path = config_manager.config_dir / \"profiles.db\"\n        config_manager._ensure_config_dir()\n        config_manager._init_database()\n        yield config_manager\n\n\ndef test_create_profile(temp_config: ConfigManager) -> None:\n    \"\"\"Test profile creation with CRUD operations\"\"\"\n    env_url = \"http://localhost:8011\"\n    # Create a profile\n    profile = temp_config.create_profile(\"test\", env_url, \"test-project\")\n\n    assert profile.name == \"test\"\n    assert profile.api_url == env_url\n    assert profile.project_id == \"test-project\"\n\n    # Retrieve the profile\n    retrieved = temp_config.get_profile(\"test\", env_url)\n    assert retrieved is not None\n    assert retrieved.name == \"test\"\n    assert retrieved.api_url == env_url\n    assert retrieved.project_id == \"test-project\"\n\n    # Test duplicate creation fails within the same environment\n    with pytest.raises(ValueError, match=\"Profile 'test' already exists\"):\n        temp_config.create_profile(\"test\", env_url, \"other-project\")\n\n    # Creating a profile with the same name in a different environment should succeed\n    other_env_url = \"http://other:8011\"\n    other_profile = temp_config.create_profile(\"test\", other_env_url, \"other\")\n    assert other_profile.api_url == other_env_url\n\n    # Test profile without project - this should now fail since project_id is required\n    with pytest.raises(ValueError):\n        temp_config.create_profile(\"minimal\", \"http://localhost:8012\", \"\")\n\n    # List profiles - should only have 1 now since the second creation failed\n    profiles = temp_config.list_profiles(env_url)\n    assert len(profiles) == 1\n    assert profiles[0].name == \"test\"\n\n    # Delete profile\n    assert temp_config.delete_profile(\"test\", env_url) is True\n    assert temp_config.get_profile(\"test\", env_url) is None\n    assert temp_config.delete_profile(\"nonexistent\", env_url) is False\n\n\ndef test_profile_migration(temp_config: ConfigManager) -> None:\n    \"\"\"Test migrating from 0001 schema to 0002 (adds id, api_key_id, device_oidc).\"\"\"\n    # Simulate pre-0002 database with 0001 schema\n    with sqlite3.connect(temp_config.db_path) as conn:\n        conn.execute(\"DROP TABLE IF EXISTS profiles\")\n        conn.execute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS profiles (\n                name TEXT NOT NULL,\n                api_url TEXT NOT NULL,\n                project_id TEXT NOT NULL,\n                api_key TEXT,\n                PRIMARY KEY (name, api_url)\n            )\n            \"\"\"\n        )\n        # Insert sample row\n        conn.execute(\n            \"INSERT INTO profiles (name, api_url, project_id, api_key) VALUES (?, ?, ?, ?)\",\n            (\"with-project\", \"http://legacy:8011\", \"legacy-project\", None),\n        )\n        # Mark DB as version 1\n        conn.execute(\"PRAGMA user_version=1\")\n        conn.commit()\n\n    # Trigger migrations (should apply 0002)\n    migrated_config = ConfigManager()\n    migrated_config.config_dir = temp_config.config_dir\n    migrated_config.db_path = temp_config.db_path\n    migrated_config._init_database()\n\n    # Row preserved and retrievable\n    profile = migrated_config.get_profile(\"with-project\", \"http://legacy:8011\")\n    assert profile is not None\n    assert profile.project_id == \"legacy-project\"\n\n    # Schema is updated with new columns\n    with sqlite3.connect(migrated_config.db_path) as conn:\n        cursor = conn.execute(\"PRAGMA table_info(profiles)\")\n        columns = [row[1] for row in cursor.fetchall()]\n        assert \"project_id\" in columns\n        assert \"api_key\" in columns\n        assert \"id\" in columns\n        assert \"api_key_id\" in columns\n        assert \"device_oidc\" in columns\n\n\ndef test_project_management(temp_config: ConfigManager) -> None:\n    \"\"\"Test setting and getting projects\"\"\"\n    env_url = \"http://localhost:8011\"\n    # Create a profile\n    temp_config.create_profile(\"test\", env_url, \"initial-project\")\n\n    # Test initial project\n    prof = temp_config.get_profile(\"test\", env_url)\n    assert prof is not None\n    assert prof.project_id == \"initial-project\"\n\n    # Set new project\n    assert temp_config.set_project(\"test\", env_url, \"new-project\") is True\n    prof = temp_config.get_profile(\"test\", env_url)\n    assert prof is not None\n    assert prof.project_id == \"new-project\"\n\n    # Projects are now required, so we can't set to None\n    # Instead test setting to a different project\n    assert temp_config.set_project(\"test\", env_url, \"another-project\") is True\n    prof = temp_config.get_profile(\"test\", env_url)\n    assert prof is not None\n    assert prof.project_id == \"another-project\"\n\n    # Test with nonexistent profile\n    assert temp_config.set_project(\"nonexistent\", env_url, \"project\") is False\n    assert temp_config.get_profile(\"nonexistent\", env_url) is None\n\n    # Test current profile integration\n    temp_config.set_settings_current_profile(\"test\")\n    current = temp_config.get_current_profile(env_url)\n    assert current is not None\n    assert current.name == \"test\"\n    assert current.project_id == \"another-project\"\n\n    # Set project and verify it's reflected in current profile\n    temp_config.set_project(\"test\", env_url, \"final-project\")\n    current = temp_config.get_current_profile(env_url)\n    assert current is not None\n    assert current.project_id == \"final-project\"\n\n    # Test deleting profile clears current setting\n    temp_config.delete_profile(\"test\", env_url)\n    assert temp_config.get_current_profile(env_url) is None\n\n\ndef test_environments_table_and_default_current_environment(\n    temp_config: ConfigManager,\n) -> None:\n    \"\"\"Fresh DB should have environments table and default current environment set.\"\"\"\n    # Verify settings contains current_environment_api_url\n    with sqlite3.connect(temp_config.db_path) as conn:\n        cursor = conn.execute(\n            \"SELECT value FROM settings WHERE key = 'current_environment_api_url'\"\n        )\n        row = cursor.fetchone()\n        assert row is not None\n        current_env_url = row[0]\n\n        # Verify environments table exists and has the current env row\n        cursor = conn.execute(\n            \"SELECT name FROM sqlite_master WHERE type='table' AND name='environments'\"\n        )\n        assert cursor.fetchone() is not None\n\n        env_cursor = conn.execute(\n            \"SELECT api_url, requires_auth FROM environments WHERE api_url = ?\",\n            (current_env_url,),\n        )\n        env_row = env_cursor.fetchone()\n        assert env_row is not None\n        assert env_row[0] == current_env_url\n        # Default seed uses requires_auth = 0\n        assert env_row[1] in (0, 1)\n\n\ndef test_environment_seed_from_profiles_migration(temp_config: ConfigManager) -> None:\n    \"\"\"Existing DB with 0001-era profiles should seed environments when migrating from version 0.\"\"\"\n    # Prepare DB with distinct profiles and simulate pre-0001 state (no environments, user_version=0)\n    with sqlite3.connect(temp_config.db_path) as conn:\n        # Start from a clean slate: drop tables and set version 0\n        conn.execute(\"DROP TABLE IF EXISTS profiles\")\n        conn.execute(\"DROP TABLE IF EXISTS environments\")\n        conn.execute(\"DROP TABLE IF EXISTS settings\")\n        conn.execute(\"PRAGMA user_version=0\")\n\n        # Create a minimal 0001-style profiles table\n        conn.execute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS profiles (\n                name TEXT NOT NULL,\n                api_url TEXT NOT NULL,\n                project_id TEXT NOT NULL,\n                api_key TEXT,\n                PRIMARY KEY (name, api_url)\n            )\n            \"\"\"\n        )\n        conn.execute(\n            \"INSERT INTO profiles (name, api_url, project_id, api_key) VALUES (?, ?, ?, ?)\",\n            (\"p1\", \"http://env-a:8000\", \"proj-a\", None),\n        )\n        conn.execute(\n            \"INSERT INTO profiles (name, api_url, project_id, api_key) VALUES (?, ?, ?, ?)\",\n            (\"p2\", \"http://env-b:8000\", \"proj-b\", None),\n        )\n        conn.commit()\n\n    # Re-run initialization to trigger 0001 creation and seeding then 0002 migration\n    temp_config._init_database()\n\n    with sqlite3.connect(temp_config.db_path) as conn:\n        # Environments should include both profile envs plus ensure default exists\n        envs = {\n            row[0]\n            for row in conn.execute(\"SELECT api_url FROM environments\").fetchall()\n        }\n        assert \"http://env-a:8000\" in envs\n        assert \"http://env-b:8000\" in envs\n        cur = conn.execute(\n            \"SELECT value FROM settings WHERE key = 'current_environment_api_url'\"\n        ).fetchone()\n        assert cur is not None\n        # setting must be a string; it may or may not be one of the above\n        assert isinstance(cur[0], str)\n\n\ndef test_environment_methods_and_current_behavior(temp_config: ConfigManager) -> None:\n    \"\"\"Validate environment CRUD and get_current_profile preference.\"\"\"\n    # Add a new environment and set requires_auth\n    temp_config.create_or_update_environment(\n        \"http://custom-env:9000\", True, min_llamactl_version=\"0.3.0a13\"\n    )\n    env = temp_config.get_environment(\"http://custom-env:9000\")\n    assert env is not None\n    assert env.api_url == \"http://custom-env:9000\"\n    assert env.requires_auth is True\n    assert env.min_llamactl_version == \"0.3.0a13\"\n\n    # List environments includes the new one\n    envs = temp_config.list_environments()\n    assert any(e.api_url == \"http://custom-env:9000\" for e in envs)\n\n    # Set current environment to a URL with a single profile and verify listing\n    env_only_url = \"http://only-here:7777\"\n    temp_config.create_profile(\"only-here\", env_only_url, \"proj-one\")\n    # Ensure the environment exists first (simulating validated add)\n    temp_config.create_or_update_environment(env_only_url, False)\n    temp_config.set_settings_current_environment(env_only_url)\n    env_profiles = temp_config.list_profiles(env_only_url)\n    assert len(env_profiles) == 1\n    assert env_profiles[0].name == \"only-here\"\n    # Set as current and verify get_current_profile returns it for this env\n    temp_config.set_settings_current_profile(\"only-here\")\n    preferred = temp_config.get_current_profile(env_only_url)\n    assert preferred is not None\n    assert preferred.name == \"only-here\"\n\n\ndef test_config_manager_honors_llamactl_config_dir_override(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    override_dir = tmp_path / \"override-config\"\n    monkeypatch.setenv(\"LLAMACTL_CONFIG_DIR\", str(override_dir))\n\n    cfg = ConfigManager()\n\n    assert cfg.config_dir == override_dir\n    assert cfg.db_path == override_dir / \"profiles.db\"\n    assert cfg.db_path.exists()\n\n\ndef test_config_manager_uses_xdg_config_home_on_unix(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    home_dir = tmp_path / \"home\"\n    xdg_dir = tmp_path / \"xdg-config\"\n    monkeypatch.setenv(\"HOME\", str(home_dir))\n    monkeypatch.delenv(\"LLAMACTL_CONFIG_DIR\", raising=False)\n    monkeypatch.setenv(\"XDG_CONFIG_HOME\", str(xdg_dir))\n\n    cfg = ConfigManager()\n\n    assert cfg.config_dir == xdg_dir / \"llamactl\"\n    assert cfg.db_path == xdg_dir / \"llamactl\" / \"profiles.db\"\n    assert cfg.db_path.exists()\n\n\ndef test_config_manager_defaults_to_dot_config_on_unix(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    home_dir = tmp_path / \"home\"\n    monkeypatch.setenv(\"HOME\", str(home_dir))\n    monkeypatch.delenv(\"LLAMACTL_CONFIG_DIR\", raising=False)\n    monkeypatch.delenv(\"XDG_CONFIG_HOME\", raising=False)\n\n    cfg = ConfigManager()\n\n    assert cfg.config_dir == home_dir / \".config\" / \"llamactl\"\n    assert cfg.db_path == home_dir / \".config\" / \"llamactl\" / \"profiles.db\"\n    assert cfg.db_path.exists()\n"
  },
  {
    "path": "packages/llamactl/tests/test_config_cli.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport json\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock, patch\n\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\n\n\ndef test_config_text_shows_current_context() -> None:\n    runner = CliRunner()\n    profile = SimpleNamespace(name=\"prof\", project_id=\"project-1\")\n    project = SimpleNamespace(project_id=\"project-1\", project_name=\"Production\")\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\n            \"llama_agents.cli.commands.config._list_projects\", return_value=[project]\n        ),\n    ):\n        mock_service.get_current_environment.return_value = SimpleNamespace(\n            api_url=\"https://api.example\"\n        )\n        auth_svc = MagicMock()\n        auth_svc.get_current_profile.return_value = profile\n        mock_service.current_auth_service.return_value = auth_svc\n\n        result = runner.invoke(app, [\"config\"])\n\n    assert result.exit_code == 0, result.output\n    assert \"environment:  https://api.example\" in result.output\n    assert \"profile:      prof\" in result.output\n    assert \"project:      Production (project-1)\" in result.output\n\n\ndef test_config_json_shows_none_values_without_error() -> None:\n    runner = CliRunner()\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.get_current_environment.return_value = SimpleNamespace(\n            api_url=\"https://api.example\"\n        )\n        auth_svc = MagicMock()\n        auth_svc.get_current_profile.return_value = None\n        mock_service.current_auth_service.return_value = auth_svc\n\n        result = runner.invoke(app, [\"config\", \"-o\", \"json\"])\n\n    assert result.exit_code == 0, result.output\n    data = json.loads(result.output)\n    assert data == {\n        \"environment\": \"https://api.example\",\n        \"profile\": None,\n        \"project_id\": None,\n        \"project_name\": None,\n    }\n\n\ndef test_config_does_not_offer_wide_output() -> None:\n    result = CliRunner().invoke(app, [\"config\", \"-o\", \"wide\"])\n    assert result.exit_code != 0\n    assert \"'wide' is not one of 'text', 'json', 'yaml'\" in result.output\n\n\ndef test_config_hidden_debug_commands() -> None:\n    runner = CliRunner()\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.config_manager.return_value.db_path = \"/tmp/llamactl.db\"\n        result = runner.invoke(app, [\"config\", \"show-db\"])\n\n    assert result.exit_code == 0, result.output\n    assert \"/tmp/llamactl.db\" in result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_config_extras.py",
    "content": "from __future__ import annotations\n\nimport sqlite3\nimport tempfile\nfrom pathlib import Path\n\nfrom llama_agents.cli.config._config import ConfigManager\n\n\ndef test_delete_environment_cascades_and_resets_current() -> None:\n    with tempfile.TemporaryDirectory() as temp_dir:\n        cfg = ConfigManager()\n        cfg.config_dir = Path(temp_dir)\n        cfg.db_path = cfg.config_dir / \"profiles.db\"\n        cfg._ensure_config_dir()\n        cfg._init_database()\n\n        url = \"https://env.del.local\"\n        cfg.create_or_update_environment(url, requires_auth=False)\n        cfg.create_profile(\"a\", url, \"p\")\n        cfg.set_settings_current_environment(url)\n        assert cfg.get_current_environment().api_url == url\n\n        deleted = cfg.delete_environment(url)\n        assert deleted is True\n        # Current env should reset to default, profiles removed\n        current = cfg.get_current_environment()\n        assert current.api_url != url\n        assert cfg.list_profiles(url) == []\n\n        # Deleting again should return False\n        assert cfg.delete_environment(url) is False\n\n\ndef test_get_current_environment_fallback_when_missing_row() -> None:\n    with tempfile.TemporaryDirectory() as temp_dir:\n        cfg = ConfigManager()\n        cfg.config_dir = Path(temp_dir)\n        cfg.db_path = cfg.config_dir / \"profiles.db\"\n        cfg._ensure_config_dir()\n        cfg._init_database()\n\n        # Manually set a current env URL without adding an env row\n        missing_url = \"https://missing.local\"\n        with sqlite3.connect(cfg.db_path) as conn:\n            conn.execute(\n                \"INSERT OR REPLACE INTO settings (key, value) VALUES ('current_environment_api_url', ?)\",\n                (missing_url,),\n            )\n            conn.commit()\n\n        env = cfg.get_current_environment()\n        assert env.api_url == missing_url\n        assert env.requires_auth is False\n        assert env.min_llamactl_version is None\n"
  },
  {
    "path": "packages/llamactl/tests/test_deployment_update_refs.py",
    "content": "from __future__ import annotations\n\nimport pytest\nfrom llama_agents.cli.utils.git_push import internal_push_refspec\n\n\ndef test_internal_push_refspec_defaults_to_main() -> None:\n    assert internal_push_refspec(None) == (\"main\", \"refs/heads/main\")\n\n\ndef test_internal_push_refspec_uses_private_pin_for_commit_sha() -> None:\n    sha = \"a\" * 40\n    assert internal_push_refspec(sha) == (sha, f\"refs/llamactl/pins/{sha}\")\n\n\ndef test_internal_push_refspec_prefers_existing_branch(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setattr(\n        \"llama_agents.cli.utils.git_push.git_ref_exists\",\n        lambda ref_name: ref_name == \"refs/heads/release\",\n    )\n    assert internal_push_refspec(\"release\") == (\n        \"refs/heads/release\",\n        \"refs/heads/release\",\n    )\n\n\ndef test_internal_push_refspec_uses_existing_tag_when_branch_missing(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setattr(\n        \"llama_agents.cli.utils.git_push.git_ref_exists\",\n        lambda ref_name: ref_name == \"refs/tags/v1.2.3\",\n    )\n    assert internal_push_refspec(\"v1.2.3\") == (\n        \"refs/tags/v1.2.3\",\n        \"refs/tags/v1.2.3\",\n    )\n\n\ndef test_internal_push_refspec_preserves_explicit_refs() -> None:\n    assert internal_push_refspec(\"refs/tags/v2.0.0\") == (\n        \"refs/tags/v2.0.0\",\n        \"refs/tags/v2.0.0\",\n    )\n"
  },
  {
    "path": "packages/llamactl/tests/test_deployments_apply_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``deployments apply -f`` and ``delete -f`` CLI commands.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport subprocess\nimport textwrap\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\nfrom types import SimpleNamespace\nfrom typing import Any\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport httpx\nimport llama_agents.cli.config.env_service as env_service\nimport pytest\nimport yaml\nfrom click.testing import CliRunner\nfrom conftest import (\n    make_deployment,\n    make_loop_bound_project_client,\n    patch_project_client,\n    set_llama_cloud_env,\n)\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.commands import deployment as deployment_cmd\nfrom llama_agents.core.schema.deployments import DeploymentResponse\nfrom llama_agents.core.schema.git_validation import RepositoryValidationResponse\n\nDEFAULT_BASE_URL = \"https://api.cloud.llamaindex.ai\"\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\n\ndef _http_404(deployment_id: str = \"unknown\") -> httpx.HTTPStatusError:\n    request = httpx.Request(\n        \"GET\", f\"http://test/api/v1beta1/deployments/{deployment_id}\"\n    )\n    response = httpx.Response(404, request=request, text='{\"detail\":\"not found\"}')\n    return httpx.HTTPStatusError(\"HTTP 404\", request=request, response=response)\n\n\ndef _http_409() -> httpx.HTTPStatusError:\n    request = httpx.Request(\"POST\", \"http://test/api/v1beta1/deployments\")\n    response = httpx.Response(\n        409, request=request, text='{\"detail\":\"conflict: deployment already exists\"}'\n    )\n    return httpx.HTTPStatusError(\"HTTP 409\", request=request, response=response)\n\n\ndef _http_422_detail(detail: list[dict[str, Any]]) -> httpx.HTTPStatusError:\n    request = httpx.Request(\"POST\", \"http://test/api/v1beta1/deployments\")\n    response = httpx.Response(422, request=request, json={\"detail\": detail})\n    return httpx.HTTPStatusError(\"HTTP 422\", request=request, response=response)\n\n\ndef _apply_client_mock(\n    *,\n    existing: DeploymentResponse | None = None,\n    created: DeploymentResponse | None = None,\n    validate_accessible: bool = True,\n) -> MagicMock:\n    \"\"\"Mock client for apply tests.\"\"\"\n    client = MagicMock()\n    client.project_id = \"proj_default\"\n    client.base_url = \"http://test:8011\"\n    client.api_key = \"profile-client-key\"\n\n    if existing:\n\n        async def _get(\n            deployment_id: str, include_events: bool = False\n        ) -> DeploymentResponse:\n            if deployment_id == existing.id:\n                return existing\n            raise _http_404(deployment_id)\n\n        client.get_deployment = AsyncMock(side_effect=_get)\n    else:\n        client.get_deployment = AsyncMock(\n            side_effect=lambda *a, **kw: (_ for _ in ()).throw(_http_404())\n        )\n\n    if created:\n        client.create_deployment = AsyncMock(return_value=created)\n    else:\n        client.create_deployment = AsyncMock(return_value=make_deployment(\"new-app\"))\n\n    client.update_deployment = AsyncMock(\n        return_value=existing or make_deployment(\"my-app\")\n    )\n\n    async def _validate(\n        repo_url: str,\n        deployment_id: str | None = None,\n        pat: str | None = None,\n    ) -> RepositoryValidationResponse:\n        return RepositoryValidationResponse(\n            accessible=validate_accessible,\n            message=\"ok\" if validate_accessible else \"repo not found\",\n        )\n\n    client.validate_repository = AsyncMock(side_effect=_validate)\n    client.delete_deployment = AsyncMock()\n\n    return client\n\n\ndef _patch_no_profile_auth(monkeypatch: pytest.MonkeyPatch) -> None:\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.get_current_profile.return_value = None\n    mock_auth_svc.list_profiles.return_value = []\n    mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n    mock_auth_svc.auth_middleware.return_value = None\n\n    mock_service = MagicMock()\n    mock_service.current_auth_service.return_value = mock_auth_svc\n    mock_service.get_current_environment.return_value = SimpleNamespace(\n        api_url=DEFAULT_BASE_URL,\n        requires_auth=True,\n    )\n    monkeypatch.setattr(env_service, \"service\", mock_service)\n\n\nMINIMAL_CREATE_YAML = textwrap.dedent(\"\"\"\\\n    name: new-app\n    generate_name: New App\n    spec:\n      repo_url: https://github.com/example/repo\n\"\"\")\n\nMINIMAL_UPDATE_YAML = textwrap.dedent(\"\"\"\\\n    name: my-app\n    spec:\n      git_ref: v2\n\"\"\")\n\nGITHUB_APP_INSTALL_URL = (\n    \"https://github.com/apps/llamaindex/installations/new/permissions?target_id=42\"\n)\nGITHUB_APP_SETTINGS_URL = \"https://github.com/settings/installations/42\"\nGITHUB_APP_AUTHORIZATION_URL = (\n    \"https://api.example.test/api/internal/external-credentials/github-app/connect\"\n)\n\n\ndef _repository_validation_response(\n    *,\n    accessible: bool,\n    message: str,\n    github_app_installation_url: str | None = None,\n    github_app_settings_url: str | None = None,\n    github_app_authorization_url: str | None = None,\n) -> RepositoryValidationResponse:\n    return RepositoryValidationResponse(\n        accessible=accessible,\n        message=message,\n        github_app_name=\"llamaindex\",\n        github_app_installation_url=github_app_installation_url,\n        github_app_settings_url=github_app_settings_url,\n        github_app_authorization_url=github_app_authorization_url,\n    )\n\n\nclass _FakeGitHubCallbackServer:\n    async def start(self) -> None:\n        pass\n\n    async def wait(self, timeout: float) -> None:\n        pass\n\n    async def stop(self) -> None:\n        pass\n\n\ndef test_apply_creates_when_not_found(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(created=make_deployment(\"new-app\"))\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.create_deployment.assert_called_once()\n    assert \"created\" in result.output.lower()\n    assert \"new-app\" in result.output\n\n\ndef test_apply_uses_complete_env_auth_without_profile(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Any\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _patch_no_profile_auth(monkeypatch)\n\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(created=make_deployment(\"new-app\"))\n    client.project_id = \"env-project\"\n    client.base_url = DEFAULT_BASE_URL\n    client.api_key = \"env-api-key\"\n    with patch_project_client(client) as ctor:\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert \"created new-app\" in result.output\n    args, _ = ctor.call_args\n    assert args == (DEFAULT_BASE_URL, \"env-project\", \"env-api-key\", None)\n\n\ndef test_apply_updates_when_exists(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_UPDATE_YAML)\n\n    existing = make_deployment(\"my-app\")\n    client = _apply_client_mock(existing=existing)\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.update_deployment.assert_called_once()\n    call_args = client.update_deployment.call_args\n    assert call_args[0][0] == \"my-app\"\n    assert \"updated\" in result.output.lower()\n    assert \"my-app\" in result.output\n\n\ndef test_apply_update_uses_one_event_loop(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_UPDATE_YAML)\n\n    client = make_loop_bound_project_client(existing=make_deployment(\"my-app\"))\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.update_deployment.assert_called_once()\n    assert \"updated\" in result.output.lower()\n\n\ndef test_apply_reads_stdin(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client = _apply_client_mock(created=make_deployment(\"new-app\"))\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"apply\", \"-f\", \"-\"],\n            input=MINIMAL_CREATE_YAML,\n        )\n\n    assert result.exit_code == 0, result.output\n    client.create_deployment.assert_called_once()\n    assert \"new-app\" in result.output\n\n\ndef test_apply_generate_name_only(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        generate_name: My App\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    server_returned = make_deployment(\"my-app-xyz\", display_name=\"My App\")\n    client = _apply_client_mock(created=server_returned)\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.create_deployment.assert_called_once()\n    create_payload = client.create_deployment.call_args[0][0]\n    assert create_payload.id is None\n    # The server-assigned id should appear in output.\n    assert \"my-app-xyz\" in result.output\n\n\ndef test_apply_409_surfaces_error(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock()\n    client.create_deployment = AsyncMock(side_effect=_http_409())\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    # HTTPStatusError is re-raised directly; the 409 info surfaces either\n    # in output (if Click wraps it) or in the exception object.\n    assert \"409\" in result.output or (\n        result.exception is not None and \"409\" in str(result.exception)\n    )\n\n\ndef test_apply_dry_run_named(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f), \"--dry-run\"])\n\n    assert result.exit_code == 0, result.output\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n    assert \"would upsert\" not in result.stdout\n    assert \"would upsert deployment 'new-app'\" in result.stderr\n    assert (\n        yaml.safe_load(result.stdout)[\"repo_url\"] == \"https://github.com/example/repo\"\n    )\n    assert \"would\" in result.output.lower()\n    assert \"upsert\" in result.output.lower()\n\n\ndef test_apply_dry_run_generate_name_only(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        generate_name: My App\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f), \"--dry-run\"])\n\n    assert result.exit_code == 0, result.output\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n    assert \"would\" in result.output.lower()\n    assert \"create\" in result.output.lower()\n\n\ndef test_apply_dry_run_validates_appserver_version(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        generate_name: My App\n        spec:\n          repo_url: https://github.com/example/repo\n          appserver_version: \"1!0.11.3\"\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f), \"--dry-run\"])\n\n    assert result.exit_code != 0\n    assert \"invalid appserver_version\" in result.output\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n    client.validate_repository.assert_not_called()\n\n\ndef test_apply_dry_run_masks_resolved_secret_values(\n    patched_auth: Any, tmp_path: Any, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.setenv(\"API_KEY_FOR_DRY_RUN\", \"sk-test-secret\")\n    monkeypatch.setenv(\"PAT_FOR_DRY_RUN\", \"ghp-test-secret\")\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        generate_name: My App\n        spec:\n          repo_url: https://github.com/example/repo\n          secrets:\n            API_KEY: ${API_KEY_FOR_DRY_RUN}\n          personal_access_token: ${PAT_FOR_DRY_RUN}\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f), \"--dry-run\"])\n\n    assert result.exit_code == 0, result.output\n    client.create_deployment.assert_not_called()\n    assert \"sk-test-secret\" not in result.output\n    assert \"ghp-test-secret\" not in result.output\n    assert \"API_KEY: '********'\" in result.output\n    assert \"personal_access_token: '********'\" in result.output\n\n\ndef test_apply_no_name_no_generate_name_errors(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    output_lower = result.output.lower()\n    assert \"name\" in output_lower or \"generate_name\" in output_lower\n\n\ndef test_apply_name_without_generate_name_creates_when_missing(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        name: new-app\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()  # get_deployment raises 404\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    payload = client.create_deployment.call_args[0][0]\n    assert payload.id == \"new-app\"\n    assert payload.display_name == \"new-app\"\n    assert \"created new-app\" in result.output\n\n\ndef test_apply_validate_repository_blocks_create(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(validate_accessible=False)\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_interactive_inaccessible_github_repo_installs_app_and_retries(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(created=make_deployment(\"new-app\"))\n    client.validate_repository = AsyncMock(\n        side_effect=[\n            _repository_validation_response(\n                accessible=False,\n                message=\"GitHub App does not have access\",\n                github_app_installation_url=GITHUB_APP_INSTALL_URL,\n            ),\n            _repository_validation_response(accessible=True, message=\"ok\"),\n        ]\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_interactive_session\", return_value=True),\n        patch(\"webbrowser.open\") as mock_open,\n        patch(f\"{_DEPLOY_CMD}._GitHubCallbackServer\", _FakeGitHubCallbackServer),\n        patch(\n            f\"{_DEPLOY_CMD}._wait_for_callback_or_interval\", new_callable=AsyncMock\n        ) as mock_wait,\n    ):\n        mock_wait.return_value = False\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    mock_open.assert_called_once_with(GITHUB_APP_INSTALL_URL)\n    assert f\"Open this URL: {GITHUB_APP_INSTALL_URL}\" in result.output\n    mock_wait.assert_awaited_once()\n    assert client.validate_repository.await_count == 2\n    client.create_deployment.assert_called_once()\n    assert \"created new-app\" in result.output\n\n\ndef test_apply_non_interactive_inaccessible_github_repo_error_includes_install_url(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock()\n    client.validate_repository = AsyncMock(\n        return_value=_repository_validation_response(\n            accessible=False,\n            message=\"GitHub App does not have access\",\n            github_app_installation_url=GITHUB_APP_INSTALL_URL,\n        )\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_interactive_session\", return_value=False),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"GitHub App does not have access\" in result.output\n    assert GITHUB_APP_INSTALL_URL in result.output\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_inaccessible_github_repo_without_install_url_preserves_error(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock()\n    client.validate_repository = AsyncMock(\n        return_value=_repository_validation_response(\n            accessible=False,\n            message=\"repo not found\",\n        )\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_interactive_session\", return_value=True),\n        patch(\"webbrowser.open\") as mock_open,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"repo not found\" in result.output\n    assert \"Install the GitHub App\" not in result.output\n    mock_open.assert_not_called()\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_interactive_github_app_settings_url_preferred_over_install_url(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(created=make_deployment(\"new-app\"))\n    client.validate_repository = AsyncMock(\n        side_effect=[\n            _repository_validation_response(\n                accessible=False,\n                message=\"GitHub App does not have access\",\n                github_app_installation_url=GITHUB_APP_INSTALL_URL,\n                github_app_settings_url=GITHUB_APP_SETTINGS_URL,\n            ),\n            _repository_validation_response(accessible=True, message=\"ok\"),\n        ]\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_interactive_session\", return_value=True),\n        patch(\"webbrowser.open\") as mock_open,\n        patch(f\"{_DEPLOY_CMD}._GitHubCallbackServer\", _FakeGitHubCallbackServer),\n        patch(\n            f\"{_DEPLOY_CMD}._wait_for_callback_or_interval\", new_callable=AsyncMock\n        ) as mock_wait,\n    ):\n        mock_wait.return_value = False\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    mock_open.assert_called_once_with(GITHUB_APP_SETTINGS_URL)\n    client.create_deployment.assert_called_once()\n\n\ndef test_apply_interactive_github_authorization_runs_before_install(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(created=make_deployment(\"new-app\"))\n    client.validate_repository = AsyncMock(\n        side_effect=[\n            _repository_validation_response(\n                accessible=False,\n                message=\"GitHub user authorization required\",\n                github_app_authorization_url=GITHUB_APP_AUTHORIZATION_URL,\n            ),\n            _repository_validation_response(\n                accessible=False,\n                message=\"GitHub App does not have access\",\n                github_app_installation_url=GITHUB_APP_INSTALL_URL,\n            ),\n            _repository_validation_response(accessible=True, message=\"ok\"),\n        ]\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_interactive_session\", return_value=True),\n        patch(\"webbrowser.open\") as mock_open,\n        patch(f\"{_DEPLOY_CMD}._GitHubCallbackServer\", _FakeGitHubCallbackServer),\n        patch(\n            f\"{_DEPLOY_CMD}._wait_for_callback_or_interval\", new_callable=AsyncMock\n        ) as mock_wait,\n    ):\n        mock_wait.side_effect = [True, False]\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert [call.args[0] for call in mock_open.call_args_list] == [\n        GITHUB_APP_AUTHORIZATION_URL,\n        GITHUB_APP_INSTALL_URL,\n    ]\n    assert f\"Open this URL: {GITHUB_APP_AUTHORIZATION_URL}\" in result.output\n    assert f\"Open this URL: {GITHUB_APP_INSTALL_URL}\" in result.output\n    assert client.validate_repository.await_count == 3\n    client.create_deployment.assert_called_once()\n\n\ndef test_apply_interactive_legacy_connect_url_treated_as_authorization(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(created=make_deployment(\"new-app\"))\n    client.validate_repository = AsyncMock(\n        side_effect=[\n            _repository_validation_response(\n                accessible=False,\n                message=\"GitHub user authorization required\",\n                github_app_installation_url=GITHUB_APP_AUTHORIZATION_URL,\n            ),\n            _repository_validation_response(accessible=True, message=\"ok\"),\n        ]\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_interactive_session\", return_value=True),\n        patch(\"webbrowser.open\") as mock_open,\n        patch(f\"{_DEPLOY_CMD}._GitHubCallbackServer\", _FakeGitHubCallbackServer),\n        patch(\n            f\"{_DEPLOY_CMD}._wait_for_callback_or_interval\", new_callable=AsyncMock\n        ) as mock_wait,\n    ):\n        mock_wait.return_value = True\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    mock_open.assert_called_once_with(GITHUB_APP_AUTHORIZATION_URL)\n    assert \"Opening browser to connect GitHub\" in result.output\n    assert \"Opening browser to install the GitHub App\" not in result.output\n    client.create_deployment.assert_called_once()\n\n\nasync def test_github_app_poll_cancelled_error_exits() -> None:\n    client = MagicMock()\n    client.validate_repository = AsyncMock()\n    vr = _repository_validation_response(\n        accessible=False,\n        message=\"GitHub App does not have access\",\n        github_app_installation_url=GITHUB_APP_INSTALL_URL,\n    )\n\n    with (\n        pytest.raises(asyncio.CancelledError),\n        patch(\"webbrowser.open\") as mock_open,\n        patch(f\"{_DEPLOY_CMD}._GitHubCallbackServer\", _FakeGitHubCallbackServer),\n        patch(\n            f\"{_DEPLOY_CMD}._wait_for_callback_or_interval\", new_callable=AsyncMock\n        ) as mock_wait,\n    ):\n        mock_wait.side_effect = asyncio.CancelledError\n        await deployment_cmd._resolve_github_app_access(\n            client,\n            vr,\n            \"https://github.com/example/repo\",\n            None,\n            None,\n        )\n\n    mock_open.assert_called_once_with(GITHUB_APP_INSTALL_URL)\n    client.validate_repository.assert_not_called()\n\n\ndef test_apply_validates_payload_before_repository(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        generate_name: My App\n        spec:\n          repo_url: https://github.com/example/repo\n          appserver_version: \"1!0.11.3\"\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock(validate_accessible=False)\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"invalid appserver_version\" in result.output\n    client.validate_repository.assert_not_called()\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_push_mode_skips_validate_repository(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        generate_name: My App\n        spec:\n          repo_url: \"\"\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client), _patched_git_push():\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.validate_repository.assert_not_called()\n\n\ndef test_apply_env_var_resolves(\n    patched_auth: Any, tmp_path: Any, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.setenv(\"MY_REPO_URL\", \"https://github.com/env-resolved/repo\")\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        name: env-app\n        generate_name: Env App\n        spec:\n          repo_url: ${MY_REPO_URL}\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock(\n        created=make_deployment(\n            \"env-app\", repo_url=\"https://github.com/env-resolved/repo\"\n        )\n    )\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    # The resolved URL should have been passed to the client.\n    create_payload = client.create_deployment.call_args[0][0]\n    assert create_payload.repo_url == \"https://github.com/env-resolved/repo\"\n\n\ndef test_apply_unresolved_env_var_errors(\n    patched_auth: Any, tmp_path: Any, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.delenv(\"NONEXISTENT_VAR_FOR_TEST\", raising=False)\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        name: env-app\n        generate_name: Env App\n        spec:\n          repo_url: ${NONEXISTENT_VAR_FOR_TEST}\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"NONEXISTENT_VAR_FOR_TEST\" in result.output\n\n\ndef test_apply_annotate_parse_error_rewrites_file(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  bogus: nope\\n\")\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app, [\"deployments\", \"apply\", \"-f\", str(f), \"--annotate-on-error\"]\n        )\n\n    assert result.exit_code == 1\n    assert \"apply failed; see annotations\" in result.output\n    assert \"## ERROR: spec.bogus: Extra inputs are not permitted\" in f.read_text()\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_annotate_unresolved_env_var_rewrites_file(\n    patched_auth: Any, tmp_path: Any, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.delenv(\"GITHUB_TOKEN_FOR_ANNOTATION\", raising=False)\n    monkeypatch.delenv(\"OPENAI_KEY_FOR_ANNOTATION\", raising=False)\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\n        textwrap.dedent(\"\"\"\\\n            generate_name: Env App\n            spec:\n              repo_url: https://github.com/example/repo\n              personal_access_token: ${GITHUB_TOKEN_FOR_ANNOTATION}\n              secrets:\n                OPENAI_API_KEY: ${OPENAI_KEY_FOR_ANNOTATION}\n        \"\"\")\n    )\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app, [\"deployments\", \"apply\", \"-f\", str(f), \"--annotate-on-error\"]\n        )\n\n    assert result.exit_code == 1\n    annotated = f.read_text()\n    assert (\n        \"  ## ERROR: unresolved environment variables: \"\n        \"GITHUB_TOKEN_FOR_ANNOTATION\\n\"\n        \"  personal_access_token:\"\n    ) in annotated\n    assert (\n        \"    ## ERROR: unresolved environment variables: \"\n        \"OPENAI_KEY_FOR_ANNOTATION\\n\"\n        \"    OPENAI_API_KEY:\"\n    ) in annotated\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_annotate_stdin_writes_yaml_to_stdout(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"apply\", \"-f\", \"-\", \"--annotate-on-error\"],\n            input=\"name: new-app\\nspec: {}\\n\",\n        )\n\n    assert result.exit_code == 1\n    assert \"## ERROR: spec.repo_url: set spec.repo_url for create\" in result.output\n    assert \"repo_url\" in result.output\n\n\ndef test_apply_annotate_dry_run_is_non_mutating(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    original = \"spec:\\n  repo_url: https://github.com/example/repo\\n\"\n    f.write_text(original)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\n                \"deployments\",\n                \"apply\",\n                \"-f\",\n                str(f),\n                \"--dry-run\",\n                \"--annotate-on-error\",\n            ],\n        )\n\n    assert result.exit_code != 0\n    assert f.read_text() == original\n    assert \"generate_name\" in result.output\n\n\ndef test_apply_annotate_repository_failure_targets_repo_url(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock(validate_accessible=False)\n    with patch_project_client(client):\n        result = runner.invoke(\n            app, [\"deployments\", \"apply\", \"-f\", str(f), \"--annotate-on-error\"]\n        )\n\n    assert result.exit_code == 1\n    assert \"  ## ERROR: repo not found\\n  repo_url:\" in f.read_text()\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_annotate_server_validation_remaps_body_loc(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(MINIMAL_CREATE_YAML)\n\n    client = _apply_client_mock()\n    client.create_deployment = AsyncMock(\n        side_effect=_http_422_detail(\n            [\n                {\n                    \"loc\": [\"body\", \"repo_url\"],\n                    \"msg\": \"invalid repository URL\",\n                    \"type\": \"value_error\",\n                }\n            ]\n        )\n    )\n    with patch_project_client(client):\n        result = runner.invoke(\n            app, [\"deployments\", \"apply\", \"-f\", str(f), \"--annotate-on-error\"]\n        )\n\n    assert result.exit_code == 1\n    assert \"  ## ERROR: invalid repository URL\\n  repo_url:\" in f.read_text()\n\n\ndef test_apply_annotate_create_secret_null_targets_secret(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\n        textwrap.dedent(\"\"\"\\\n            generate_name: My App\n            spec:\n              repo_url: https://github.com/example/repo\n              secrets:\n                DELETE_ME: null\n        \"\"\")\n    )\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app, [\"deployments\", \"apply\", \"-f\", str(f), \"--annotate-on-error\"]\n        )\n\n    assert result.exit_code == 1\n    assert \"    ## ERROR: cannot delete secrets on create\" in f.read_text()\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_annotate_invalid_appserver_version_targets_field(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\n        textwrap.dedent(\"\"\"\\\n            generate_name: My App\n            spec:\n              repo_url: \"\"\n              appserver_version: tilt-dev\n        \"\"\")\n    )\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app, [\"deployments\", \"apply\", \"-f\", str(f), \"--annotate-on-error\"]\n        )\n\n    assert result.exit_code == 1\n    assert \"  ## ERROR: invalid appserver_version\" in f.read_text()\n    assert \"  appserver_version: tilt-dev\" in f.read_text()\n    client.create_deployment.assert_not_called()\n\n\ndef test_apply_annotate_save_then_push_failure_preserves_recovery(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"generate_name: My App\\nspec:\\n  repo_url: ''\\n\")\n\n    client = _apply_client_mock()\n    with (\n        patch_project_client(client),\n        _patched_git_push(returncode=1, stderr=b\"auth failed\"),\n    ):\n        result = runner.invoke(\n            app, [\"deployments\", \"apply\", \"-f\", str(f), \"--annotate-on-error\"]\n        )\n\n    assert result.exit_code == 1\n    annotated = f.read_text()\n    assert \"push failed: auth failed\" in annotated\n    assert \"re-run `llamactl deployments apply -f <file>`\" in annotated\n    assert \"created new-app\" not in annotated\n\n\ndef test_delete_from_file(patched_auth: Any, tmp_path: Any) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        name: doomed-app\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"delete\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"deleted doomed-app\" in result.stderr\n    client.delete_deployment.assert_called_once()\n    call_args = client.delete_deployment.call_args\n    assert call_args[0][0] == \"doomed-app\"\n\n\ndef test_delete_requires_name_or_file(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client = _apply_client_mock()\n    client.list_deployments = AsyncMock(\n        return_value=[make_deployment(\"my-app\"), make_deployment(\"other\")]\n    )\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"delete\"])\n    assert result.exit_code != 0\n    assert \"llamactl deployments delete <deployment_id>\" in result.output\n    assert \"my-app\" in result.output\n\n\ndef test_delete_file_and_positional_mutually_exclusive(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    yaml_text = \"name: my-app\\nspec: {}\\n\"\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(yaml_text)\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"delete\", \"my-app\", \"-f\", str(f)],\n        )\n\n    assert result.exit_code != 0\n    assert \"mutually exclusive\" in result.output.lower()\n\n\ndef test_delete_reads_stdin(patched_auth: Any) -> None:\n    runner = CliRunner()\n    yaml_text = textwrap.dedent(\"\"\"\\\n        name: stdin-app\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n\n    client = _apply_client_mock()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"delete\", \"-f\", \"-\"],\n            input=yaml_text,\n        )\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"deleted stdin-app\" in result.stderr\n    client.delete_deployment.assert_called_once()\n    call_args = client.delete_deployment.call_args\n    assert call_args[0][0] == \"stdin-app\"\n\n\ndef _push_mode_client(\n    *,\n    existing_repo_url: str = \"internal://\",\n    deployment_id: str = \"my-app\",\n) -> MagicMock:\n    \"\"\"Client mock for a push-mode deployment.\"\"\"\n    existing = make_deployment(deployment_id, repo_url=existing_repo_url)\n    client = _apply_client_mock(existing=existing)\n    client.update_deployment = AsyncMock(return_value=existing)\n    return client\n\n\n_DEPLOY_CMD = \"llama_agents.cli.commands.deployment\"\n\n\n@contextmanager\ndef _patched_git_push(\n    *, returncode: int = 0, stderr: bytes = b\"\"\n) -> Generator[MagicMock, None, None]:\n    \"\"\"Patch git-push helpers so push-mode tests don't hit real git.\n\n    Yields the ``push_to_remote`` mock for assertions.\n    \"\"\"\n    with (\n        patch(f\"{_DEPLOY_CMD}.has_deployment_git_remote\", return_value=True),\n        patch(f\"{_DEPLOY_CMD}.configure_git_remote\", return_value=\"llamaagents-test\"),\n        patch(\n            f\"{_DEPLOY_CMD}.push_to_remote\",\n            return_value=subprocess.CompletedProcess([], returncode, stderr=stderr),\n        ) as mock_push,\n    ):\n        yield mock_push\n\n\ndef test_configure_git_remote_uses_profile_project_client_api_key(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    client = _apply_client_mock()\n    client.api_key = \"profile-client-key\"\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(\n            f\"{_DEPLOY_CMD}.configure_git_remote\", return_value=\"llamaagents-my-app\"\n        ) as mock_configure,\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"deployments\",\n                \"configure-git-remote\",\n                \"my-app\",\n            ],\n        )\n\n    assert result.exit_code == 0, result.output\n    mock_configure.assert_called_once_with(\n        \"http://test:8011/api/v1beta1/deployments/my-app/git\",\n        \"profile-client-key\",\n        \"proj_default\",\n        \"my-app\",\n    )\n\n\ndef test_configure_git_remote_uses_env_project_client_api_key(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _patch_no_profile_auth(monkeypatch)\n\n    runner = CliRunner()\n    client = _apply_client_mock()\n    client.project_id = \"env-project\"\n    client.base_url = DEFAULT_BASE_URL\n    client.api_key = \"env-api-key\"\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(\n            f\"{_DEPLOY_CMD}.configure_git_remote\", return_value=\"llamaagents-my-app\"\n        ) as mock_configure,\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"deployments\",\n                \"configure-git-remote\",\n                \"my-app\",\n            ],\n        )\n\n    assert result.exit_code == 0, result.output\n    mock_configure.assert_called_once_with(\n        f\"{DEFAULT_BASE_URL}/api/v1beta1/deployments/my-app/git\",\n        \"env-api-key\",\n        \"env-project\",\n        \"my-app\",\n    )\n\n\ndef test_apply_push_mode_uses_selected_project_client_api_key(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"generate_name: My App\\nspec:\\n  repo_url: ''\\n\")\n\n    client = _apply_client_mock()\n    client.api_key = \"profile-client-key\"\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(\n            f\"{_DEPLOY_CMD}.configure_git_remote\", return_value=\"llamaagents-new-app\"\n        ) as mock_configure,\n        patch(\n            f\"{_DEPLOY_CMD}.push_to_remote\",\n            return_value=subprocess.CompletedProcess([], 0, stderr=b\"\"),\n        ),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    mock_configure.assert_called_once_with(\n        \"http://test:8011/api/v1beta1/deployments/new-app/git\",\n        \"profile-client-key\",\n        \"proj_default\",\n        \"new-app\",\n    )\n\n\ndef test_apply_push_mode_uses_env_project_client_api_key(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Any\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _patch_no_profile_auth(monkeypatch)\n\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"generate_name: My App\\nspec:\\n  repo_url: ''\\n\")\n\n    client = _apply_client_mock()\n    client.project_id = \"env-project\"\n    client.base_url = DEFAULT_BASE_URL\n    client.api_key = \"env-api-key\"\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(\n            f\"{_DEPLOY_CMD}.configure_git_remote\", return_value=\"llamaagents-new-app\"\n        ) as mock_configure,\n        patch(\n            f\"{_DEPLOY_CMD}.push_to_remote\",\n            return_value=subprocess.CompletedProcess([], 0, stderr=b\"\"),\n        ),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    mock_configure.assert_called_once_with(\n        f\"{DEFAULT_BASE_URL}/api/v1beta1/deployments/new-app/git\",\n        \"env-api-key\",\n        \"env-project\",\n        \"new-app\",\n    )\n\n\ndef test_apply_push_mode_create_does_save_then_push(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"Create with repo_url=\"\" → save first (POST), then push.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"generate_name: My App\\nspec:\\n  repo_url: ''\\n  git_ref: main\\n\")\n\n    client = _apply_client_mock()\n    with patch_project_client(client), _patched_git_push() as mock_push:\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert \"created new-app\" in result.output\n    client.create_deployment.assert_called_once()\n    mock_push.assert_called_once()\n\n\ndef test_apply_push_mode_update_does_push_then_save(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"Existing push-mode + linked repo → push first, then save.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: feature-branch\\n\")\n\n    client = _push_mode_client()\n    with patch_project_client(client), _patched_git_push() as mock_push:\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"updated my-app\" in result.output\n    mock_push.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_apply_push_mode_update_skips_push_when_remote_missing(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"Existing push-mode + unlinked repo → save without pushing.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: feature-branch\\n\")\n\n    client = _push_mode_client()\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(f\"{_DEPLOY_CMD}.has_deployment_git_remote\", return_value=False),\n        patch(f\"{_DEPLOY_CMD}.configure_git_remote\") as configure,\n        patch(f\"{_DEPLOY_CMD}.push_to_remote\") as push,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert \"updated my-app\" in result.output\n    assert \"warning: not pushing code; no llamaagents-my-app remote\" in result.stderr\n    configure.assert_not_called()\n    push.assert_not_called()\n    client.update_deployment.assert_called_once()\n\n\ndef test_apply_push_mode_update_push_flag_forces_push(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"``--push`` configures and pushes even when the remote is missing.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: feature-branch\\n\")\n\n    client = _push_mode_client()\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(f\"{_DEPLOY_CMD}.has_deployment_git_remote\", return_value=False),\n        patch(f\"{_DEPLOY_CMD}.configure_git_remote\", return_value=\"llamaagents-test\"),\n        patch(\n            f\"{_DEPLOY_CMD}.push_to_remote\",\n            return_value=subprocess.CompletedProcess([], 0, stderr=b\"\"),\n        ) as push,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f), \"--push\"])\n\n    assert result.exit_code == 0, result.output\n    assert \"updated my-app\" in result.output\n    push.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_apply_push_and_no_push_are_mutually_exclusive(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: feature-branch\\n\")\n\n    client = _push_mode_client()\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"apply\", \"-f\", str(f), \"--push\", \"--no-push\"],\n        )\n\n    assert result.exit_code != 0\n    assert \"--push and --no-push are mutually exclusive\" in result.output\n    client.get_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n\n\ndef test_apply_push_then_save_push_failure_aborts(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"Push-then-save: if push fails, update must NOT be called.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: main\\n\")\n\n    client = _push_mode_client()\n    with (\n        patch_project_client(client),\n        _patched_git_push(returncode=1, stderr=b\"push rejected\"),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"push failed\" in result.output.lower() or \"push rejected\" in result.output\n    client.update_deployment.assert_not_called()\n\n\ndef test_apply_save_then_push_push_failure_shows_recovery(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"Save-then-push: if push fails after save, show recovery hint.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"generate_name: My App\\nspec:\\n  repo_url: ''\\n\")\n\n    client = _apply_client_mock()\n    with (\n        patch_project_client(client),\n        _patched_git_push(returncode=1, stderr=b\"auth failed\"),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"created new-app\" in result.output\n    assert \"re-run\" in result.output.lower()\n\n\ndef test_apply_external_to_push_mode_does_save_then_push(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"Switching from external repo to push-mode → save then push.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  repo_url: ''\\n\")\n\n    client = _push_mode_client(existing_repo_url=\"https://github.com/org/repo\")\n    with patch_project_client(client), _patched_git_push() as mock_push:\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"updated my-app\" in result.output\n    client.update_deployment.assert_called_once()\n    mock_push.assert_called_once()\n\n\ndef test_apply_push_to_external_does_save_only(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"Switching from push-mode to external → save only, no push.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  repo_url: https://github.com/org/new-repo\\n\")\n\n    client = _push_mode_client()\n    with patch_project_client(client), _patched_git_push() as mock_push:\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"updated my-app\" in result.output\n    mock_push.assert_not_called()\n\n\ndef test_apply_internal_scheme_roundtrip_pushes(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"``get -o template`` emits ``repo_url: internal://``; re-applying that\n    YAML should treat it as push-mode and push-then-save.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  repo_url: 'internal://'\\n  git_ref: main\\n\")\n\n    client = _push_mode_client()\n    with patch_project_client(client), _patched_git_push() as mock_push:\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"updated my-app\" in result.output\n    mock_push.assert_called_once()\n    client.validate_repository.assert_not_called()\n\n\ndef test_apply_internal_scheme_skips_push_when_not_in_git_repo(\n    patched_auth: Any, tmp_path: Any\n) -> None:\n    \"\"\"When not in a git repo, push-mode apply should skip the push\n    and still succeed (spec-only update).\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  suspended: true\\n\")\n\n    client = _push_mode_client()\n    with (\n        patch_project_client(client),\n        _patched_git_push() as mock_push,\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=False),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert (\n        \"warning: not in a git repo; skipping push, server will use last pushed code\"\n        in result.stderr\n    )\n    assert \"updated my-app\" in result.output\n    # Push should be skipped entirely.\n    mock_push.assert_not_called()\n    # The update should still go through.\n    client.update_deployment.assert_called_once()\n\n\ndef test_apply_no_push_skips_push(patched_auth: Any, tmp_path: Any) -> None:\n    \"\"\"``--no-push`` suppresses the git push even when the deployment is\n    push-mode and cwd is a valid git repo.\"\"\"\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: feature-branch\\n\")\n\n    client = _push_mode_client()\n    with patch_project_client(client), _patched_git_push() as mock_push:\n        result = runner.invoke(app, [\"deployments\", \"apply\", \"-f\", str(f), \"--no-push\"])\n\n    assert result.exit_code == 0, result.output\n    assert \"updated my-app\" in result.output\n    mock_push.assert_not_called()\n    client.update_deployment.assert_called_once()\n"
  },
  {
    "path": "packages/llamactl/tests/test_deployments_editor_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for editor-backed deployment create/edit commands.\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\nimport textwrap\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom types import SimpleNamespace\nfrom typing import Any\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport httpx\nimport pytest\nfrom click.testing import CliRunner\nfrom conftest import (\n    make_deployment,\n    make_loop_bound_project_client,\n    patch_project_client,\n)\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.commands import deployment as deployment_cmd\nfrom llama_agents.cli.local_context import LocalContext\nfrom llama_agents.core.schema.deployments import DeploymentResponse\nfrom llama_agents.core.schema.git_validation import RepositoryValidationResponse\n\n_DEPLOY_CMD = \"llama_agents.cli.commands.deployment\"\n_INTERACTIVE_PATCH = \"llama_agents.cli.commands.deployment.is_interactive_session\"\n\n\n@pytest.fixture(autouse=True)\ndef _interactive_session(monkeypatch: pytest.MonkeyPatch) -> Any:\n    \"\"\"Default editor tests to interactive mode (most tests need the editor).\n\n    Individual tests that verify non-interactive behavior override this.\n    \"\"\"\n    monkeypatch.delenv(\"CI\", raising=False)\n    with patch(_INTERACTIVE_PATCH, return_value=True):\n        yield\n\n\ndef _http_404(deployment_id: str = \"unknown\") -> httpx.HTTPStatusError:\n    request = httpx.Request(\n        \"GET\", f\"http://test/api/v1beta1/deployments/{deployment_id}\"\n    )\n    response = httpx.Response(404, request=request, text='{\"detail\":\"not found\"}')\n    return httpx.HTTPStatusError(\"HTTP 404\", request=request, response=response)\n\n\ndef _editor_client_mock(\n    *,\n    existing: DeploymentResponse | None = None,\n    created: DeploymentResponse | None = None,\n) -> MagicMock:\n    client = MagicMock()\n    client.project_id = \"proj_default\"\n    client.base_url = \"http://test:8011\"\n    client.api_key = \"profile-client-key\"\n\n    if existing is None:\n        client.get_deployment = AsyncMock(side_effect=_http_404())\n    else:\n\n        async def _get(deployment_id: str) -> DeploymentResponse:\n            if deployment_id == existing.id:\n                return existing\n            raise _http_404(deployment_id)\n\n        client.get_deployment = AsyncMock(side_effect=_get)\n\n    client.create_deployment = AsyncMock(\n        return_value=created or make_deployment(\"new-app\")\n    )\n    client.update_deployment = AsyncMock(\n        return_value=existing or make_deployment(\"my-app\")\n    )\n    client.validate_repository = AsyncMock(\n        return_value=RepositoryValidationResponse(accessible=True, message=\"ok\")\n    )\n    return client\n\n\ndef _patch_local_context(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.setattr(\n        deployment_cmd,\n        \"gather_local_context\",\n        lambda: LocalContext(\n            is_git_repo=True,\n            repo_url=\"https://github.com/example/repo\",\n            git_ref=\"main\",\n            generate_name=\"scaffold-app\",\n            deployment_file_path=\"llama_deploy.yaml\",\n            installed_appserver_version=\"0.5.0\",\n        ),\n    )\n\n\n@contextmanager\ndef _patch_yaml_editor(*responses: str | None) -> Iterator[list[str]]:\n    opened_texts: list[str] = []\n    response_iter = iter(responses)\n\n    def _open(text: str) -> str | None:\n        opened_texts.append(text)\n        try:\n            return next(response_iter)\n        except StopIteration:\n            return text\n\n    if hasattr(deployment_cmd, \"_open_deployment_yaml_editor\"):\n        with patch(f\"{_DEPLOY_CMD}._open_deployment_yaml_editor\", side_effect=_open):\n            yield opened_texts\n        return\n\n    def _edit(*args: Any, **kwargs: Any) -> str | None:\n        if \"filename\" in kwargs:\n            raise AssertionError(\n                \"TODO: editor commands should use \"\n                \"_open_deployment_yaml_editor(text: str) -> str | None or \"\n                \"click.edit(text=..., extension='.yaml')\"\n            )\n        if len(args) > 1:\n            raise AssertionError(\"click.edit should receive only YAML text\")\n        text = kwargs.get(\"text\", args[0] if args else None)\n        if not isinstance(text, str):\n            raise AssertionError(\"click.edit should receive YAML text\")\n        assert kwargs.get(\"extension\") == \".yaml\"\n        return _open(text)\n\n    with patch(f\"{_DEPLOY_CMD}.click.edit\", side_effect=_edit):\n        yield opened_texts\n\n\ndef test_create_opens_editor_with_template_and_applies_saved_yaml(\n    patched_auth: Any, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    _patch_local_context(monkeypatch)\n    runner = CliRunner()\n    client = _editor_client_mock(created=make_deployment(\"editor-app\"))\n    saved_text = textwrap.dedent(\"\"\"\\\n        generate_name: Editor App\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n\n    with patch_project_client(client), _patch_yaml_editor(saved_text) as opened_texts:\n        result = runner.invoke(app, [\"deployments\", \"create\"])\n\n    assert result.exit_code == 0, result.output\n    assert len(opened_texts) == 1\n    assert \"# generate_name: scaffold-app\" in opened_texts[0]\n    assert 'repo_url: \"\"' in opened_texts[0]\n    client.create_deployment.assert_called_once()\n    assert \"created editor-app\" in result.output\n\n\ndef test_create_preflights_auth_and_shows_logged_in_email(\n    patched_auth: Any, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    _patch_local_context(monkeypatch)\n    runner = CliRunner()\n    client = _editor_client_mock(created=make_deployment(\"editor-app\"))\n    saved_text = textwrap.dedent(\"\"\"\\\n        generate_name: Editor App\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n    order: list[str] = []\n    profile = SimpleNamespace(\n        device_oidc=SimpleNamespace(email=\"user@example.com\"),\n    )\n\n    def _validate_profile() -> SimpleNamespace:\n        order.append(\"auth\")\n        return profile\n\n    def _open_editor(text: str) -> str:\n        order.append(\"editor\")\n        assert \"## Logged in as user@example.com\" in text\n        return saved_text\n\n    with (\n        patch_project_client(client),\n        patch(\n            \"llama_agents.cli.commands.auth.validate_authenticated_profile\",\n            side_effect=_validate_profile,\n        ) as validate_authenticated_profile,\n        patch(\n            f\"{_DEPLOY_CMD}._open_deployment_yaml_editor\",\n            side_effect=_open_editor,\n        ),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"create\"])\n\n    assert result.exit_code == 0, result.output\n    assert order == [\"auth\", \"editor\"]\n    validate_authenticated_profile.assert_called_once_with()\n    client.create_deployment.assert_called_once()\n\n\ndef test_create_file_applies_without_editor_and_threads_project_no_push(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"generate_name: File App\\nspec:\\n  repo_url: ''\\n\")\n    client = _editor_client_mock(created=make_deployment(\"file-app\"))\n\n    with (\n        patch_project_client(client) as ctor,\n        patch(f\"{_DEPLOY_CMD}.click.edit\") as edit,\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(f\"{_DEPLOY_CMD}.push_to_remote\") as push,\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"deployments\",\n                \"create\",\n                \"-f\",\n                str(f),\n                \"--project\",\n                \"proj_other\",\n                \"--no-push\",\n            ],\n        )\n\n    assert result.exit_code == 0, result.output\n    edit.assert_not_called()\n    push.assert_not_called()\n    args, _ = ctor.call_args\n    assert args[1] == \"proj_other\"\n    client.create_deployment.assert_called_once()\n\n\ndef test_create_file_without_profile_raises_auth_error(\n    monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"generate_name: File App\\nspec:\\n  repo_url: ''\\n\")\n\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.get_current_profile.return_value = None\n    mock_auth_svc.list_profiles.return_value = []\n    mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n\n    mock_service = MagicMock()\n    mock_service.current_auth_service.return_value = mock_auth_svc\n\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\", mock_service),\n        patch(\"llama_agents.cli.client.is_interactive_session\", return_value=False),\n        patch(_INTERACTIVE_PATCH, return_value=False),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"create\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"No profile configured. To get started, run: llamactl auth login\" in (\n        result.output\n    )\n\n\ndef test_create_file_uses_create_intent_not_upsert(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\n        textwrap.dedent(\"\"\"\\\n            name: existing-app\n            generate_name: Existing App\n            spec:\n              repo_url: https://github.com/example/repo\n        \"\"\")\n    )\n    client = make_loop_bound_project_client(\n        existing=make_deployment(\"existing-app\"),\n        created=make_deployment(\"existing-app\"),\n    )\n\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"create\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.get_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n    client.create_deployment.assert_called_once()\n\n\ndef test_create_file_name_only_uses_name_as_display_name(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: explicit-app\\nspec:\\n  repo_url: ''\\n\")\n    client = _editor_client_mock(created=make_deployment(\"explicit-app\"))\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=False),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"create\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    payload = client.create_deployment.call_args[0][0]\n    assert payload.id == \"explicit-app\"\n    assert payload.display_name == \"explicit-app\"\n\n\ndef test_create_file_reports_identity_and_source_errors_together(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"spec:\\n  git_ref: main\\n\")\n    client = _editor_client_mock()\n\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"create\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"set top-level 'name' or 'generate_name'\" in result.output\n    assert \"set spec.repo_url for create\" in result.output\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n\n\ndef test_create_file_uses_one_event_loop(patched_auth: Any, tmp_path: Path) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\n        textwrap.dedent(\"\"\"\\\n            generate_name: File App\n            spec:\n              repo_url: https://github.com/example/repo\n        \"\"\")\n    )\n    client = make_loop_bound_project_client(created=make_deployment(\"file-app\"))\n\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"create\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.validate_repository.assert_awaited_once()\n    client.create_deployment.assert_called_once()\n\n\ndef test_edit_opens_current_template_and_updates_saved_yaml(patched_auth: Any) -> None:\n    runner = CliRunner()\n    existing = make_deployment(\"my-app\", git_ref=\"main\")\n    client = _editor_client_mock(existing=existing)\n    saved_text = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          git_ref: v2\n    \"\"\")\n\n    with patch_project_client(client), _patch_yaml_editor(saved_text) as opened_texts:\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert len(opened_texts) == 1\n    assert \"name: my-app\" in opened_texts[0]\n    assert \"status:\" not in opened_texts[0]\n    assert client.get_deployment.await_count == 2\n    client.update_deployment.assert_called_once()\n    assert client.update_deployment.call_args[0][0] == \"my-app\"\n    assert \"updated my-app\" in result.output\n\n\ndef test_edit_push_mode_skips_push_when_remote_missing(patched_auth: Any) -> None:\n    runner = CliRunner()\n    existing = make_deployment(\"my-app\", repo_url=\"internal://\", git_ref=\"main\")\n    client = _editor_client_mock(existing=existing)\n    saved_text = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          git_ref: v2\n    \"\"\")\n\n    with (\n        patch_project_client(client),\n        _patch_yaml_editor(saved_text),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(f\"{_DEPLOY_CMD}.has_deployment_git_remote\", return_value=False),\n        patch(f\"{_DEPLOY_CMD}.configure_git_remote\") as configure,\n        patch(f\"{_DEPLOY_CMD}.push_to_remote\") as push,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert \"warning: not pushing code; no llamaagents-my-app remote\" in result.stderr\n    configure.assert_not_called()\n    push.assert_not_called()\n    client.update_deployment.assert_called_once()\n\n\ndef test_edit_preserves_existing_secret_names_as_masks(patched_auth: Any) -> None:\n    runner = CliRunner()\n    existing = make_deployment(\"my-app\", secret_names=[\"MY_SECRET\"])\n    client = _editor_client_mock(existing=existing)\n\n    with patch_project_client(client), _patch_yaml_editor(None) as opened_texts:\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert \"  secrets:\\n    MY_SECRET: '********'\" in opened_texts[0]\n    assert \"    # MY_SECRET:\" not in opened_texts[0]\n    client.update_deployment.assert_not_called()\n\n\ndef test_edit_file_uses_update_intent_not_create(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: v2\\n\")\n    client = make_loop_bound_project_client(existing=make_deployment(\"my-app\"))\n\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    client.get_deployment.assert_awaited_once()\n    client.create_deployment.assert_not_called()\n    client.update_deployment.assert_called_once()\n    assert client.update_deployment.call_args[0][0] == \"my-app\"\n\n\ndef test_edit_file_push_mode_skips_push_when_remote_missing(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: v2\\n\")\n    client = _editor_client_mock(\n        existing=make_deployment(\"my-app\", repo_url=\"internal://\")\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(f\"{_DEPLOY_CMD}.has_deployment_git_remote\", return_value=False),\n        patch(f\"{_DEPLOY_CMD}.configure_git_remote\") as configure,\n        patch(f\"{_DEPLOY_CMD}.push_to_remote\") as push,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"-f\", str(f)])\n\n    assert result.exit_code == 0, result.output\n    assert \"warning: not pushing code; no llamaagents-my-app remote\" in result.stderr\n    configure.assert_not_called()\n    push.assert_not_called()\n    client.update_deployment.assert_called_once()\n\n\ndef test_edit_file_push_flag_forces_push(patched_auth: Any, tmp_path: Path) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: my-app\\nspec:\\n  git_ref: v2\\n\")\n    client = _editor_client_mock(\n        existing=make_deployment(\"my-app\", repo_url=\"internal://\")\n    )\n\n    with (\n        patch_project_client(client),\n        patch(f\"{_DEPLOY_CMD}.is_git_repo\", return_value=True),\n        patch(f\"{_DEPLOY_CMD}.has_deployment_git_remote\", return_value=False),\n        patch(f\"{_DEPLOY_CMD}.configure_git_remote\", return_value=\"llamaagents-test\"),\n        patch(\n            f\"{_DEPLOY_CMD}.push_to_remote\",\n            return_value=subprocess.CompletedProcess([], 0, stderr=b\"\"),\n        ) as push,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"-f\", str(f), \"--push\"])\n\n    assert result.exit_code == 0, result.output\n    push.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_interactive_edit_uses_separate_clients_for_fetch_and_apply(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    existing = make_deployment(\"my-app\", git_ref=\"main\")\n    fetch_client = make_loop_bound_project_client(existing=existing)\n    apply_client = make_loop_bound_project_client(existing=existing)\n    saved_text = textwrap.dedent(\"\"\"\\\n        name: my-app\n        spec:\n          git_ref: v2\n    \"\"\")\n\n    with (\n        patch(\n            \"llama_agents.core.client.manage_client.ProjectClient\",\n            side_effect=[fetch_client, apply_client],\n        ) as ctor,\n        _patch_yaml_editor(saved_text),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert ctor.call_count == 2\n    fetch_client.get_deployment.assert_awaited_once_with(\"my-app\")\n    fetch_client.update_deployment.assert_not_called()\n    apply_client.update_deployment.assert_called_once()\n    assert apply_client.update_deployment.call_args[0][0] == \"my-app\"\n\n\ndef test_edit_file_rejects_name_that_does_not_match_argument(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"name: other-app\\nspec:\\n  git_ref: v2\\n\")\n    client = _editor_client_mock(existing=make_deployment(\"my-app\"))\n\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"my-app\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"does not match deployment 'my-app'\" in result.output\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n\n\ndef test_edit_file_requires_name_without_argument(\n    patched_auth: Any, tmp_path: Path\n) -> None:\n    runner = CliRunner()\n    f = tmp_path / \"deploy.yaml\"\n    f.write_text(\"spec:\\n  git_ref: v2\\n\")\n    client = _editor_client_mock(existing=make_deployment(\"my-app\"))\n\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"-f\", str(f)])\n\n    assert result.exit_code != 0\n    assert \"top-level 'name' for edit\" in result.output\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n\n\ndef test_edit_editor_reopens_when_name_changes(patched_auth: Any) -> None:\n    runner = CliRunner()\n    existing = make_deployment(\"my-app\", git_ref=\"main\")\n    client = _editor_client_mock(existing=existing)\n    changed_name = \"name: other-app\\nspec:\\n  git_ref: v2\\n\"\n    fixed_name = \"name: my-app\\nspec:\\n  git_ref: v2\\n\"\n\n    with (\n        patch_project_client(client),\n        _patch_yaml_editor(changed_name, fixed_name) as opened_texts,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert len(opened_texts) == 2\n    assert \"## ERROR: YAML name 'other-app'\" in opened_texts[1]\n    client.update_deployment.assert_called_once()\n    assert client.update_deployment.call_args[0][0] == \"my-app\"\n\n\ndef test_editor_parse_error_annotates_and_reopens(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client = _editor_client_mock(created=make_deployment(\"retry-app\"))\n    invalid_yaml = textwrap.dedent(\"\"\"\\\n        generate_name: Retry App\n        spec:\n          bogus: nope\n    \"\"\")\n    valid_yaml = textwrap.dedent(\"\"\"\\\n        generate_name: Retry App\n        spec:\n          repo_url: https://github.com/example/repo\n    \"\"\")\n\n    with (\n        patch_project_client(client),\n        _patch_yaml_editor(invalid_yaml, valid_yaml) as opened_texts,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"create\"])\n\n    assert result.exit_code == 0, result.output\n    assert len(opened_texts) == 2\n    assert \"## ERROR: spec.bogus: Extra inputs are not permitted\" in opened_texts[1]\n    client.create_deployment.assert_called_once()\n    assert \"created retry-app\" in result.output\n\n\ndef test_editor_create_validation_annotates_all_errors(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client = _editor_client_mock()\n\n    with (\n        patch_project_client(client),\n        _patch_yaml_editor(\"spec: {}\\n\", None) as opened_texts,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"create\"])\n\n    assert result.exit_code == 0, result.output\n    assert len(opened_texts) == 2\n    assert \"set top-level 'name' or 'generate_name'\" in opened_texts[1]\n    assert \"set spec.repo_url for create\" in opened_texts[1]\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n\n\ndef test_unchanged_editor_file_aborts_without_api_calls(\n    patched_auth: Any, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    _patch_local_context(monkeypatch)\n    runner = CliRunner()\n    client = _editor_client_mock()\n\n    with patch_project_client(client), _patch_yaml_editor() as opened_texts:\n        result = runner.invoke(app, [\"deployments\", \"create\"])\n\n    assert result.exit_code == 0, result.output\n    assert len(opened_texts) == 1\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n    client.validate_repository.assert_not_called()\n\n\n@pytest.mark.parametrize(\"saved_text\", [\"\", \"# only comments\\n\\n  # still comments\\n\"])\ndef test_empty_or_all_comment_editor_file_aborts_without_api_calls(\n    patched_auth: Any,\n    monkeypatch: pytest.MonkeyPatch,\n    saved_text: str,\n) -> None:\n    _patch_local_context(monkeypatch)\n    runner = CliRunner()\n    client = _editor_client_mock()\n\n    with patch_project_client(client), _patch_yaml_editor(saved_text):\n        result = runner.invoke(app, [\"deployments\", \"create\"])\n\n    assert result.exit_code == 0, result.output\n    client.get_deployment.assert_not_called()\n    client.create_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n    client.validate_repository.assert_not_called()\n\n\n@pytest.mark.parametrize(\n    (\"argv\", \"message\"),\n    [\n        ([\"deployments\", \"create\"], \"create\"),\n        ([\"deployments\", \"edit\", \"my-app\"], \"edit\"),\n    ],\n)\ndef test_non_interactive_editor_commands_require_file(\n    argv: list[str], message: str\n) -> None:\n    runner = CliRunner()\n    with patch(_INTERACTIVE_PATCH, return_value=False):\n        result = runner.invoke(app, argv)\n\n    assert result.exit_code != 0\n    assert f\"pass -f <file> for non-interactive {message}\" in result.output\n\n\ndef test_ci_forces_editor_commands_to_require_file(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setenv(\"CI\", \"true\")\n    runner = CliRunner()\n\n    with patch(_INTERACTIVE_PATCH, return_value=False):\n        result = runner.invoke(app, [\"deployments\", \"edit\", \"my-app\"])\n\n    assert result.exit_code != 0\n    assert \"pass -f <file> for non-interactive edit\" in result.output\n\n\ndef test_editor_command_help_avoids_deployment_implementation_imports() -> None:\n    script = textwrap.dedent(\n        \"\"\"\n        import sys\n        from click.testing import CliRunner\n        from llama_agents.cli.app import app\n\n        result = CliRunner().invoke(app, [\"deployments\", \"create\", \"--help\"])\n        if result.exit_code != 0:\n            raise SystemExit(result.exit_code)\n        print(\"llama_agents.cli.apply\" in sys.modules)\n        \"\"\"\n    )\n\n    proc = subprocess.run(\n        [\"python\", \"-c\", script],\n        check=False,\n        capture_output=True,\n        text=True,\n    )\n\n    assert proc.returncode == 0, proc.stderr\n    assert proc.stdout.strip() == \"False\"\n"
  },
  {
    "path": "packages/llamactl/tests/test_deployments_get_output.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``deployments get`` output modes, ``--project`` override, and the\n``deployments list`` hidden alias.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom datetime import datetime, timezone\nfrom types import SimpleNamespace\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nimport httpx\nimport llama_agents.cli.config.env_service as env_service\nimport pytest\nimport yaml\nfrom click.testing import CliRunner\nfrom conftest import make_deployment, patch_project_client, set_llama_cloud_env\nfrom llama_agents.cli.app import app\nfrom llama_agents.core.schema.deployments import (\n    DeploymentHistoryResponse,\n    DeploymentResponse,\n    ReleaseHistoryItem,\n)\n\nDEFAULT_BASE_URL = \"https://api.cloud.llamaindex.ai\"\n\n\ndef _make_client_mock(deployments: list[DeploymentResponse]) -> MagicMock:\n    \"\"\"A mock ProjectClient stand-in with the methods the commands hit.\"\"\"\n\n    async def _list() -> list[DeploymentResponse]:\n        return list(deployments)\n\n    async def _get(\n        deployment_id: str, include_events: bool = False\n    ) -> DeploymentResponse:\n        for d in deployments:\n            if d.id == deployment_id:\n                return d\n        raise RuntimeError(f\"deployment not found: {deployment_id}\")\n\n    client = MagicMock()\n    client.list_deployments.side_effect = _list\n    client.get_deployment.side_effect = _get\n    client.project_id = \"proj_default\"\n    client.base_url = \"http://test:8011\"\n    return client\n\n\ndef _patch_no_profile_auth(monkeypatch: pytest.MonkeyPatch) -> None:\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.get_current_profile.return_value = None\n    mock_auth_svc.list_profiles.return_value = []\n    mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n    mock_auth_svc.auth_middleware.return_value = None\n\n    mock_service = MagicMock()\n    mock_service.current_auth_service.return_value = mock_auth_svc\n    mock_service.get_current_environment.return_value = SimpleNamespace(\n        api_url=DEFAULT_BASE_URL,\n        requires_auth=True,\n    )\n    monkeypatch.setattr(env_service, \"service\", mock_service)\n\n\ndef test_deployments_get_text_no_args_lists(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\"), make_deployment(\"app-b\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\"])\n    assert result.exit_code == 0, result.output\n    assert \"app-a\" in result.output\n    assert \"app-b\" in result.output\n    # Plain-table headers, no Rich markup, no truncation ellipsis.\n    assert \"NAME\" in result.output\n    assert \"PHASE\" in result.output\n    assert \"\\x1b[\" not in result.output  # no ANSI escapes\n    assert \"…\" not in result.output\n\n\ndef test_deployments_get_uses_complete_env_auth_without_profile(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _patch_no_profile_auth(monkeypatch)\n\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\", project_id=\"env-project\")]\n    client_mock = _make_client_mock(deployments)\n    client_mock.project_id = \"env-project\"\n    client_mock.base_url = DEFAULT_BASE_URL\n    client_mock.api_key = \"env-api-key\"\n    with patch_project_client(client_mock) as ctor:\n        result = runner.invoke(app, [\"deployments\", \"get\", \"-o\", \"json\"])\n\n    assert result.exit_code == 0, result.output\n    assert json.loads(result.output)[0][\"name\"] == \"app-a\"\n    args, _ = ctor.call_args\n    assert args == (DEFAULT_BASE_URL, \"env-project\", \"env-api-key\", None)\n\n\ndef test_deployments_get_json_array(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\"), make_deployment(\"app-b\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"-o\", \"json\"])\n    assert result.exit_code == 0, result.output\n    data = json.loads(result.output)\n    assert isinstance(data, list)\n    assert {d[\"name\"] for d in data} == {\"app-a\", \"app-b\"}\n    # All deployments returned should expose ``status``.\n    assert all(\"status\" in d for d in data)\n    assert all(\"phase\" in d[\"status\"] for d in data)\n    # Deprecated aliases / leaked flags must not appear.\n    for d in data:\n        assert \"id\" not in d\n        assert \"llama_deploy_version\" not in d\n        assert \"has_personal_access_token\" not in d\n        assert \"secret_names\" not in d\n\n\ndef test_deployments_get_yaml_list(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [make_deployment(\"only-one\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"-o\", \"yaml\"])\n    assert result.exit_code == 0, result.output\n    parsed = yaml.safe_load(result.output)\n    assert isinstance(parsed, list)\n    assert parsed[0][\"name\"] == \"only-one\"\n\n\ndef test_deployments_get_single_text_outputs_table(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [make_deployment(\"my-app\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"my-app\"])\n    assert result.exit_code == 0, result.output\n    assert \"my-app\" in result.output\n    # Single-row uses the same column layout as the list view.\n    assert \"NAME\" in result.output\n    assert \"PHASE\" in result.output\n\n\ndef test_deployments_get_single_json(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [make_deployment(\"my-app\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"my-app\", \"-o\", \"json\"])\n    assert result.exit_code == 0, result.output\n    obj = json.loads(result.output)\n    assert isinstance(obj, dict)\n    assert obj[\"name\"] == \"my-app\"\n    assert \"spec\" in obj\n    assert obj[\"status\"][\"phase\"] == \"Running\"\n    assert obj[\"status\"][\"project_id\"] == \"proj_default\"\n    # warning is always-explicit-null\n    assert obj[\"status\"][\"warning\"] is None\n    # No deprecated aliases / leaks\n    assert \"id\" not in obj\n    assert \"llama_deploy_version\" not in obj\n    assert \"has_personal_access_token\" not in obj\n    assert \"secret_names\" not in obj\n    # Empty secrets / no PAT means the keys are omitted entirely from spec.\n    assert \"secrets\" not in obj[\"spec\"]\n    assert \"personal_access_token\" not in obj[\"spec\"]\n\n\ndef test_deployments_get_single_yaml(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [make_deployment(\"my-app\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"my-app\", \"-o\", \"yaml\"])\n    assert result.exit_code == 0, result.output\n    obj = yaml.safe_load(result.output)\n    assert isinstance(obj, dict)\n    assert obj[\"name\"] == \"my-app\"\n    assert obj[\"status\"][\"phase\"] == \"Running\"\n\n\ndef test_deployments_get_preserves_secret_mask_placeholders(patched_auth: Any) -> None:\n    \"\"\"Secret names are shown with masked values so the user can see which\n    secrets are configured.\"\"\"\n    runner = CliRunner()\n    deployments = [\n        make_deployment(\n            \"secret-app\",\n            secret_names=[\"LLAMA_CLOUD_API_KEY\", \"OPENAI_API_KEY\"],\n            has_personal_access_token=True,\n        )\n    ]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"secret-app\", \"-o\", \"json\"])\n    assert result.exit_code == 0, result.output\n    obj = json.loads(result.output)\n    assert obj[\"spec\"][\"secrets\"] == {\n        \"LLAMA_CLOUD_API_KEY\": \"********\",\n        \"OPENAI_API_KEY\": \"********\",\n    }\n    assert obj[\"spec\"][\"personal_access_token\"] == \"********\"\n\n\ndef test_deployments_get_empty_json_is_array(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client_mock = _make_client_mock([])\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"-o\", \"json\"])\n    assert result.exit_code == 0, result.output\n    assert json.loads(result.output) == []\n\n\ndef test_deployments_list_hidden_alias_works(patched_auth: Any) -> None:\n    \"\"\"The hidden ``deployments list`` alias should still produce JSON output.\"\"\"\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"list\", \"-o\", \"json\"])\n    assert result.exit_code == 0, result.output\n    data = json.loads(result.output)\n    assert [d[\"name\"] for d in data] == [\"app-a\"]\n\n\ndef test_deployments_list_hidden_in_help(patched_auth: Any) -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"--help\"])\n    assert result.exit_code == 0\n    # `list` should be hidden (not surfaced in `--help`), but `get` should be.\n    assert \"  list \" not in result.output\n    assert \"  get \" in result.output\n\n\n@pytest.mark.parametrize(\n    \"command\",\n    [\"configure-git-remote\", \"update\", \"history\", \"rollback\", \"logs\"],\n)\ndef test_deployment_name_required_for_single_deployment_commands(\n    patched_auth: Any,\n    command: str,\n) -> None:\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\"), make_deployment(\"app-b\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", command])\n    assert result.exit_code != 0\n    assert f\"llamactl deployments {command} <deployment_id>\" in result.output\n    assert \"app-a\" in result.output\n\n\ndef test_deployments_get_project_override_threads_to_client(\n    patched_auth: Any,\n) -> None:\n    \"\"\"``--project foo`` should construct a ProjectClient with project_id='foo'.\"\"\"\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\", project_id=\"proj_other\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock) as ctor:\n        result = runner.invoke(\n            app,\n            [\n                \"deployments\",\n                \"get\",\n                \"--project\",\n                \"proj_other\",\n                \"-o\",\n                \"json\",\n            ],\n        )\n    assert result.exit_code == 0, result.output\n    # ProjectClient was called with project_id='proj_other'\n    args, kwargs = ctor.call_args\n    # Positional: (api_url, project_id, api_key, auth_middleware)\n    assert args[1] == \"proj_other\"\n\n\ndef _full_sha(prefix: str) -> str:\n    \"\"\"Return a 40-char hex string starting with ``prefix`` for history tests.\"\"\"\n    return (prefix + \"0\" * 40)[:40]\n\n\ndef _history_client_mock(items: list[ReleaseHistoryItem]) -> MagicMock:\n    client_mock = _make_client_mock([make_deployment(\"my-app\")])\n\n    async def _hist(deployment_id: str) -> DeploymentHistoryResponse:\n        return DeploymentHistoryResponse(\n            deployment_id=deployment_id, history=list(items)\n        )\n\n    client_mock.get_deployment_history.side_effect = _hist\n    return client_mock\n\n\ndef test_deployments_history_json_output(patched_auth: Any) -> None:\n    runner = CliRunner()\n    items = [\n        ReleaseHistoryItem(\n            git_sha=_full_sha(\"aaaaaaa1111\"),\n            released_at=datetime(2026, 1, 1, tzinfo=timezone.utc),\n        ),\n        ReleaseHistoryItem(\n            git_sha=_full_sha(\"bbbbbbb2222\"),\n            released_at=datetime(2026, 2, 1, tzinfo=timezone.utc),\n        ),\n    ]\n    client_mock = _history_client_mock(items)\n    with patch_project_client(client_mock):\n        result = runner.invoke(\n            app,\n            [\n                \"deployments\",\n                \"history\",\n                \"my-app\",\n                \"-o\",\n                \"json\",\n            ],\n        )\n    assert result.exit_code == 0, result.output\n    data = json.loads(result.output)\n    assert isinstance(data, list)\n    # Newest first\n    assert data[0][\"git_sha\"] == _full_sha(\"bbbbbbb2222\")\n    assert data[1][\"git_sha\"] == _full_sha(\"aaaaaaa1111\")\n    # JSON keeps full 40-char shas.\n    assert all(len(d[\"git_sha\"]) == 40 for d in data)\n    # JSON timestamps are Z-suffixed (Pydantic default).\n    assert all(d[\"released_at\"].endswith(\"Z\") for d in data)\n\n\ndef test_deployments_history_text_short_sha_and_z_timestamp(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    full_sha = _full_sha(\"640f764\")\n    items = [\n        ReleaseHistoryItem(\n            git_sha=full_sha,\n            released_at=datetime(2026, 4, 25, 15, 1, 15, tzinfo=timezone.utc),\n            image_tag=\"0.11.1\",\n        ),\n    ]\n    client_mock = _history_client_mock(items)\n    with patch_project_client(client_mock):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"history\", \"my-app\"],\n        )\n    assert result.exit_code == 0, result.output\n    # Header present.\n    assert \"RELEASED_AT\" in result.output\n    assert \"GIT_SHA\" in result.output\n    assert \"IMAGE_TAG\" in result.output\n    # Z-suffixed timestamp; no +00:00.\n    assert \"2026-04-25T15:01:15Z\" in result.output\n    assert \"+00:00\" not in result.output\n    # Short sha (7 chars) only — full sha must not appear.\n    assert \"640f764\" in result.output\n    assert full_sha not in result.output\n    assert \"0.11.1\" in result.output\n\n\ndef test_rollback_without_git_sha_non_interactive_lists_history_and_hints(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    items = [\n        ReleaseHistoryItem(\n            git_sha=_full_sha(\"aaaaaaa1111\"),\n            released_at=datetime(2026, 1, 1, tzinfo=timezone.utc),\n        ),\n        ReleaseHistoryItem(\n            git_sha=_full_sha(\"bbbbbbb2222\"),\n            released_at=datetime(2026, 2, 1, tzinfo=timezone.utc),\n        ),\n    ]\n    client_mock = _history_client_mock(items)\n    with (\n        patch_project_client(client_mock),\n        patch(\n            \"llama_agents.cli.interactive.is_interactive_session\", return_value=False\n        ),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"rollback\", \"my-app\"])\n\n    assert result.exit_code != 0\n    assert \"Select git sha:\" in result.output\n    assert \"bbbbbbb\" in result.output\n    assert \"--git-sha\" in result.output\n    assert \"llamactl deployments history my-app\" in result.output\n\n\ndef test_rollback_empty_history_errors(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client_mock = _history_client_mock([])\n    with (\n        patch_project_client(client_mock),\n        patch(\n            \"llama_agents.cli.interactive.is_interactive_session\", return_value=False\n        ),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"rollback\", \"my-app\"])\n\n    assert result.exit_code != 0\n    assert \"No history available\" in result.output\n\n\ndef _http_status_error(\n    status: int, *, url: str = \"http://internal/api\"\n) -> httpx.HTTPStatusError:\n    \"\"\"Build an HTTPStatusError with a real Response for friendly-error tests.\"\"\"\n    request = httpx.Request(\"GET\", url)\n    response = httpx.Response(status, request=request, text='{\"detail\":\"x\"}')\n    return httpx.HTTPStatusError(\n        f\"HTTP {status} for url {url} - {response.text}\",\n        request=request,\n        response=response,\n    )\n\n\ndef test_deployments_get_404_renders_friendly_message(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client_mock = _make_client_mock([])\n\n    async def _raise_404(deployment_id: str, include_events: bool = False) -> None:\n        raise _http_status_error(404)\n\n    client_mock.get_deployment.side_effect = _raise_404\n    with patch_project_client(client_mock):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"get\", \"nonexistent-app\"],\n        )\n    assert result.exit_code != 0\n    assert (\n        \"deployment 'nonexistent-app' not found in project 'proj_default'\"\n        in result.output\n    )\n    # No URL, no JSON body should leak through.\n    assert \"http://\" not in result.output\n    assert \"detail\" not in result.output\n\n\ndef test_deployments_get_404_with_project_includes_project(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    client_mock = _make_client_mock([])\n    client_mock.project_id = \"proj_other\"\n\n    async def _raise_404(deployment_id: str, include_events: bool = False) -> None:\n        raise _http_status_error(404)\n\n    client_mock.get_deployment.side_effect = _raise_404\n    with patch_project_client(client_mock):\n        result = runner.invoke(\n            app,\n            [\n                \"deployments\",\n                \"get\",\n                \"nonexistent-app\",\n                \"--project\",\n                \"proj_other\",\n            ],\n        )\n    assert result.exit_code != 0\n    assert (\n        \"deployment 'nonexistent-app' not found in project 'proj_other'\"\n        in result.output\n    )\n\n\ndef test_deployments_get_500_keeps_verbose_message(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client_mock = _make_client_mock([])\n\n    async def _raise_500(deployment_id: str, include_events: bool = False) -> None:\n        raise _http_status_error(500)\n\n    client_mock.get_deployment.side_effect = _raise_500\n    with patch_project_client(client_mock):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"get\", \"boom\"],\n        )\n    assert result.exit_code != 0\n    # Non-404 keeps the verbose default message (URL + body) for debug-visibility.\n    assert \"HTTP 500\" in result.output\n    assert \"http://\" in result.output\n\n\ndef test_deployments_get_text_column_order(patched_auth: Any) -> None:\n    \"\"\"Text mode columns are in declaration order: NAME, REPO, GIT_REF, PHASE.\"\"\"\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\"])\n    assert result.exit_code == 0, result.output\n    header = result.output.splitlines()[0]\n    name_idx = header.index(\"NAME\")\n    repo_idx = header.index(\"REPO\")\n    ref_idx = header.index(\"GIT_REF\")\n    phase_idx = header.index(\"PHASE\")\n    assert name_idx < repo_idx < ref_idx < phase_idx\n\n\ndef test_deployments_get_text_no_wide_columns(patched_auth: Any) -> None:\n    \"\"\"``-o text`` excludes wide-only columns (GIT_SHA, APISERVER_URL, ...).\"\"\"\n    runner = CliRunner()\n    deployments = [make_deployment(\"app-a\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\"])\n    assert result.exit_code == 0, result.output\n    assert \"GIT_SHA\" not in result.output\n    assert \"APISERVER_URL\" not in result.output\n    assert \"PROJECT\" not in result.output\n    assert \"APPSERVER\" not in result.output\n    assert \"SUSPENDED\" not in result.output\n\n\ndef test_deployments_get_wide_includes_extra_columns(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [\n        make_deployment(\"app-a\", git_sha=\"abc1234567\", appserver_version=\"0.4.2\")\n    ]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"-o\", \"wide\"])\n    assert result.exit_code == 0, result.output\n    header = result.output.splitlines()[0]\n    # Default columns still present; wide columns now appear too.\n    for h in (\"NAME\", \"REPO\", \"GIT_REF\", \"PHASE\", \"APPSERVER\", \"GIT_SHA\"):\n        assert h in header\n    # Wide columns slot into their natural positions, interleaved.\n    # APPSERVER (spec) should appear before PHASE (status).\n    assert header.index(\"APPSERVER\") < header.index(\"PHASE\")\n\n\ndef test_deployments_get_template_single_emits_apply_shape(patched_auth: Any) -> None:\n    runner = CliRunner()\n    deployments = [\n        make_deployment(\n            \"my-app\",\n            secret_names=[\"OPENAI_API_KEY\"],\n            has_personal_access_token=True,\n            appserver_version=\"0.5.0\",\n        )\n    ]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"get\", \"my-app\", \"-o\", \"template\"],\n        )\n    assert result.exit_code == 0, result.output\n    out = result.output\n    parsed = yaml.safe_load(out)\n    assert parsed[\"name\"] == \"my-app\"\n    assert \"spec\" in parsed\n    # Status is omitted from template output unconditionally.\n    assert \"status\" not in parsed\n    assert \"phase\" not in out\n    # Doc comments above fields.\n    assert \"## Stable id for the deployment\" in out\n    # Masked secrets / PAT are preserved as placeholders so the user can see\n    # which secrets exist.  Stripping happens on the apply/parse side.\n    assert \"OPENAI_API_KEY\" in out\n    assert parsed[\"spec\"][\"secrets\"] == {\"OPENAI_API_KEY\": \"********\"}\n    assert parsed[\"spec\"][\"personal_access_token\"] == \"********\"\n\n\ndef test_deployments_get_template_does_not_scaffold_generate_name(\n    patched_auth: Any,\n) -> None:\n    \"\"\"``get -o template`` is a faithful projection of an existing deployment;\n    it does not emit the ``# generate_name`` scaffolding line that the offline\n    ``deployments template`` command does.\"\"\"\n    runner = CliRunner()\n    deployments = [make_deployment(\"my-app\", display_name=\"My App\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"get\", \"my-app\", \"-o\", \"template\"],\n        )\n    assert result.exit_code == 0, result.output\n    assert \"generate_name\" not in result.output\n    assert \"generateName\" not in result.output\n\n\ndef test_deployments_get_yaml_emits_generate_name_at_top_level(\n    patched_auth: Any,\n) -> None:\n    \"\"\"``-o yaml`` lifts ``display_name`` out of ``spec`` to the top level\n    as ``generate_name`` (sibling to ``name``).\"\"\"\n    runner = CliRunner()\n    deployments = [make_deployment(\"my-app\", display_name=\"My App\")]\n    client_mock = _make_client_mock(deployments)\n    with patch_project_client(client_mock):\n        result = runner.invoke(app, [\"deployments\", \"get\", \"my-app\", \"-o\", \"yaml\"])\n    assert result.exit_code == 0, result.output\n    obj = yaml.safe_load(result.output)\n    assert obj[\"generate_name\"] == \"My App\"\n    assert \"display_name\" not in obj\n    assert \"display_name\" not in obj[\"spec\"]\n\n\ndef test_deployments_get_template_no_name_errors(patched_auth: Any) -> None:\n    \"\"\"``deployments get -o template`` without a deployment name errors clearly.\"\"\"\n    runner = CliRunner()\n    # No client interaction expected — fail fast before list_deployments.\n    result = runner.invoke(app, [\"deployments\", \"get\", \"-o\", \"template\"])\n    assert result.exit_code != 0\n    assert \"template requires a deployment name\" in result.output\n\n\ndef test_other_commands_reject_template_mode(patched_auth: Any) -> None:\n    \"\"\"``-o template`` is only meaningful for ``deployments get``.\"\"\"\n    runner = CliRunner()\n    result = runner.invoke(app, [\"auth\", \"get\", \"-o\", \"template\"])\n    # The choice is accepted but render_output rejects it with a clear message.\n    assert result.exit_code != 0\n    assert \"only supported for\" in result.output or \"template\" in result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_deployments_logs.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``deployments logs``.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom datetime import datetime, timezone\nfrom types import SimpleNamespace\nfrom typing import Any, AsyncIterator\nfrom unittest.mock import MagicMock\n\nimport llama_agents.cli.config.env_service as env_service\nimport pytest\nfrom click.testing import CliRunner\nfrom conftest import patch_project_client, set_llama_cloud_env\nfrom llama_agents.cli.app import app\nfrom llama_agents.core.schema import LogEvent\n\nDEFAULT_BASE_URL = \"https://api.cloud.llamaindex.ai\"\n\n\ndef _make_log_events(n: int = 3) -> list[LogEvent]:\n    base_ts = datetime(2026, 4, 26, 12, 0, 0, tzinfo=timezone.utc)\n    return [\n        LogEvent(\n            pod=\"pod-x\",\n            container=\"app\",\n            text=(\n                f'{{\"event\": \"msg-{i}\", \"level\": \"info\", '\n                f'\"timestamp\": \"2026-04-26T12:00:0{i}.000Z\"}}'\n            ),\n            timestamp=base_ts.replace(second=i),\n        )\n        for i in range(n)\n    ]\n\n\ndef _make_logs_client(events: list[LogEvent]) -> MagicMock:\n    \"\"\"Project-client mock with stream_deployment_logs + aclose.\"\"\"\n\n    async def _stream(*args: Any, **kwargs: Any) -> AsyncIterator[LogEvent]:\n        for ev in events:\n            yield ev\n\n    async def _aclose() -> None:\n        return None\n\n    client = MagicMock()\n    # SAM: side_effect on a non-async returning an async iterator works because\n    # the command does `async for ev in client.stream_deployment_logs(...)`.\n    # The mock returns the async generator object directly when called.\n    client.stream_deployment_logs = MagicMock(side_effect=_stream)\n    client.get_deployment = MagicMock()\n    client.aclose = MagicMock(side_effect=_aclose)\n    client.project_id = \"proj_default\"\n    client.base_url = \"http://test:8011\"\n    return client\n\n\ndef _patch_no_profile_auth(monkeypatch: pytest.MonkeyPatch) -> None:\n    mock_auth_svc = MagicMock()\n    mock_auth_svc.get_current_profile.return_value = None\n    mock_auth_svc.list_profiles.return_value = []\n    mock_auth_svc.env = SimpleNamespace(requires_auth=True)\n    mock_auth_svc.auth_middleware.return_value = None\n\n    mock_service = MagicMock()\n    mock_service.current_auth_service.return_value = mock_auth_svc\n    mock_service.get_current_environment.return_value = SimpleNamespace(\n        api_url=DEFAULT_BASE_URL,\n        requires_auth=True,\n    )\n    monkeypatch.setattr(env_service, \"service\", mock_service)\n\n\ndef test_logs_default_prints_recent_and_exits(patched_auth: Any) -> None:\n    runner = CliRunner()\n    events = _make_log_events(3)\n    client = _make_logs_client(events)\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"logs\", \"my-app\"])\n    assert result.exit_code == 0, result.output\n    # Three log lines, one per event.\n    lines = [ln for ln in result.output.splitlines() if ln.strip()]\n    assert len(lines) == 3\n    assert \"msg-0\" in result.output\n    assert \"msg-2\" in result.output\n    # Verify follow=False was passed.\n    kwargs = client.stream_deployment_logs.call_args.kwargs\n    assert kwargs[\"follow\"] is False\n\n\ndef test_logs_uses_complete_env_auth_without_profile(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n    _patch_no_profile_auth(monkeypatch)\n\n    runner = CliRunner()\n    client = _make_logs_client(_make_log_events(1))\n    client.project_id = \"env-project\"\n    client.base_url = DEFAULT_BASE_URL\n    client.api_key = \"env-api-key\"\n    with patch_project_client(client) as ctor:\n        result = runner.invoke(app, [\"deployments\", \"logs\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert \"msg-0\" in result.output\n    args, _ = ctor.call_args\n    assert args == (DEFAULT_BASE_URL, \"env-project\", \"env-api-key\", None)\n\n\ndef test_logs_structured_text_uses_body_timestamp_once(patched_auth: Any) -> None:\n    runner = CliRunner()\n    events = _make_log_events(1)\n    client = _make_logs_client(events)\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"logs\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    line = result.output.strip()\n    assert line.startswith(\"pod-x/app 12:00:00.000\")\n    assert line.count(\"12:00:00\") == 1\n\n\ndef test_logs_unstructured_text_uses_envelope_timestamp(patched_auth: Any) -> None:\n    runner = CliRunner()\n    event = LogEvent(\n        pod=\"pod-x\",\n        container=\"app\",\n        text=\"plain stdout line\",\n        timestamp=datetime(2026, 4, 26, 12, 0, 0, tzinfo=timezone.utc),\n    )\n    client = _make_logs_client([event])\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"logs\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert result.output.strip() == (\n        \"2026-04-26T12:00:00+00:00 pod-x/app plain stdout line\"\n    )\n\n\ndef test_logs_follow_passes_follow_true(patched_auth: Any) -> None:\n    runner = CliRunner()\n    events = _make_log_events(2)\n    client = _make_logs_client(events)\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"logs\", \"my-app\", \"--follow\"],\n        )\n    assert result.exit_code == 0, result.output\n    kwargs = client.stream_deployment_logs.call_args.kwargs\n    assert kwargs[\"follow\"] is True\n\n\ndef test_logs_json_outputs_jsonl(patched_auth: Any) -> None:\n    runner = CliRunner()\n    events = _make_log_events(2)\n    client = _make_logs_client(events)\n    with patch_project_client(client):\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"logs\", \"my-app\", \"--json\"],\n        )\n    assert result.exit_code == 0, result.output\n    lines = [ln for ln in result.output.splitlines() if ln.strip()]\n    assert len(lines) == 2\n    parsed = [json.loads(ln) for ln in lines]\n    # Each line is a LogEvent envelope.\n    for obj in parsed:\n        assert obj[\"pod\"] == \"pod-x\"\n        assert obj[\"container\"] == \"app\"\n        assert \"text\" in obj\n        assert \"timestamp\" in obj\n\n\ndef test_logs_no_events_emits_stderr_note(patched_auth: Any) -> None:\n    runner = CliRunner()\n    client = _make_logs_client([])\n    with patch_project_client(client):\n        # mix_stderr=False so we can inspect stderr separately.\n        result = runner.invoke(\n            app,\n            [\"deployments\", \"logs\", \"my-app\"],\n        )\n    assert result.exit_code == 0, result.output\n    # stderr message present in combined output (CliRunner default).\n    assert \"no logs available yet\" in result.output\n\n\ndef test_logs_rejects_zero_tail(patched_auth: Any) -> None:\n    runner = CliRunner()\n    result = runner.invoke(\n        app,\n        [\"deployments\", \"logs\", \"my-app\", \"--tail\", \"0\"],\n    )\n\n    assert result.exit_code != 0\n    assert \"Invalid value for '--tail'\" in result.output\n\n\ndef test_logs_rejects_negative_since_seconds(patched_auth: Any) -> None:\n    runner = CliRunner()\n    result = runner.invoke(\n        app,\n        [\n            \"deployments\",\n            \"logs\",\n            \"my-app\",\n            \"--since-seconds\",\n            \"-1\",\n        ],\n    )\n\n    assert result.exit_code != 0\n    assert \"Invalid value for '--since-seconds'\" in result.output\n\n\ndef test_deployments_status_command_removed() -> None:\n    \"\"\"``deployments status`` was removed in Slice A.5; ``get`` covers the use case.\"\"\"\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"--help\"])\n    assert result.exit_code == 0\n    assert \"  status \" not in result.output\n\n    result = runner.invoke(app, [\"deployments\", \"status\"])\n    assert result.exit_code != 0\n"
  },
  {
    "path": "packages/llamactl/tests/test_deployments_template_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``llamactl deployments template``.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\nimport yaml as pyyaml\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.local_context import LocalContext\n\n\ndef _patch_git(\n    monkeypatch: pytest.MonkeyPatch,\n    *,\n    is_repo: bool,\n    branch: str = \"main\",\n    remotes: list[str] | None = None,\n    git_root: Path | None = None,\n) -> None:\n    monkeypatch.setattr(\"llama_agents.cli.local_context.is_git_repo\", lambda: is_repo)\n    monkeypatch.setattr(\n        \"llama_agents.cli.local_context.list_remotes\",\n        lambda: remotes if remotes is not None else [],\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.local_context.get_current_branch\", lambda: branch\n    )\n    if git_root is not None:\n        monkeypatch.setattr(\n            \"llama_agents.cli.local_context.get_git_root\", lambda: git_root\n        )\n\n\ndef test_template_in_git_repo_emits_expected_yaml(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    (tmp_path / \"llama_deploy.yaml\").write_text(\n        \"\"\"\nname: my-app\nworkflows:\n  svc: \"module.workflow:flow\"\nrequired_env_vars: [\"API_KEY\", \"DB_URL\"]\n\"\"\".strip()\n    )\n    (tmp_path / \".env\").write_text(\"API_KEY=k\\n\")\n\n    _patch_git(\n        monkeypatch,\n        is_repo=True,\n        branch=\"develop\",\n        remotes=[\"git@github.com:user/repo.git\"],\n        git_root=tmp_path,\n    )\n\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"template\"])\n    assert result.exit_code == 0, result.output\n\n    out = result.output\n    # Identity tier: both keys commented-out; ``generate_name`` carries the\n    # config-derived name (``my-app`` from llama_deploy.yaml).\n    assert \"\\n# name: \" in out\n    assert \"# generate_name: my-app\" in out\n    # Spec block has no legacy display_name / generateName.\n    assert \"  display_name:\" not in out\n    assert \"generateName\" not in out\n    # Push-mode signal: empty repo_url, double-quoted.\n    assert 'repo_url: \"\"' in out\n    # Detected remote alternative line follows directly under the empty repo_url.\n    repo_idx = out.index('repo_url: \"\"')\n    after = out[repo_idx:]\n    next_line = after.splitlines()[1]\n    assert \"# repo_url: https://github.com/user/repo\" in next_line\n    assert \"## auto-detected from your git remotes\" in next_line\n    # Detected branch.\n    assert \"git_ref: develop\" in out\n    # Required secrets rendered as ${VAR}.\n    assert \"API_KEY: ${API_KEY}\" in out\n    assert \"DB_URL: ${DB_URL}\" in out\n    # Set-mode secret annotations are trailing comments on the secret lines.\n    api_line = next(line for line in out.splitlines() if \"API_KEY:\" in line)\n    assert api_line.startswith(\"    API_KEY: ${API_KEY}\")\n    assert api_line.endswith(\"## from your .env\")\n    assert \"    ## from your .env\" not in out, out\n    # Missing-from-.env secret carries the explicit \"not in your .env\" comment.\n    db_line = next(line for line in out.splitlines() if \"DB_URL:\" in line)\n    assert db_line.startswith(\"    DB_URL: ${DB_URL}\")\n    assert db_line.endswith(\"## not in your .env — add it before apply\")\n    assert \"Not in your .env\" not in out\n    assert \"before `apply`\" not in out\n    # No \"Optional fields\" tail block — single-pass rendering.\n    assert \"Optional fields\" not in out\n    # Output parses as YAML when comments are stripped.\n    parsed = pyyaml.safe_load(out)\n    assert parsed[\"spec\"][\"repo_url\"] == \"\"\n    assert parsed[\"spec\"][\"git_ref\"] == \"develop\"\n\n\ndef test_template_emits_local_context_warnings(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.deployment.gather_local_context\",\n        lambda: LocalContext(\n            is_git_repo=True,\n            repo_url=\"https://github.com/user/repo\",\n            git_ref=\"main\",\n            warnings=[\"Could not parse local deployment config. It may be invalid.\"],\n        ),\n    )\n\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"template\"])\n    assert result.exit_code == 0, result.output\n\n    lines = result.output.splitlines()\n    assert (\n        lines[0]\n        == \"## WARNING: Could not parse local deployment config. It may be invalid.\"\n    )\n    assert lines[1] == \"##\"\n    assert lines[2] == \"## Edit, then run: llamactl deployments apply -f <file>\"\n\n\ndef test_template_outside_git_repo_emits_compact_head_and_required_tildes(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    _patch_git(monkeypatch, is_repo=False)\n    cwd_name = tmp_path.name\n\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"template\"])\n    assert result.exit_code == 0, result.output\n\n    out = result.output\n    lines = out.splitlines()\n    assert lines[0] == \"## Edit, then run: llamactl deployments apply -f <file>\"\n    assert lines[1] == \"##\"\n    assert (\n        lines[2]\n        == \"## NOT IN A GIT REPO — set repo_url, or cd into a working tree and re-run.\"\n    )\n    assert \"═══\" not in out\n    assert \"Set repo_url below before running apply.\" not in out\n\n    # ``repo_url`` is the only required-tilde field outside a git repo;\n    # ``name`` and ``generate_name`` are commented-out (server defaults the id).\n    assert \"  repo_url: ~\" in out\n    repo_idx = out.index(\"  repo_url: ~\")\n    assert \"  ## REQUIRED.\\n\" in out[:repo_idx]\n    assert \"## Required — set before `apply`.\" not in out\n\n    # Top-level identity tier: both keys commented-out, cwd-derived defaults.\n    assert f\"\\n# name: {cwd_name}\" in out\n    assert f\"# generate_name: {cwd_name}\" in out\n    # No uncommented identity-tier line.\n    assert \"\\nname:\" not in out\n    assert \"\\ngenerate_name:\" not in out\n    # Spec block does NOT contain the legacy display_name / generateName.\n    assert \"  # generateName\" not in out\n    assert \"  display_name:\" not in out\n\n    # Other unset fields render as commented-out one-liners in declaration\n    # order inside the spec block.\n    assert \"  # deployment_file_path:\" in out\n    assert \"  # git_ref: main\" in out\n    assert \"  # suspended: false\" in out\n    assert \"  # secrets:\" in out\n    assert \"    # MY_SECRET: ${MY_SECRET}\" in out\n    assert \"  # personal_access_token:\" in out\n\n    # No \"Optional fields\" tail block.\n    assert \"Optional fields\" not in out\n\n    # YAML round-trip: required ~ parses to None; commented keys are absent.\n    parsed = pyyaml.safe_load(out)\n    assert \"name\" not in parsed\n    assert \"generate_name\" not in parsed\n    assert \"display_name\" not in parsed[\"spec\"]\n    assert parsed[\"spec\"][\"repo_url\"] is None\n\n\ndef test_template_emits_blank_lines_between_output_sections(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    _patch_git(monkeypatch, is_repo=False)\n\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"template\"])\n    assert result.exit_code == 0, result.output\n\n    lines = result.output.splitlines()\n    assert lines[0] == \"## Edit, then run: llamactl deployments apply -f <file>\"\n    assert lines[1] == \"##\"\n    assert lines[3] == \"\"\n\n    generate_name_idx = next(\n        i for i, line in enumerate(lines) if line.startswith(\"# generate_name:\")\n    )\n    spec_idx = lines.index(\"spec:\")\n    assert lines[spec_idx - 1] == \"\"\n    assert spec_idx > generate_name_idx\n\n    repo_doc_idx = next(\n        i for i, line in enumerate(lines) if line.startswith(\"  ## REQUIRED.\")\n    )\n    deploy_path_group_idx = next(\n        i\n        for i, line in enumerate(lines)\n        if line.startswith(\"  # deployment_file_path:\")\n        or \"pyproject.toml or llama_deploy.yaml\" in line\n    )\n    assert lines[deploy_path_group_idx - 1] == \"\"\n    assert deploy_path_group_idx > repo_doc_idx\n\n\ndef test_template_does_not_require_auth_profile(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    \"\"\"The command must work without an auth profile configured.\n\n    The conftest's per-worker LLAMACTL_CONFIG_DIR is empty by default — no\n    ``patched_auth`` fixture is applied here, so a successful invocation is\n    proof the command did not call ``validate_authenticated_profile``.\n    \"\"\"\n    monkeypatch.chdir(tmp_path)\n    _patch_git(monkeypatch, is_repo=False)\n\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"template\"])\n    assert result.exit_code == 0, result.output\n\n\ndef test_template_has_no_display_name_or_name_flag() -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"deployments\", \"template\", \"--help\"])\n    assert result.exit_code == 0\n    assert \"--display-name\" not in result.output\n    assert \"--name\" not in result.output\n\n\ndef test_template_advertises_template_only_on_deployments_get() -> None:\n    \"\"\"`-o template` is local to ``deployments get`` — not advertised on\n    other read commands that share the same output decorator.\"\"\"\n    runner = CliRunner()\n    for argv in (\n        [\"organizations\", \"get\", \"--help\"],\n        [\"environments\", \"get\", \"--help\"],\n        [\"deployments\", \"history\", \"--help\"],\n    ):\n        result = runner.invoke(app, argv)\n        assert result.exit_code == 0, result.output\n        assert \"template\" not in result.output.lower(), (\n            f\"{argv} should not advertise template output: {result.output}\"\n        )\n    result = runner.invoke(app, [\"deployments\", \"get\", \"--help\"])\n    assert result.exit_code == 0\n    assert \"template\" in result.output.lower()\n"
  },
  {
    "path": "packages/llamactl/tests/test_deployments_update.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``deployments update``.\n\nThe headline regression here: ``deployments update <id>`` for an internal-code\ndeployment used to call ``asyncio.run`` twice with the same ``ProjectClient``.\nThe shared httpx pool was bound to the first (closed) event loop, and the\nsecond run raised ``RuntimeError: Event loop is closed`` — surfacing in CI as\n``Error: Event loop is closed`` after the \"continuing with update using last\npushed code\" fallback when the internal git mirror returned a transient 500.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nfrom click.testing import CliRunner\nfrom conftest import make_deployment, patch_project_client\nfrom llama_agents.cli.app import app\nfrom llama_agents.core.schema.deployments import (\n    INTERNAL_CODE_REPO_SCHEME,\n    DeploymentResponse,\n)\n\n\ndef _client_mock(current: DeploymentResponse, updated: DeploymentResponse) -> MagicMock:\n    \"\"\"ProjectClient stand-in for the update flow.\"\"\"\n\n    async def _get(\n        deployment_id: str, include_events: bool = False\n    ) -> DeploymentResponse:\n        return current\n\n    async def _update(deployment_id: str, update: Any) -> DeploymentResponse:\n        return updated\n\n    client = MagicMock()\n    client.get_deployment.side_effect = _get\n    client.update_deployment.side_effect = _update\n    client.project_id = \"proj_default\"\n    client.base_url = \"http://test:8011\"\n    return client\n\n\ndef _completed_process(\n    returncode: int = 0, stdout: bytes = b\"\", stderr: bytes = b\"\"\n) -> subprocess.CompletedProcess[bytes]:\n    return subprocess.CompletedProcess(\n        args=[], returncode=returncode, stdout=stdout, stderr=stderr\n    )\n\n\ndef test_deployments_update_external_repo(patched_auth: Any) -> None:\n    \"\"\"External-repo update: no push step, single asyncio.run, exit 0.\"\"\"\n    runner = CliRunner()\n    current = make_deployment(\"my-app\", git_sha=\"a\" * 40)\n    updated = make_deployment(\"my-app\", git_sha=\"b\" * 40)\n    client = _client_mock(current, updated)\n    with patch_project_client(client):\n        result = runner.invoke(app, [\"deployments\", \"update\", \"my-app\"])\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"refreshing my-app\" in result.stderr\n    assert \"updated my-app aaaaaaa -> bbbbbbb\" in result.stderr\n    client.get_deployment.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_deployments_update_internal_repo_push_failure_does_not_abort(\n    patched_auth: Any,\n) -> None:\n    \"\"\"Push 500 → fallback path runs, deployment still updates, exit 0.\n\n    Reproduces the GitHub Actions failure mode where a transient 500 from the\n    internal git mirror caused ``Event loop is closed`` and a non-zero exit.\n    The fix routes both ``get_deployment`` and ``update_deployment`` through a\n    single ``asyncio.run`` so the httpx pool isn't reused across loops.\n\n    Patches are at the helper boundary (``configure_git_remote`` /\n    ``push_to_remote``) so the test doesn't depend on the exact shape of the\n    underlying git plumbing.\n    \"\"\"\n    runner = CliRunner()\n    current = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"a\" * 40\n    )\n    updated = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"b\" * 40\n    )\n    client = _client_mock(current, updated)\n\n    with (\n        patch_project_client(client),\n        patch(\"llama_agents.cli.commands.deployment.is_git_repo\", return_value=True),\n        patch(\n            \"llama_agents.cli.commands.deployment.has_deployment_git_remote\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.commands.deployment.configure_git_remote\",\n            return_value=\"llamaagents-my-app\",\n        ),\n        # The push itself fails with a 500 (the originally reported flake).\n        patch(\n            \"llama_agents.cli.commands.deployment.push_to_remote\",\n            return_value=_completed_process(\n                returncode=1,\n                stderr=(\n                    b\"error: RPC failed; HTTP 500\\n\"\n                    b\"fatal: the remote end hung up unexpectedly\\n\"\n                ),\n            ),\n        ),\n    ):\n        result = runner.invoke(app, [\"deployments\", \"update\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"warning: push failed:\" in result.stderr\n    assert \"warning: continuing with update using last pushed code\" in result.stderr\n    assert \"Event loop is closed\" not in result.output\n    client.get_deployment.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_deployments_update_internal_repo_skips_push_when_remote_missing(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    current = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"a\" * 40\n    )\n    updated = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"b\" * 40\n    )\n    client = _client_mock(current, updated)\n\n    with (\n        patch_project_client(client),\n        patch(\"llama_agents.cli.commands.deployment.is_git_repo\", return_value=True),\n        patch(\n            \"llama_agents.cli.commands.deployment.has_deployment_git_remote\",\n            return_value=False,\n        ),\n        patch(\n            \"llama_agents.cli.commands.deployment.configure_git_remote\"\n        ) as configure_git_remote,\n        patch(\"llama_agents.cli.commands.deployment.push_to_remote\") as push_to_remote,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"update\", \"my-app\"])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"warning: not pushing code; no llamaagents-my-app remote\" in result.stderr\n    assert \"refreshing my-app\" in result.stderr\n    configure_git_remote.assert_not_called()\n    push_to_remote.assert_not_called()\n    client.get_deployment.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_deployments_update_push_flag_forces_internal_git_push(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    current = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"a\" * 40\n    )\n    updated = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"b\" * 40\n    )\n    client = _client_mock(current, updated)\n\n    with (\n        patch_project_client(client),\n        patch(\"llama_agents.cli.commands.deployment.is_git_repo\", return_value=True),\n        patch(\n            \"llama_agents.cli.commands.deployment.has_deployment_git_remote\",\n            return_value=False,\n        ),\n        patch(\n            \"llama_agents.cli.commands.deployment.configure_git_remote\",\n            return_value=\"llamaagents-my-app\",\n        ) as configure_git_remote,\n        patch(\n            \"llama_agents.cli.commands.deployment.push_to_remote\",\n            return_value=_completed_process(),\n        ) as push_to_remote,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"update\", \"my-app\", \"--push\"])\n\n    assert result.exit_code == 0, result.output\n    configure_git_remote.assert_called_once()\n    push_to_remote.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_deployments_update_no_push_skips_internal_git_push(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    current = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"a\" * 40\n    )\n    updated = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"b\" * 40\n    )\n    client = _client_mock(current, updated)\n\n    with (\n        patch_project_client(client),\n        patch(\"llama_agents.cli.commands.deployment.is_git_repo\") as is_git_repo,\n        patch(\"llama_agents.cli.commands.deployment.push_to_remote\") as push_to_remote,\n        patch(\n            \"llama_agents.cli.commands.deployment.configure_git_remote\"\n        ) as configure_git_remote,\n    ):\n        result = runner.invoke(app, [\"deployments\", \"update\", \"my-app\", \"--no-push\"])\n\n    assert result.exit_code == 0, result.output\n    assert result.stdout == \"\"\n    assert \"refreshing my-app\" in result.stderr\n    assert \"updated my-app aaaaaaa -> bbbbbbb\" in result.stderr\n    is_git_repo.assert_not_called()\n    configure_git_remote.assert_not_called()\n    push_to_remote.assert_not_called()\n    client.get_deployment.assert_called_once()\n    client.update_deployment.assert_called_once()\n\n\ndef test_deployments_update_push_and_no_push_are_mutually_exclusive(\n    patched_auth: Any,\n) -> None:\n    runner = CliRunner()\n    current = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"a\" * 40\n    )\n    updated = make_deployment(\n        \"my-app\", repo_url=INTERNAL_CODE_REPO_SCHEME, git_sha=\"b\" * 40\n    )\n    client = _client_mock(current, updated)\n\n    with patch_project_client(client):\n        result = runner.invoke(\n            app, [\"deployments\", \"update\", \"my-app\", \"--push\", \"--no-push\"]\n        )\n\n    assert result.exit_code != 0\n    assert \"--push and --no-push are mutually exclusive\" in result.output\n    client.get_deployment.assert_not_called()\n    client.update_deployment.assert_not_called()\n"
  },
  {
    "path": "packages/llamactl/tests/test_dev_commands.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom types import SimpleNamespace\n\nimport pytest\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\n\n\n@pytest.fixture\ndef runner() -> CliRunner:\n    return CliRunner()\n\n\ndef test_dev_serve_aliases_serve(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, runner: CliRunner\n) -> None:\n    (tmp_path / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n\n    called = SimpleNamespace(prepare=False, start=False)\n\n    def _mark_prepare(*_: object, **__: object) -> None:\n        called.prepare = True\n\n    def _mark_start(*_: object, **__: object) -> None:\n        called.start = True\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.serve._maybe_inject_llama_cloud_credentials\",\n        lambda *args, **kwargs: None,\n    )\n    monkeypatch.setattr(\"llama_agents.appserver.app.prepare_server\", _mark_prepare)\n    monkeypatch.setattr(\n        \"llama_agents.appserver.app.start_server_in_target_venv\", _mark_start\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.serve._print_connection_summary\", lambda: None\n    )\n\n    result = runner.invoke(\n        app,\n        [\n            \"dev\",\n            \"serve\",\n            str(tmp_path),\n            \"--no-install\",\n            \"--no-reload\",\n            \"--no-open-browser\",\n        ],\n    )\n    assert result.exit_code == 0, result.output\n    assert called.prepare is True\n    assert called.start is True\n\n\ndef test_dev_validate_runs_inside_project_venv(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, runner: CliRunner\n) -> None:\n    called = SimpleNamespace(prepared=False, preflight=False)\n\n    # Ensure layout passes\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._ensure_project_layout\",\n        lambda deployment_file, command_name: Path(deployment_file),\n    )\n\n    # Stub creds injection\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.serve._maybe_inject_llama_cloud_credentials\",\n        lambda *args, **kwargs: None,\n    )\n\n    # Mark prepare and preflight calls\n    def _mark_prepare(*_: object, **__: object) -> None:\n        called.prepared = True\n\n    def _mark_preflight(*_: object, **__: object) -> None:\n        called.preflight = True\n\n    monkeypatch.setattr(\"llama_agents.cli.commands.dev.prepare_server\", _mark_prepare)\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.start_preflight_in_target_venv\", _mark_preflight\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\", lambda: None\n    )\n\n    result = runner.invoke(app, [\"dev\", \"validate\", str(tmp_path)])\n    assert result.exit_code == 0, result.output\n    assert called.prepared is True\n    assert called.preflight is True\n\n\ndef test_dev_validate_reports_failures(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, runner: CliRunner\n) -> None:\n    # Skip real layout checks and creds injection\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._ensure_project_layout\",\n        lambda deployment_file, command_name: Path(deployment_file),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.serve._maybe_inject_llama_cloud_credentials\",\n        lambda *args, **kwargs: None,\n    )\n    # Prepare server is a no-op\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.prepare_server\", lambda *a, **k: None\n    )\n\n    # Simulate preflight failure propagated by the helper (CLI catches and prints friendly msg)\n    import subprocess as _sp\n\n    def _raise_called_process_error(*_: object, **__: object) -> None:\n        raise _sp.CalledProcessError(1, [\"uv\", \"run\"])  # pragma: no cover - simple stub\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.start_preflight_in_target_venv\",\n        _raise_called_process_error,\n    )\n\n    result = runner.invoke(app, [\"dev\", \"validate\", str(tmp_path)])\n    assert result.exit_code != 0\n    assert \"workflow validation failed; see errors above\" in result.output\n\n\n@dataclass\nclass _Captured:\n    cmd: tuple[str, ...] | None = None\n    env: dict[str, str] | None = None\n\n\ndef test_dev_run_sets_env_and_invokes_subprocess(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, runner: CliRunner\n) -> None:\n    captured = _Captured()\n\n    def _fake_run(\n        cmd: tuple[str, ...] | list[str],\n        *,\n        env: dict[str, str] | None = None,\n        check: bool = False,\n    ) -> SimpleNamespace:\n        captured.cmd = tuple(cmd)\n        captured.env = (env or {}).copy()\n        return SimpleNamespace(returncode=0)\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._ensure_project_layout\",\n        lambda deployment_file, command_name: Path(deployment_file),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._prepare_environment\",\n        lambda deployment_file, require_cloud: (\n            SimpleNamespace(),\n            Path(\".\"),\n        ),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.parse_environment_variables\",\n        lambda config, parent: {\n            \"LOCAL_ONLY\": \"value-from-env-file\",\n            \"LLAMA_DEPLOY_PROJECT_ID\": \"proj-from-config\",\n        },\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\", lambda: None\n    )\n    monkeypatch.setattr(\"llama_agents.cli.commands.dev.subprocess.run\", _fake_run)\n\n    result = runner.invoke(\n        app, [\"dev\", \"run\", \"--deployment-file\", str(tmp_path), \"--\", \"echo\", \"hello\"]\n    )\n    assert result.exit_code == 0, result.output\n    assert captured.cmd == (\"echo\", \"hello\")\n    assert captured.env is not None\n    assert captured.env[\"LOCAL_ONLY\"] == \"value-from-env-file\"\n    assert captured.env[\"LLAMA_DEPLOY_PROJECT_ID\"] == \"proj-from-config\"\n\n\ndef test_dev_run_enables_auth_by_default(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, runner: CliRunner\n) -> None:\n    captured = SimpleNamespace(require_cloud=None)\n\n    def _capture_prepare(\n        deployment_file: Path, require_cloud: bool\n    ) -> tuple[SimpleNamespace, Path]:\n        captured.require_cloud = require_cloud\n        return (SimpleNamespace(), Path(\".\"))\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._prepare_environment\", _capture_prepare\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.parse_environment_variables\",\n        lambda config, parent: {},\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\", lambda: None\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.subprocess.run\",\n        lambda *a, **k: SimpleNamespace(returncode=0),\n    )\n\n    result = runner.invoke(\n        app, [\"dev\", \"run\", \"--deployment-file\", str(tmp_path), \"--\", \"true\"]\n    )\n    assert result.exit_code == 0, result.output\n    assert captured.require_cloud is True\n\n\ndef test_dev_run_disable_auth_flag(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, runner: CliRunner\n) -> None:\n    captured = SimpleNamespace(require_cloud=None)\n\n    def _capture_prepare(\n        deployment_file: Path, require_cloud: bool\n    ) -> tuple[SimpleNamespace, Path]:\n        captured.require_cloud = require_cloud\n        return (SimpleNamespace(), Path(\".\"))\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._prepare_environment\", _capture_prepare\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.parse_environment_variables\",\n        lambda config, parent: {},\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\", lambda: None\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.subprocess.run\",\n        lambda *a, **k: SimpleNamespace(returncode=0),\n    )\n\n    result = runner.invoke(\n        app,\n        [\n            \"dev\",\n            \"run\",\n            \"--deployment-file\",\n            str(tmp_path),\n            \"--no-auth\",\n            \"--\",\n            \"true\",\n        ],\n    )\n    assert result.exit_code == 0, result.output\n    assert captured.require_cloud is False\n\n\ndef test_dev_run_does_not_require_pyproject(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, runner: CliRunner\n) -> None:\n    # No pyproject written to tmp_path on purpose\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._prepare_environment\",\n        lambda deployment_file, require_cloud: (\n            SimpleNamespace(),\n            Path(\".\"),\n        ),\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.parse_environment_variables\",\n        lambda config, parent: {},\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\", lambda: None\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.subprocess.run\",\n        lambda *a, **k: SimpleNamespace(returncode=0),\n    )\n\n    result = runner.invoke(\n        app, [\"dev\", \"run\", \"--deployment-file\", str(tmp_path), \"--\", \"true\"]\n    )\n    assert result.exit_code == 0, result.output\n\n\ndef test_export_json_graph_defaults(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    runner: CliRunner,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    deployment_file = tmp_path / \"llama_deploy.yaml\"\n    deployment_file.write_text(\"name='x'\\n\", encoding=\"utf-8\")\n    (tmp_path / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n\n    called = SimpleNamespace(cwd=None, deployment_file=None, output=None)\n\n    def _fake_start_export(*, cwd: Path, deployment_file: Path, output: Path) -> None:\n        called.cwd = cwd\n        called.deployment_file = deployment_file\n        called.output = output\n        output.write_text(\"{}\", encoding=\"utf-8\")\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._ensure_project_layout\",\n        lambda deployment_file, command_name: tmp_path,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.serve._maybe_inject_llama_cloud_credentials\",\n        lambda *args, **kwargs: None,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.prepare_server\",\n        lambda *a, **k: None,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.start_export_json_graph_in_target_venv\",\n        _fake_start_export,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\",\n        lambda: None,\n    )\n\n    result = runner.invoke(app, [\"dev\", \"export-json-graph\"])\n    assert result.exit_code == 0, result.output\n    assert called.cwd == tmp_path\n    # Default deployment_file argument points at the project root (\".\")\n    assert called.deployment_file == tmp_path\n    assert called.output == tmp_path / \"workflows.json\"\n    assert (tmp_path / \"workflows.json\").exists()\n\n\ndef test_export_json_graph_with_output_arg(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    runner: CliRunner,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    deployment_file = tmp_path / \"llama_deploy.yaml\"\n    deployment_file.write_text(\"name='x'\\n\", encoding=\"utf-8\")\n    (tmp_path / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n\n    called = SimpleNamespace(cwd=None, deployment_file=None, output=None)\n\n    def _fake_start_export(*, cwd: Path, deployment_file: Path, output: Path) -> None:\n        called.cwd = cwd\n        called.deployment_file = deployment_file\n        called.output = output\n        output.write_text(\"{}\", encoding=\"utf-8\")\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._ensure_project_layout\",\n        lambda deployment_file, command_name: tmp_path,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.serve._maybe_inject_llama_cloud_credentials\",\n        lambda *args, **kwargs: None,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.prepare_server\",\n        lambda *a, **k: None,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.start_export_json_graph_in_target_venv\",\n        _fake_start_export,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\",\n        lambda: None,\n    )\n\n    result = runner.invoke(app, [\"dev\", \"export-json-graph\", \"--output\", \"test.json\"])\n    assert result.exit_code == 0, result.output\n    assert called.cwd == tmp_path\n    assert called.deployment_file == tmp_path\n    assert called.output == tmp_path / \"test.json\"\n    assert (tmp_path / \"test.json\").exists()\n\n\ndef test_export_json_graph_reports_failures(\n    tmp_path: Path,\n    monkeypatch: pytest.MonkeyPatch,\n    runner: CliRunner,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    deployment_file = tmp_path / \"llama_deploy.yaml\"\n    deployment_file.write_text(\"name='x'\\n\", encoding=\"utf-8\")\n    (tmp_path / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n\n    import subprocess as _sp\n\n    def _raise_called_process_error(\n        *, cwd: Path, deployment_file: Path, output: Path\n    ) -> None:\n        raise _sp.CalledProcessError(1, [\"uv\", \"run\"])\n\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._ensure_project_layout\",\n        lambda deployment_file, command_name: tmp_path,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.serve._maybe_inject_llama_cloud_credentials\",\n        lambda *args, **kwargs: None,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.prepare_server\",\n        lambda *a, **k: None,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev.start_export_json_graph_in_target_venv\",\n        _raise_called_process_error,\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.commands.dev._print_connection_summary\",\n        lambda: None,\n    )\n\n    result = runner.invoke(app, [\"dev\", \"export-json-graph\"])\n    assert result.exit_code != 0, result.output\n    assert \"workflow JSON graph export failed; see errors above\" in result.output\n\n\ndef test_export_json_graph_deployment_file_not_exist(\n    tmp_path: Path,\n    runner: CliRunner,\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    result = runner.invoke(app, [\"dev\", \"export-json-graph\", \"hello.toml\"])\n    assert result.exit_code != 0, result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_display.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for the CLI ``DeploymentDisplay`` projection model.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport pytest\nfrom conftest import make_deployment\nfrom llama_agents.cli.display import (\n    SECRET_MASK,\n    DeploymentDisplay,\n    DeploymentSpec,\n    DeploymentStatus,\n    PayloadError,\n)\nfrom llama_agents.core.schema.deployments import DeploymentCreate, DeploymentUpdate\nfrom pydantic import ValidationError\n\n_CREATE_SPEC_FIELDS = frozenset(\n    {\n        \"repo_url\",\n        \"deployment_file_path\",\n        \"git_ref\",\n        \"appserver_version\",\n        \"secrets\",\n        \"personal_access_token\",\n    }\n)\n_UPDATE_SPEC_FIELDS = frozenset(\n    {\n        \"repo_url\",\n        \"deployment_file_path\",\n        \"git_ref\",\n        \"appserver_version\",\n        \"suspended\",\n        \"secrets\",\n        \"personal_access_token\",\n    }\n)\n_HANDLED_SPEC_FIELDS = _CREATE_SPEC_FIELDS | _UPDATE_SPEC_FIELDS\n_SERVICE_UPDATE_FIELDS = frozenset(\n    {\n        \"git_sha\",\n        \"static_assets_path\",\n        \"image_tag\",\n        \"bump_to_latest_appserver\",\n        \"rebuild\",\n    }\n)\n\n\ndef test_from_response_translates_spec_fields() -> None:\n    response = make_deployment(\n        \"my-app\",\n        display_name=\"My App\",\n        repo_url=\"https://github.com/example/repo\",\n        deployment_file_path=\"llama_deploy.yaml\",\n        git_ref=\"main\",\n        appserver_version=\"0.4.2\",\n        suspended=True,\n    )\n    display = DeploymentDisplay.from_response(response)\n\n    # ``name`` is the stable id, NOT the deprecated ``r.name`` alias.\n    assert display.name == \"my-app\"\n    # ``display_name`` from the wire surfaces at the identity tier as\n    # ``generate_name`` — not on ``spec``.\n    assert display.generate_name == \"My App\"\n    assert isinstance(display.spec, DeploymentSpec)\n    assert display.spec.repo_url == \"https://github.com/example/repo\"\n    assert display.spec.deployment_file_path == \"llama_deploy.yaml\"\n    assert display.spec.git_ref == \"main\"\n    assert display.spec.appserver_version == \"0.4.2\"\n    assert display.spec.suspended is True\n\n\ndef test_from_response_status_block() -> None:\n    response = make_deployment(\n        \"my-app\",\n        git_sha=\"abc123\",\n        apiserver_url=\"http://my-app.svc.cluster.local/\",\n        warning=\"upgrade me\",\n    )\n    display = DeploymentDisplay.from_response(response)\n\n    assert isinstance(display.status, DeploymentStatus)\n    assert display.status.phase == \"Running\"\n    assert display.status.git_sha == \"abc123\"\n    assert display.status.apiserver_url == \"http://my-app.svc.cluster.local/\"\n    assert display.status.project_id == \"proj_default\"\n    assert display.status.warning == \"upgrade me\"\n\n\ndef test_from_response_secrets_masked() -> None:\n    response = make_deployment(\n        \"my-app\", secret_names=[\"LLAMA_CLOUD_API_KEY\", \"OPENAI_API_KEY\"]\n    )\n    display = DeploymentDisplay.from_response(response)\n\n    assert display.spec.secrets == {\n        \"LLAMA_CLOUD_API_KEY\": SECRET_MASK,\n        \"OPENAI_API_KEY\": SECRET_MASK,\n    }\n\n\ndef test_from_response_no_secrets_is_none() -> None:\n    response = make_deployment(\"my-app\", secret_names=[])\n    display = DeploymentDisplay.from_response(response)\n    assert display.spec.secrets is None\n\n    response = make_deployment(\"my-app\", secret_names=None)\n    display = DeploymentDisplay.from_response(response)\n    assert display.spec.secrets is None\n\n\ndef test_from_response_pat_masking() -> None:\n    response = make_deployment(\"my-app\", has_personal_access_token=True)\n    display = DeploymentDisplay.from_response(response)\n    assert display.spec.personal_access_token == SECRET_MASK\n\n    response = make_deployment(\"my-app\", has_personal_access_token=False)\n    display = DeploymentDisplay.from_response(response)\n    assert display.spec.personal_access_token is None\n\n\ndef test_spec_as_redacted_masks_secret_values() -> None:\n    spec = DeploymentSpec(\n        repo_url=\"https://github.com/example/repo\",\n        secrets={\"API_KEY\": \"sk-test\", \"DELETE_ME\": None},\n        personal_access_token=\"ghp-test\",\n    )\n\n    data = spec.as_redacted().model_dump(mode=\"json\", exclude_unset=True)\n\n    assert data == {\n        \"repo_url\": \"https://github.com/example/repo\",\n        \"secrets\": {\"API_KEY\": SECRET_MASK, \"DELETE_ME\": None},\n        \"personal_access_token\": SECRET_MASK,\n    }\n\n\ndef test_without_mask_sentinels_removes_apply_unsafe_masks() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(\n            repo_url=\"https://github.com/example/repo\",\n            secrets={\"REAL\": \"value\", \"MASKED\": SECRET_MASK},\n            personal_access_token=SECRET_MASK,\n        ),\n    )\n\n    stripped = display.without_mask_sentinels()\n\n    assert stripped.spec.secrets == {\"REAL\": \"value\"}\n    assert \"personal_access_token\" not in stripped.spec.model_fields_set\n\n\ndef test_all_deployment_spec_fields_have_apply_semantics() -> None:\n    assert set(DeploymentSpec.model_fields) == _HANDLED_SPEC_FIELDS\n\n\ndef test_create_payload_mapping_matches_wire_model_boundary() -> None:\n    assert _CREATE_SPEC_FIELDS == set(DeploymentCreate.model_fields) - {\n        \"id\",\n        \"display_name\",\n    }\n\n\ndef test_update_payload_mapping_matches_wire_model_boundary() -> None:\n    assert _UPDATE_SPEC_FIELDS == (\n        set(DeploymentUpdate.model_fields) - {\"display_name\"} - _SERVICE_UPDATE_FIELDS\n    )\n\n\ndef test_to_create_payload_with_name_sets_id() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        generate_name=\"My App\",\n        spec=DeploymentSpec(repo_url=\"https://github.com/example/repo\"),\n    )\n\n    payload = display.to_create_payload()\n\n    assert payload.id == \"my-app\"\n    assert payload.display_name == \"My App\"\n\n\ndef test_to_create_payload_without_name_uses_generate_name() -> None:\n    display = DeploymentDisplay(\n        generate_name=\"My App\",\n        spec=DeploymentSpec(repo_url=\"https://github.com/example/repo\"),\n    )\n\n    payload = display.to_create_payload()\n\n    assert payload.id is None\n    assert payload.display_name == \"My App\"\n\n\ndef test_to_create_payload_without_generate_name_raises() -> None:\n    display = DeploymentDisplay(\n        spec=DeploymentSpec(repo_url=\"https://github.com/example/repo\"),\n    )\n\n    with pytest.raises(PayloadError, match=\"generate_name\"):\n        display.to_create_payload()\n\n\ndef test_to_create_payload_suspended_raises() -> None:\n    display = DeploymentDisplay(\n        generate_name=\"My App\",\n        spec=DeploymentSpec(\n            repo_url=\"https://github.com/example/repo\",\n            suspended=True,\n        ),\n    )\n\n    with pytest.raises(PayloadError, match=\"suspended\"):\n        display.to_create_payload()\n\n\ndef test_to_create_payload_secrets_with_null_value_raises() -> None:\n    display = DeploymentDisplay(\n        generate_name=\"My App\",\n        spec=DeploymentSpec(\n            repo_url=\"https://github.com/example/repo\",\n            secrets={\"FOO\": None},\n        ),\n    )\n\n    with pytest.raises(PayloadError, match=\"null values\"):\n        display.to_create_payload()\n\n\ndef test_to_create_payload_unset_fields_default() -> None:\n    display = DeploymentDisplay(\n        generate_name=\"My App\",\n        spec=DeploymentSpec(repo_url=\"https://github.com/example/repo\"),\n    )\n\n    payload = display.to_create_payload()\n\n    assert payload.git_ref is None\n\n\ndef test_to_create_payload_empty_repo_url_passthrough() -> None:\n    display = DeploymentDisplay(\n        generate_name=\"My App\",\n        spec=DeploymentSpec(repo_url=\"\"),\n    )\n\n    payload = display.to_create_payload()\n\n    assert payload.repo_url == \"\"\n\n\ndef test_to_update_payload_unset_fields_remain_none() -> None:\n    display = DeploymentDisplay(name=\"my-app\", spec=DeploymentSpec(git_ref=\"v2\"))\n\n    payload = display.to_update_payload()\n\n    assert payload.git_ref == \"v2\"\n    assert payload.repo_url is None\n\n\ndef test_to_update_payload_null_pat_becomes_delete_sentinel() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\", spec=DeploymentSpec(personal_access_token=None)\n    )\n\n    payload = display.to_update_payload()\n\n    assert payload.personal_access_token == \"\"\n\n\ndef test_to_update_payload_secrets_null_values_preserved() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(secrets={\"FOO\": None, \"BAR\": \"new-value\"}),\n    )\n\n    payload = display.to_update_payload()\n\n    assert payload.secrets is not None\n    assert payload.secrets[\"FOO\"] is None\n    assert payload.secrets[\"BAR\"] == \"new-value\"\n\n\ndef test_to_update_payload_generate_name_maps_to_display_name() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        generate_name=\"New Name\",\n        spec=DeploymentSpec(repo_url=\"https://github.com/example/repo\"),\n    )\n\n    payload = display.to_update_payload()\n\n    assert payload.display_name == \"New Name\"\n\n\ndef test_to_output_dict_omits_empty_spec_fields() -> None:\n    response = make_deployment(\"my-app\")  # no secrets, no PAT\n    data = DeploymentDisplay.from_response(response).to_output_dict()\n\n    spec = data[\"spec\"]\n    assert \"secrets\" not in spec\n    assert \"personal_access_token\" not in spec\n    # Editable defaults still surface inside spec so apply round-trips cleanly.\n    assert data[\"name\"] == \"my-app\"\n    assert spec[\"suspended\"] is False\n\n\ndef test_to_output_dict_keeps_explicit_status_warning_null() -> None:\n    response = make_deployment(\"my-app\", warning=None)\n    data = DeploymentDisplay.from_response(response).to_output_dict()\n    assert data[\"status\"][\"warning\"] is None\n\n\ndef test_to_output_dict_preserves_mask_placeholders() -> None:\n    \"\"\"Masked secret names are preserved so JSON/YAML output shows which\n    secrets exist.  Stripping happens on the apply/parse side instead.\"\"\"\n    response = make_deployment(\n        \"my-app\",\n        secret_names=[\"KEY\"],\n        has_personal_access_token=True,\n    )\n    data = DeploymentDisplay.from_response(response).to_output_dict()\n    assert data[\"spec\"][\"secrets\"] == {\"KEY\": SECRET_MASK}\n    assert data[\"spec\"][\"personal_access_token\"] == SECRET_MASK\n\n\ndef test_to_output_dict_emits_generate_name_when_set() -> None:\n    response = make_deployment(\"my-app\", display_name=\"My App\")\n    data = DeploymentDisplay.from_response(response).to_output_dict()\n    assert data[\"generate_name\"] == \"My App\"\n    assert \"display_name\" not in data[\"spec\"]\n    assert \"display_name\" not in data\n\n\ndef test_display_model_forbids_extra_fields() -> None:\n    \"\"\"Adding an unknown wire field should fail loudly during translation.\"\"\"\n    with pytest.raises(ValidationError):\n        DeploymentDisplay.model_validate(\n            {\n                \"name\": \"x\",\n                \"spec\": {\n                    \"repo_url\": \"https://github.com/x/y\",\n                    \"deployment_file_path\": \".\",\n                },\n                \"novel_field\": \"leak\",\n            }\n        )\n\n\ndef test_spec_model_forbids_extra_fields() -> None:\n    with pytest.raises(ValidationError):\n        DeploymentSpec.model_validate(\n            {\n                \"repo_url\": \"https://github.com/x/y\",\n                \"deployment_file_path\": \".\",\n                \"novel_field\": \"leak\",\n            }\n        )\n\n\ndef test_spec_model_no_longer_accepts_display_name() -> None:\n    \"\"\"``display_name`` lives on ``DeploymentDisplay.generate_name`` now;\n    the spec model rejects it as an unknown field.\"\"\"\n    with pytest.raises(ValidationError):\n        DeploymentSpec.model_validate({\"display_name\": \"x\"})\n\n\ndef test_status_model_forbids_extra_fields() -> None:\n    with pytest.raises(ValidationError):\n        DeploymentStatus.model_validate(\n            {\"phase\": \"Running\", \"project_id\": \"proj\", \"extra_field\": \"x\"}\n        )\n\n\ndef test_no_legacy_aliases_in_output(monkeypatch: Any) -> None:\n    \"\"\"Sanity: deprecated wire aliases (``id``, ``llama_deploy_version``,\n    ``has_personal_access_token``, ``secret_names``) must not leak.\"\"\"\n    response = make_deployment(\n        \"my-app\",\n        secret_names=[\"KEY\"],\n        has_personal_access_token=True,\n        appserver_version=\"0.4.2\",\n    )\n    data = DeploymentDisplay.from_response(response).to_output_dict()\n    for forbidden in (\n        \"id\",\n        \"llama_deploy_version\",\n        \"has_personal_access_token\",\n        \"secret_names\",\n    ):\n        assert forbidden not in data\n\n\n# ---------------------------------------------------------------------------\n# Column framework — walker\n# ---------------------------------------------------------------------------\n\n\nfrom typing import Literal  # noqa: E402\n\nfrom llama_agents.cli.display import (  # noqa: E402\n    Column,\n    render_columns,\n    resolve_columns,\n)\nfrom pydantic import BaseModel  # noqa: E402\nfrom typing_extensions import Annotated  # noqa: E402\n\n\nclass _Flat(BaseModel):\n    name: Annotated[str, Column(\"NAME\")]\n    note: str  # no Column → skipped\n    age: Annotated[int, Column(\"AGE\", format=lambda v: f\"~{v}\")]\n\n\nclass _Inner(BaseModel):\n    phase: Annotated[str, Column(\"PHASE\")]\n    secret: str  # no Column → skipped\n\n\nclass _Nested(BaseModel):\n    name: Annotated[str, Column(\"NAME\")]\n    inner: _Inner\n    optional_inner: _Inner | None = None\n\n\nclass _Wide(BaseModel):\n    name: Annotated[str, Column(\"NAME\")]\n    extra: Annotated[str, Column(\"EXTRA\", wide=True)] = \"x\"\n\n\ndef test_resolve_columns_flat_model_declaration_order() -> None:\n    cols = resolve_columns(_Flat)\n    assert [c.column.header for c in cols] == [\"NAME\", \"AGE\"]\n    assert [c.path for c in cols] == [(\"name\",), (\"age\",)]\n\n\ndef test_resolve_columns_descends_nested_models() -> None:\n    cols = resolve_columns(_Nested)\n    # Outer NAME, then inner PHASE (descended), then optional_inner PHASE.\n    assert [c.column.header for c in cols] == [\"NAME\", \"PHASE\", \"PHASE\"]\n    assert [c.path for c in cols] == [\n        (\"name\",),\n        (\"inner\", \"phase\"),\n        (\"optional_inner\", \"phase\"),\n    ]\n\n\ndef test_resolve_columns_skips_field_without_column() -> None:\n    cols = resolve_columns(_Inner)\n    # ``secret`` carries no Column → excluded.\n    assert [c.column.header for c in cols] == [\"PHASE\"]\n\n\ndef test_resolve_columns_supports_multiple_independent_markers() -> None:\n    \"\"\"Forward-compat: extra markers on the same field don't perturb output.\"\"\"\n\n    class _Marker:\n        pass\n\n    class _M(BaseModel):\n        name: Annotated[str, Column(\"NAME\"), _Marker()]\n\n    cols = resolve_columns(_M)\n    assert len(cols) == 1\n    assert cols[0].column.header == \"NAME\"\n\n\ndef test_resolve_columns_rejects_duplicate_columns_on_one_field() -> None:\n    class _Bad(BaseModel):\n        name: Annotated[str, Column(\"A\"), Column(\"B\")]\n\n    with pytest.raises(ValueError, match=\"multiple Column\"):\n        resolve_columns(_Bad)\n\n\ndef test_render_columns_filters_wide(capsys: Any) -> None:\n    rows = [_Wide(name=\"a\"), _Wide(name=\"b\", extra=\"z\")]\n    render_columns(rows)\n    out = capsys.readouterr().out\n    assert \"EXTRA\" not in out\n    assert \"NAME\" in out\n\n    render_columns(rows, wide=True)\n    out = capsys.readouterr().out\n    assert \"EXTRA\" in out\n    assert \"z\" in out\n\n\ndef test_render_columns_applies_format_and_default(capsys: Any) -> None:\n    class _M(BaseModel):\n        ref: Annotated[str | None, Column(\"REF\", default=\"-\")] = None\n        age: Annotated[int, Column(\"AGE\", format=lambda v: f\"~{v}\")] = 0\n\n    render_columns([_M(ref=None, age=3), _M(ref=\"main\", age=7)])\n    out = capsys.readouterr().out\n    lines = out.strip().splitlines()\n    assert \"REF\" in lines[0]\n    # First row uses the default; format is applied to age.\n    assert \"-\" in lines[1]\n    assert \"~3\" in lines[1]\n    assert \"main\" in lines[2]\n    assert \"~7\" in lines[2]\n\n\ndef test_render_columns_propagates_none_through_missing_nested_model(\n    capsys: Any,\n) -> None:\n    class _Inner2(BaseModel):\n        phase: Annotated[str, Column(\"PHASE\", default=\"-\")]\n\n    class _Outer(BaseModel):\n        name: Annotated[str, Column(\"NAME\")]\n        inner: _Inner2 | None = None\n\n    render_columns([_Outer(name=\"a\", inner=None)])\n    out = capsys.readouterr().out\n    lines = out.strip().splitlines()\n    assert \"PHASE\" in lines[0]\n    # Missing nested model → cell renders the column's default.\n    assert \"-\" in lines[1]\n\n\ndef test_resolve_columns_handles_optional_basemodel_union() -> None:\n    cols = resolve_columns(_Nested)\n    paths = {c.path for c in cols}\n    assert (\"optional_inner\", \"phase\") in paths\n\n\ndef test_resolve_columns_is_cached() -> None:\n    \"\"\"Cache hit returns the same tuple instance.\"\"\"\n    a = resolve_columns(_Flat)\n    b = resolve_columns(_Flat)\n    assert a is b\n\n\ndef test_render_columns_literal_field_renders_value(capsys: Any) -> None:\n    class _M(BaseModel):\n        kind: Annotated[Literal[\"a\", \"b\"], Column(\"KIND\")] = \"a\"\n\n    render_columns([_M()])\n    out = capsys.readouterr().out\n    assert \"a\" in out\n"
  },
  {
    "path": "packages/llamactl/tests/test_env_and_auth_services.py",
    "content": "from __future__ import annotations\n\nimport tempfile\nfrom pathlib import Path\nfrom types import TracebackType\nfrom typing import Generator, Type\n\nimport httpx\nimport pytest\nimport respx\nfrom llama_agents.cli.config._config import ConfigManager\nfrom llama_agents.cli.config.auth_service import AuthService\nfrom llama_agents.cli.config.env_service import EnvService\nfrom llama_agents.cli.config.schema import DeviceOIDC, Environment\nfrom llama_agents.core.client.manage_client import ControlPlaneClient\nfrom llama_agents.core.schema.projects import ProjectSummary\nfrom llama_agents.core.schema.public import VersionResponse\n\n\n@pytest.fixture\ndef temp_config() -> Generator[ConfigManager, None, None]:\n    with tempfile.TemporaryDirectory() as temp_dir:\n        cfg = ConfigManager()\n        cfg.config_dir = Path(temp_dir)\n        cfg.db_path = cfg.config_dir / \"profiles.db\"\n        cfg._ensure_config_dir()\n        cfg._init_database()\n        yield cfg\n\n\n@pytest.fixture\ndef env_svc(temp_config: ConfigManager) -> EnvService:\n    return EnvService(lambda: temp_config)\n\n\ndef test_env_create_update_and_switch_clears_current_profile(\n    env_svc: EnvService, temp_config: ConfigManager\n) -> None:\n    # Seed a profile and set it current\n    env_url_a = \"https://api.a.local\"\n    env_url_b = \"https://api.b.local\"\n    temp_config.create_or_update_environment(env_url_a, requires_auth=False)\n    temp_config.create_profile(\"p1\", env_url_a, \"proj-a\")\n    temp_config.set_settings_current_profile(\"p1\")\n\n    # Creating/updating environment B should switch current env and clear current profile\n    env_svc.create_or_update_environment(\n        Environment(api_url=env_url_b, requires_auth=True)\n    )\n    assert temp_config.get_settings_current_profile_name() is None\n    assert temp_config.get_current_environment().api_url == env_url_b\n\n    # Switching to A without existing env row should raise\n    with pytest.raises(ValueError):\n        env_svc.switch_environment(\"https://does-not-exist\")\n\n    # Create env A row and then switch; should also clear profile\n    temp_config.create_or_update_environment(env_url_a, requires_auth=False)\n    temp_config.set_settings_current_profile(\"p1\")\n    switched = env_svc.switch_environment(env_url_a)\n    assert switched.api_url == env_url_a\n    assert temp_config.get_settings_current_profile_name() is None\n\n\ndef test_env_auto_update_env_persists_changes(\n    env_svc: EnvService, monkeypatch: pytest.MonkeyPatch, temp_config: ConfigManager\n) -> None:\n    # Seed an env row to be updated\n    env = Environment(api_url=\"https://api.auto.local\", requires_auth=False)\n    temp_config.create_or_update_environment(env.api_url, env.requires_auth)\n\n    # Monkeypatch version fetch to return updated values\n    def fake_fetch(self: AuthService) -> VersionResponse:\n        return VersionResponse(\n            version=\"1.2.3\", requires_auth=True, min_llamactl_version=\"0.3.0a99\"\n        )\n\n    monkeypatch.setattr(AuthService, \"fetch_server_version\", fake_fetch)\n\n    updated = env_svc.auto_update_env(env)\n    assert updated.requires_auth is True\n    assert updated.min_llamactl_version == \"0.3.0a99\"\n\n    # Verify persisted\n    from_db = temp_config.get_environment(env.api_url)\n    assert from_db is not None\n    assert from_db.requires_auth is True\n    assert from_db.min_llamactl_version == \"0.3.0a99\"\n\n\ndef test_env_probe_environment_uses_config_manager_instance(\n    env_svc: EnvService, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    def fake_fetch(self: AuthService) -> VersionResponse:\n        return VersionResponse(\n            version=\"0.0.1\", requires_auth=False, min_llamactl_version=None\n        )\n\n    monkeypatch.setattr(AuthService, \"fetch_server_version\", fake_fetch)\n\n    probed = env_svc.probe_environment(\"https://api.probe.local/\")\n    assert probed.api_url == \"https://api.probe.local\"\n    assert probed.requires_auth is False\n\n\n@pytest.fixture\ndef device_oidc() -> DeviceOIDC:\n    return DeviceOIDC(\n        device_name=\"my-device\",\n        user_id=\"user-123\",\n        email=\"test@example.com\",\n        client_id=\"client-123\",\n        discovery_url=\"https://auth.local/.well-known/openid-configuration\",\n        device_access_token=\"at-1\",\n        device_refresh_token=\"rt-1\",\n        device_id_token=\"idt-1\",\n    )\n\n\n@pytest.mark.asyncio\n@respx.mock\nasync def test_auth_middleware_refresh_updates_db(\n    temp_config: ConfigManager, device_oidc: DeviceOIDC, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    env_url = \"https://api.auth.local\"\n    temp_config.create_or_update_environment(env_url, requires_auth=True)\n    prof = temp_config.create_profile(\"p\", env_url, \"proj-1\", device_oidc=device_oidc)\n    temp_config.set_settings_current_profile(\"p\")\n\n    svc = AuthService(temp_config, Environment(api_url=env_url, requires_auth=True))\n    auth = svc.auth_middleware()\n    assert isinstance(auth, httpx.Auth)\n\n    # Mock OIDC discovery and refresh token exchange\n    discovery_url = device_oidc.discovery_url\n    token_endpoint = \"https://auth.local/oauth/token\"\n    respx.get(discovery_url).mock(\n        return_value=respx.MockResponse(200, json={\"token_endpoint\": token_endpoint})\n    )\n    respx.post(token_endpoint).mock(\n        return_value=respx.MockResponse(\n            200,\n            json={\n                \"access_token\": \"at-2\",\n                \"refresh_token\": \"rt-2\",\n                \"id_token\": \"idt-2\",\n                \"token_type\": \"Bearer\",\n            },\n        )\n    )\n\n    # Target request: first 401, then 200 with updated Authorization header\n    calls = {\"count\": 0}\n\n    def sequenced_response(req: httpx.Request) -> respx.MockResponse:\n        if calls[\"count\"] == 0:\n            calls[\"count\"] += 1\n            return respx.MockResponse(401)\n        assert req.headers.get(\"Authorization\") == \"Bearer at-2\"\n        return respx.MockResponse(200)\n\n    respx.get(\"https://example.com/x\").mock(side_effect=sequenced_response)\n\n    # Execute request through httpx with the auth middleware\n    async with httpx.AsyncClient(auth=auth) as client:\n        response = await client.get(\"https://example.com/x\")\n        assert response.status_code == 200\n\n    # Verify DB updated via on_refresh callback\n    from_db = temp_config.get_profile_by_id(prof.id)\n    assert from_db is not None and from_db.device_oidc is not None\n    assert from_db.device_oidc.device_access_token == \"at-2\"\n    assert from_db.device_oidc.device_refresh_token == \"rt-2\"\n    assert from_db.device_oidc.device_id_token == \"idt-2\"\n\n\ndef test_auth_profile_creation_helpers(\n    temp_config: ConfigManager, device_oidc: DeviceOIDC\n) -> None:\n    env_url = \"https://api.create.local\"\n    temp_config.create_or_update_environment(env_url, requires_auth=True)\n    svc = AuthService(temp_config, Environment(api_url=env_url, requires_auth=True))\n\n    # From token\n    prof1 = svc.create_profile_from_token(\"proj-1\", api_key=\"abc 123 456 789 000\")\n    assert prof1.api_url == env_url\n    assert temp_config.get_settings_current_profile_name() == prof1.name\n    # Masked name should include **** and use first and last chars from token sans spaces\n    assert \"****\" in prof1.name\n\n    # From OIDC\n    prof2 = svc.create_or_update_profile_from_oidc(\"proj-2\", device_oidc)\n    assert prof2.device_oidc is not None\n    assert temp_config.get_settings_current_profile_name() == prof2.name\n\n\ndef test_auth_fetch_server_version_and_list_projects(\n    monkeypatch: pytest.MonkeyPatch, temp_config: ConfigManager\n) -> None:\n    env_url = \"https://api.manage.local\"\n    temp_config.create_or_update_environment(env_url, requires_auth=False)\n    svc = AuthService(temp_config, Environment(api_url=env_url, requires_auth=False))\n\n    class DummyCtx:\n        async def __aenter__(self) -> \"DummyCtx\":\n            return self\n\n        async def __aexit__(\n            self,\n            exc_type: Type[BaseException] | None,\n            exc: BaseException | None,\n            tb: TracebackType | None,\n        ) -> None:\n            return None\n\n        async def server_version(self) -> VersionResponse:\n            return VersionResponse(version=\"9.9.9\", requires_auth=False)\n\n        async def list_projects(self) -> list[ProjectSummary]:\n            return [\n                ProjectSummary(\n                    project_id=\"p1\",\n                    project_name=\"Proj One\",\n                    deployment_count=0,\n                )\n            ]\n\n    def fake_ctx(\n        base_url: str, api_key: str | None = None, auth: httpx.Auth | None = None\n    ) -> DummyCtx:\n        return DummyCtx()\n\n    monkeypatch.setattr(\n        ControlPlaneClient,\n        \"ctx\",\n        classmethod(\n            lambda cls, base_url, api_key=None, auth=None: fake_ctx(\n                base_url, api_key, auth\n            )\n        ),\n    )\n\n    ver = svc.fetch_server_version()\n    assert ver.version == \"9.9.9\"\n\n    projects = svc._validate_token_and_list_projects(\"ignored-token\")\n    assert len(projects) == 1\n    assert projects[0].project_id == \"p1\"\n"
  },
  {
    "path": "packages/llamactl/tests/test_env_settings.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport pytest\nfrom conftest import clear_llama_cloud_env, set_llama_cloud_env\nfrom llama_agents.cli.config.schema import DEFAULT_ENVIRONMENT\nfrom llama_agents.cli.env_settings import LlamactlEnvSettings\n\n\ndef test_base_url_defaults_to_current_default_environment(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    clear_llama_cloud_env(monkeypatch)\n\n    settings = LlamactlEnvSettings()\n\n    assert settings.llama_cloud_base_url == DEFAULT_ENVIRONMENT.api_url\n    assert settings.normalized_base_url == DEFAULT_ENVIRONMENT.api_url.rstrip(\"/\")\n\n\ndef test_base_url_normalized_value_strips_trailing_slash(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, base_url=\"https://api.example.test/\")\n\n    settings = LlamactlEnvSettings()\n\n    assert settings.llama_cloud_base_url == \"https://api.example.test/\"\n    assert settings.normalized_base_url == \"https://api.example.test\"\n\n\ndef test_empty_api_key_and_project_id_are_incomplete(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    clear_llama_cloud_env(monkeypatch)\n    monkeypatch.setenv(\"LLAMA_CLOUD_API_KEY\", \"\")\n    monkeypatch.setenv(\"LLAMA_AGENTS_PROJECT_ID\", \"\")\n\n    settings = LlamactlEnvSettings()\n\n    assert settings.llama_cloud_api_key is None\n    assert settings.llama_agents_project_id is None\n    assert settings.has_complete_cloud_auth is False\n\n\ndef test_cloud_auth_is_complete_with_api_key_and_project_id(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, api_key=\"env-api-key\", project_id=\"env-project\")\n\n    settings = LlamactlEnvSettings()\n\n    assert settings.has_complete_cloud_auth is True\n\n\ndef test_use_profile_env_value_one_parses_as_true(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, use_profile=True)\n\n    settings = LlamactlEnvSettings()\n\n    assert settings.llama_cloud_use_profile is True\n    assert settings.cloud_auth_disabled is True\n\n\ndef test_lowercase_env_names_populate_normal_cloud_fields(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    clear_llama_cloud_env(monkeypatch)\n    monkeypatch.setenv(\"llama_cloud_api_key\", \"lower-api-key\")\n    monkeypatch.setenv(\"llama_agents_project_id\", \"lower-project\")\n    monkeypatch.setenv(\"llama_cloud_base_url\", \"https://lower.example.test\")\n    monkeypatch.setenv(\"llama_cloud_use_profile\", \"1\")\n\n    settings = LlamactlEnvSettings()\n\n    assert settings.llama_cloud_api_key == \"lower-api-key\"\n    assert settings.llama_agents_project_id == \"lower-project\"\n    assert settings.llama_cloud_base_url == \"https://lower.example.test\"\n    assert settings.llama_cloud_use_profile is True\n\n\ndef test_completion_env_var_marks_completion_active(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    set_llama_cloud_env(monkeypatch, completion=\"zsh_source\")\n\n    settings = LlamactlEnvSettings()\n\n    assert settings.llamactl_complete == \"zsh_source\"\n    assert settings.completion_active is True\n"
  },
  {
    "path": "packages/llamactl/tests/test_environments_cli.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom types import SimpleNamespace\nfrom unittest.mock import patch\n\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.config._config import Environment\n\n_INTERACTIVE_PATCH = \"llama_agents.cli.commands.environments.is_interactive_session\"\n\n\ndef test_environments_get_prints_table() -> None:\n    runner = CliRunner()\n    env1 = Environment(api_url=\"https://api1\", requires_auth=False)\n    env2 = Environment(api_url=\"https://api2\", requires_auth=True)\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.list_environments.return_value = [env1, env2]\n        mock_service.get_current_environment.return_value = env2\n        result = runner.invoke(app, [\"environments\", \"get\"])\n        assert result.exit_code == 0\n        assert \"REQUIRES_AUTH  ACTIVE\" in result.output\n        assert \"https://api1  no\" in result.output\n        assert \"https://api2  yes\" in result.output\n        assert \"https://api1\" in result.output\n        assert \"https://api2\" in result.output\n\n\ndef test_environments_get_single_environment_json() -> None:\n    runner = CliRunner()\n    env1 = Environment(api_url=\"https://api1\", requires_auth=False)\n    env2 = Environment(api_url=\"https://api2\", requires_auth=True)\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.list_environments.return_value = [env1, env2]\n        mock_service.get_current_environment.return_value = None\n        result = runner.invoke(\n            app, [\"environments\", \"get\", \"https://api2\", \"-o\", \"json\"]\n        )\n        assert result.exit_code == 0, result.output\n        assert '\"api_url\": \"https://api2\"' in result.output\n        assert result.output.strip().startswith(\"{\")\n\n\ndef test_environments_help_describes_commands() -> None:\n    result = CliRunner().invoke(app, [\"environments\", \"--help\"])\n    assert result.exit_code == 0\n    assert \"get     List environments or show one environment.\" in result.output\n    assert \"add     Probe and store an environment.\" in result.output\n    assert \"delete  Delete an environment and its profiles.\" in result.output\n    assert \"use     Set the active environment.\" in result.output\n\n\ndef test_environments_get_does_not_offer_wide_output() -> None:\n    result = CliRunner().invoke(app, [\"environments\", \"get\", \"-o\", \"wide\"])\n    assert result.exit_code != 0\n    assert \"'wide' is not one of 'text', 'json', 'yaml'\" in result.output\n\n\ndef test_environments_add_probes_and_upserts() -> None:\n    runner = CliRunner()\n    env = Environment(\n        api_url=\"https://api\", requires_auth=True, min_llamactl_version=None\n    )\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.probe_environment.return_value = env\n        result = runner.invoke(app, [\"environments\", \"add\", \"https://api/\"])\n        assert result.exit_code == 0\n        mock_service.probe_environment.assert_called_once_with(\"https://api\")\n        mock_service.create_or_update_environment.assert_called_once_with(env)\n\n\ndef test_environments_use_argument_and_interactive() -> None:\n    runner = CliRunner()\n    # Argument path\n    env = Environment(api_url=\"https://api\", requires_auth=False)\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.switch_environment.return_value = env\n        mock_service.auto_update_env.return_value = env\n        result = runner.invoke(app, [\"environments\", \"use\", \"https://api\"])\n        assert result.exit_code == 0\n        mock_service.switch_environment.assert_called_once_with(\"https://api\")\n\n    # Interactive path (select existing)\n    envs = [\n        Environment(api_url=\"https://e1\", requires_auth=False),\n        Environment(api_url=\"https://e2\", requires_auth=True),\n    ]\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.environments.select_or_exit\") as mock_select,\n        patch(_INTERACTIVE_PATCH, return_value=True),\n    ):\n        mock_service.list_environments.return_value = envs\n        mock_service.get_current_environment.return_value = envs[0]\n        mock_service.switch_environment.return_value = envs[1]\n        mock_service.auto_update_env.return_value = envs[1]\n        mock_select.return_value = SimpleNamespace(api_url=\"https://e2\")\n        result = runner.invoke(app, [\"environments\", \"use\"])\n        assert result.exit_code == 0\n        mock_service.switch_environment.assert_called_once_with(\"https://e2\")\n\n    # Missing environment should error\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.switch_environment.side_effect = ValueError(\n            \"Environment 'https://missing' not found. Add it with 'llamactl environments add <API_URL>'\"\n        )\n        result = runner.invoke(app, [\"environments\", \"use\", \"https://missing\"])\n        assert result.exit_code != 0\n        assert \"not found\" in result.output\n\n\ndef test_environments_add_interactive_selection_for_url() -> None:\n    runner = CliRunner()\n    env = Environment(api_url=\"https://x\", requires_auth=False)\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.environments.click.prompt\") as mock_prompt,\n        patch(_INTERACTIVE_PATCH, return_value=True),\n    ):\n        mock_service.get_current_environment.return_value = Environment(\n            api_url=\"https://default\", requires_auth=False\n        )\n        mock_service.probe_environment.return_value = env\n        mock_prompt.return_value = \"https://x\"\n        result = runner.invoke(app, [\"environments\", \"add\"])\n        assert result.exit_code == 0\n        mock_service.create_or_update_environment.assert_called_once_with(env)\n\n    # Non-interactive missing URL should error with hint\n    with patch(_INTERACTIVE_PATCH, return_value=False):\n        result = runner.invoke(app, [\"environments\", \"add\"])\n    assert result.exit_code != 0\n    assert \"Pass <api_url>\" in result.output\n\n\ndef test_environments_delete_argument_and_prompt() -> None:\n    runner = CliRunner()\n    # Argument path\n    with patch(\"llama_agents.cli.config.env_service.service\") as mock_service:\n        mock_service.delete_environment.return_value = True\n        result = runner.invoke(app, [\"environments\", \"delete\", \"https://api\"])\n        assert result.exit_code == 0\n        mock_service.delete_environment.assert_called_once_with(\"https://api\")\n\n    # Interactive prompt path\n    envs = [\n        Environment(api_url=\"https://e1\", requires_auth=False),\n        Environment(api_url=\"https://e2\", requires_auth=True),\n    ]\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.environments.select_or_exit\") as mock_select,\n        patch(_INTERACTIVE_PATCH, return_value=True),\n    ):\n        mock_service.list_environments.return_value = envs\n        mock_service.get_current_environment.return_value = envs[0]\n        mock_service.delete_environment.return_value = True\n        mock_select.return_value = SimpleNamespace(api_url=\"https://e2\")\n        result = runner.invoke(app, [\"environments\", \"delete\"])\n        assert result.exit_code == 0\n        mock_service.delete_environment.assert_called_once_with(\"https://e2\")\n\n    # Non-interactive missing URL should list envs and hint\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(_INTERACTIVE_PATCH, return_value=False),\n    ):\n        mock_service.list_environments.return_value = envs\n        mock_service.get_current_environment.return_value = envs[0]\n        result = runner.invoke(app, [\"environments\", \"delete\"])\n    assert result.exit_code != 0\n    assert \"Pass <api_url>\" in result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_git_push.py",
    "content": "\"\"\"Tests for llama_agents.cli.utils.git_push utilities.\"\"\"\n\nfrom __future__ import annotations\n\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock, call, patch\n\nimport pytest\nfrom llama_agents.cli.utils.git_push import (\n    _git_remote_name,\n    _set_extra_headers,\n    configure_git_remote,\n    get_api_key,\n    get_deployment_git_url,\n    has_deployment_git_remote,\n    push_to_remote,\n)\n\n# ---------------------------------------------------------------------------\n# Pure helpers\n# ---------------------------------------------------------------------------\n\n\ndef test_git_remote_name() -> None:\n    assert _git_remote_name(\"dep-123\") == \"llamaagents-dep-123\"\n\n\ndef test_get_deployment_git_url() -> None:\n    url = get_deployment_git_url(\"http://localhost:8000\", \"dep-1\")\n    assert url == \"http://localhost:8000/api/v1beta1/deployments/dep-1/git\"\n\n\ndef test_get_deployment_git_url_strips_trailing_slash() -> None:\n    url = get_deployment_git_url(\"http://localhost:8000/\", \"dep-1\")\n    assert url == \"http://localhost:8000/api/v1beta1/deployments/dep-1/git\"\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\ndef test_has_deployment_git_remote_returns_true_when_remote_exists(\n    mock_subprocess: MagicMock,\n) -> None:\n    mock_subprocess.run.return_value = MagicMock(returncode=0)\n\n    assert has_deployment_git_remote(\"dep-1\") is True\n    mock_subprocess.run.assert_called_once_with(\n        [\"git\", \"remote\", \"get-url\", \"llamaagents-dep-1\"],\n        capture_output=True,\n    )\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\ndef test_has_deployment_git_remote_returns_false_when_remote_missing(\n    mock_subprocess: MagicMock,\n) -> None:\n    mock_subprocess.run.return_value = MagicMock(returncode=2)\n\n    assert has_deployment_git_remote(\"dep-1\") is False\n\n\n# ---------------------------------------------------------------------------\n# get_api_key\n# ---------------------------------------------------------------------------\n\n\n@patch(\"llama_agents.cli.config.env_service.service\")\ndef test_get_api_key_returns_key(mock_service: MagicMock) -> None:\n    profile = SimpleNamespace(api_key=\"sk-test-abc\")\n    auth_svc = MagicMock()\n    auth_svc.get_current_profile.return_value = profile\n    mock_service.current_auth_service.return_value = auth_svc\n\n    assert get_api_key() == \"sk-test-abc\"\n\n\n@patch(\"llama_agents.cli.config.env_service.service\")\ndef test_get_api_key_returns_none_when_no_auth_required(\n    mock_service: MagicMock,\n) -> None:\n    auth_svc = MagicMock()\n    auth_svc.get_current_profile.return_value = None\n    auth_svc.env.requires_auth = False\n    mock_service.current_auth_service.return_value = auth_svc\n\n    assert get_api_key() is None\n\n\n@patch(\"llama_agents.cli.config.env_service.service\")\ndef test_get_api_key_raises_when_auth_required_but_no_profile(\n    mock_service: MagicMock,\n) -> None:\n    auth_svc = MagicMock()\n    auth_svc.get_current_profile.return_value = None\n    auth_svc.env.requires_auth = True\n    mock_service.current_auth_service.return_value = auth_svc\n\n    with pytest.raises(RuntimeError, match=\"Not authenticated\"):\n        get_api_key()\n\n\n# ---------------------------------------------------------------------------\n# _set_extra_headers\n# ---------------------------------------------------------------------------\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\ndef test_set_extra_headers_with_api_key(mock_subprocess: MagicMock) -> None:\n    _set_extra_headers(\"http://git-url\", \"sk-key\", \"proj-1\")\n\n    calls = mock_subprocess.run.call_args_list\n    # First call: unset-all existing headers\n    assert calls[0] == call(\n        [\"git\", \"config\", \"--local\", \"--unset-all\", \"http.http://git-url.extraHeader\"],\n        capture_output=True,\n    )\n    # Second call: project-id header\n    assert calls[1] == call(\n        [\n            \"git\",\n            \"config\",\n            \"--local\",\n            \"--add\",\n            \"http.http://git-url.extraHeader\",\n            \"project-id: proj-1\",\n        ],\n        check=True,\n        capture_output=True,\n    )\n    # Third call: Authorization header\n    assert calls[2] == call(\n        [\n            \"git\",\n            \"config\",\n            \"--local\",\n            \"--add\",\n            \"http.http://git-url.extraHeader\",\n            \"Authorization: Bearer sk-key\",\n        ],\n        check=True,\n        capture_output=True,\n    )\n    assert len(calls) == 3\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\ndef test_set_extra_headers_without_api_key(mock_subprocess: MagicMock) -> None:\n    _set_extra_headers(\"http://git-url\", None, \"proj-1\")\n\n    calls = mock_subprocess.run.call_args_list\n    # unset-all + project-id only, no Authorization header\n    assert len(calls) == 2\n\n\n# ---------------------------------------------------------------------------\n# configure_git_remote\n# ---------------------------------------------------------------------------\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\n@patch(\"llama_agents.cli.utils.git_push._set_extra_headers\")\ndef test_configure_git_remote_adds_new_remote(\n    mock_headers: MagicMock, mock_subprocess: MagicMock\n) -> None:\n    # Simulate remote not existing (get-url fails)\n    mock_subprocess.run.side_effect = [\n        MagicMock(returncode=2),  # git remote get-url -> not found\n        MagicMock(returncode=0),  # git remote add -> success\n    ]\n\n    name = configure_git_remote(\"http://git-url\", \"sk-key\", \"proj-1\", \"dep-1\")\n    assert name == \"llamaagents-dep-1\"\n\n    # Should call remote add (not set-url)\n    add_call = mock_subprocess.run.call_args_list[1]\n    assert add_call[0][0] == [\n        \"git\",\n        \"remote\",\n        \"add\",\n        \"llamaagents-dep-1\",\n        \"http://git-url\",\n    ]\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\n@patch(\"llama_agents.cli.utils.git_push._set_extra_headers\")\ndef test_configure_git_remote_updates_existing_remote(\n    mock_headers: MagicMock, mock_subprocess: MagicMock\n) -> None:\n    # Simulate remote already existing\n    mock_subprocess.run.side_effect = [\n        MagicMock(returncode=0),  # git remote get-url -> found\n        MagicMock(returncode=0),  # git remote set-url -> success\n    ]\n\n    name = configure_git_remote(\"http://git-url\", \"sk-key\", \"proj-1\", \"dep-1\")\n    assert name == \"llamaagents-dep-1\"\n\n    # Should call remote set-url (not add)\n    set_call = mock_subprocess.run.call_args_list[1]\n    assert set_call[0][0] == [\n        \"git\",\n        \"remote\",\n        \"set-url\",\n        \"llamaagents-dep-1\",\n        \"http://git-url\",\n    ]\n\n\n# ---------------------------------------------------------------------------\n# push_to_remote\n# ---------------------------------------------------------------------------\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\ndef test_push_to_remote(mock_subprocess: MagicMock) -> None:\n    mock_subprocess.run.return_value = MagicMock(returncode=0, stderr=b\"\")\n\n    result = push_to_remote(\n        \"my-remote\", local_ref=\"feature\", target_ref=\"refs/heads/main\"\n    )\n    assert result.returncode == 0\n\n    mock_subprocess.run.assert_called_once_with(\n        [\"git\", \"push\", \"my-remote\", \"feature:refs/heads/main\"],\n        capture_output=True,\n    )\n\n\n@patch(\"llama_agents.cli.utils.git_push.subprocess\")\ndef test_push_to_remote_uses_defaults(mock_subprocess: MagicMock) -> None:\n    mock_subprocess.run.return_value = MagicMock(returncode=0)\n\n    push_to_remote(\"my-remote\")\n\n    mock_subprocess.run.assert_called_once_with(\n        [\"git\", \"push\", \"my-remote\", \"HEAD:refs/heads/main\"],\n        capture_output=True,\n    )\n"
  },
  {
    "path": "packages/llamactl/tests/test_init_command.py",
    "content": "import json\nimport os\nimport subprocess\nfrom pathlib import Path\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\n\n_INTERACTIVE_PATCH = \"llama_agents.cli.commands.init.is_interactive_session\"\n\n\n@pytest.fixture(autouse=True)\ndef _non_interactive() -> Any:\n    \"\"\"Default all init tests to non-interactive mode.\"\"\"\n    with patch(_INTERACTIVE_PATCH, return_value=False):\n        yield\n\n\ndef test_init_help_shows_options() -> None:\n    runner = CliRunner()\n    result = runner.invoke(app, [\"init\", \"--help\"])\n    assert result.exit_code == 0\n    # Basic sanity checks on help text\n    assert \"Create a new app repository from a template\" in result.output\n    assert \"--update\" in result.output\n    assert \"--template\" in result.output\n    assert \"--dir\" in result.output\n    assert \"--force\" in result.output\n\n\ndef test_init_create_with_flags_calls_copier_and_git(tmp_path: Path) -> None:\n    runner = CliRunner()\n\n    target_dir = tmp_path / \"my-app\"\n\n    def _mock_run_copy(\n        repo_url: str, dst: Path, quiet: bool = True, *args: Any, **kwargs: Any\n    ) -> None:\n        # Simulate template copy by creating the directory\n        Path(dst).mkdir(parents=True, exist_ok=True)\n        # Create a trivial file so that `git add .` has something\n        (Path(dst) / \"README.md\").write_text(\"# App\\n\")\n\n    def _mock_subprocess_run(\n        cmd: list[str],\n        check: bool = True,\n        capture_output: bool = False,\n        text: bool = False,\n    ) -> MagicMock:\n        # Emulate success\n        return MagicMock(returncode=0, stdout=\"\")\n\n    def _mock_copy_scaffold() -> None:\n        (target_dir / \"AGENTS.md\").write_text(\"# Agents\\n\")\n        (target_dir / \".claude\").mkdir(exist_ok=True)\n        (target_dir / \".cursor\").mkdir(exist_ok=True)\n        (target_dir / \".codex\").mkdir(exist_ok=True)\n\n    with (\n        patch(\n            \"copier.run_copy\",\n            side_effect=_mock_run_copy,\n        ) as mock_copy,\n        patch(\n            \"llama_agents.cli.commands.init.subprocess.run\",\n            side_effect=_mock_subprocess_run,\n        ) as mock_subproc,\n        patch(\n            \"llama_agents.cli.commands.init._copy_scaffold\",\n            side_effect=_mock_copy_scaffold,\n        ) as mock_scaffold,\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"init\",\n                \"--template\",\n                \"basic-ui\",\n                \"--dir\",\n                str(target_dir),\n            ],\n        )\n\n        assert result.exit_code == 0, result.output\n        mock_copy.assert_called_once()\n        mock_scaffold.assert_called_once()\n        # Git commands should have been attempted\n        calls = [\" \".join(call_args.args[0]) for call_args in mock_subproc.mock_calls]\n        assert any(cmd.startswith(\"git --version\") for cmd in calls)\n        assert target_dir.exists()\n        assert \"uvx llamactl deployments create\" in result.output\n        assert \"uvx llamactl deploy create\" not in result.output\n        # Symlinks to AGENTS.md should be created by the init flow\n        claude_link = target_dir / \"CLAUDE.md\"\n        gemini_link = target_dir / \"GEMINI.md\"\n        agents_file = target_dir / \"AGENTS.md\"\n        assert agents_file.exists()\n        assert claude_link.exists()\n        assert claude_link.is_symlink()\n        assert gemini_link.is_symlink()\n        assert claude_link.resolve() == agents_file.resolve()\n        assert gemini_link.resolve() == agents_file.resolve()\n\n\ndef test_init_update_calls_copier_run_update() -> None:\n    runner = CliRunner()\n\n    with (\n        patch(\n            \"copier.run_update\",\n            return_value=None,\n        ) as mock_update,\n        patch(\n            \"llama_agents.cli.commands.init.subprocess.run\",\n            return_value=MagicMock(returncode=0, stdout=\"\"),\n        ),\n    ):\n        result = runner.invoke(app, [\"init\", \"--update\"])\n        assert result.exit_code == 0, result.output\n        mock_update.assert_called_once()\n\n\ndef test_init_handles_missing_git_gracefully(tmp_path: Path) -> None:\n    runner = CliRunner()\n\n    target_dir = tmp_path / \"my-app-missing-git\"\n\n    def _mock_subprocess_run(\n        cmd: list[str],\n        check: bool = True,\n        capture_output: bool = False,\n        text: bool = False,\n    ) -> MagicMock:\n        if cmd[:2] == [\"git\", \"--version\"]:\n            raise FileNotFoundError(\"git not found\")\n        return MagicMock(returncode=0, stdout=\"\")\n\n    with patch(\n        \"llama_agents.cli.commands.init.subprocess.run\",\n        side_effect=_mock_subprocess_run,\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"init\",\n                \"--template\",\n                \"basic-ui\",\n                \"--dir\",\n                str(target_dir),\n            ],\n        )\n\n        assert result.exit_code == 1, result.output\n        assert \"git is required\" in result.output\n        assert len(result.output.split(\"\\n\")) < 6\n        assert not target_dir.exists()\n\n\ndef test_init_handles_git_init_failure_gracefully(tmp_path: Path) -> None:\n    runner = CliRunner()\n\n    target_dir = tmp_path / \"my-app-git-fails\"\n\n    def _mock_run_copy(\n        repo_url: str, dst: Path, quiet: bool = True, *args: Any, **kwargs: Any\n    ) -> None:\n        Path(dst).mkdir(parents=True, exist_ok=True)\n        (Path(dst) / \"README.md\").write_text(\"# App\\n\")\n\n    def _mock_subprocess_run(\n        cmd: list[str],\n        check: bool = True,\n        capture_output: bool = False,\n        text: bool = False,\n    ) -> MagicMock:\n        if cmd[:2] == [\"git\", \"--version\"]:\n            return MagicMock(returncode=0, stdout=\"git version 2.x\\n\")\n        if cmd[:2] == [\"git\", \"init\"]:\n            raise subprocess.CalledProcessError(\n                returncode=1, cmd=cmd, stderr=b\"fatal: not a git repo\\n\"\n            )\n        return MagicMock(returncode=0, stdout=\"\")\n\n    with (\n        patch(\"copier.run_copy\", side_effect=_mock_run_copy),\n        patch(\n            \"llama_agents.cli.commands.init.subprocess.run\",\n            side_effect=_mock_subprocess_run,\n        ),\n        patch(\"llama_agents.cli.commands.init._copy_scaffold\") as mock_scaffold,\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"init\",\n                \"--template\",\n                \"basic-ui\",\n                \"--dir\",\n                str(target_dir),\n            ],\n        )\n\n        assert result.exit_code == 0, result.output\n        assert target_dir.exists()\n        assert \"warning: skipping git initialization\" in result.output\n        mock_scaffold.assert_called_once()\n\n\ndef test_init_skips_git_init_when_inside_parent_repo(tmp_path: Path) -> None:\n    runner = CliRunner()\n\n    target_dir = tmp_path / \"my-app-inside-repo\"\n\n    def _mock_run_copy(\n        repo_url: str, dst: Path, quiet: bool = True, *args: Any, **kwargs: Any\n    ) -> None:\n        Path(dst).mkdir(parents=True, exist_ok=True)\n        (Path(dst) / \"README.md\").write_text(\"# App\\n\")\n\n    def _mock_subprocess_run(\n        cmd: list[str],\n        check: bool = True,\n        capture_output: bool = False,\n        text: bool = False,\n    ) -> MagicMock:\n        # Simulate: git is available, and current directory is inside a repo\n        if cmd[:2] == [\"git\", \"--version\"]:\n            return MagicMock(returncode=0, stdout=\"git version 2.x\\n\")\n        if cmd[:3] == [\"git\", \"rev-parse\", \"--is-inside-work-tree\"]:\n            return MagicMock(returncode=0, stdout=\"true\\n\")\n        # These commands should NOT be called when inside a parent repo\n        if cmd[:2] in ([\"git\", \"init\"], [\"git\", \"add\"], [\"git\", \"commit\"]):\n            raise AssertionError(\n                \"git init/add/commit should not be called inside parent repo\"\n            )\n        return MagicMock(returncode=0, stdout=\"\")\n\n    with (\n        patch(\"copier.run_copy\", side_effect=_mock_run_copy),\n        patch(\n            \"llama_agents.cli.commands.init.subprocess.run\",\n            side_effect=_mock_subprocess_run,\n        ),\n        patch(\"llama_agents.cli.commands.init._copy_scaffold\"),\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"init\",\n                \"--template\",\n                \"basic-ui\",\n                \"--dir\",\n                str(target_dir),\n            ],\n        )\n\n        assert result.exit_code == 0, result.output\n        assert target_dir.exists()\n        assert (\n            \"warning: skipping git initialization: existing Git repository\"\n            in result.output\n        )\n\n\ndef test_copy_scaffold(tmp_path: Path) -> None:\n    from llama_agents.cli.commands.init import _copy_scaffold\n\n    os.chdir(tmp_path)\n    _copy_scaffold()\n\n    # AGENTS.md\n    content = (tmp_path / \"AGENTS.md\").read_text()\n    assert \"developers.llamaindex.ai/mcp\" in content\n    assert \"search_docs\" in content\n\n    # Claude Code: .mcp.json + .claude/settings.json\n    mcp_json = json.loads((tmp_path / \".mcp.json\").read_text())\n    assert \"llama-index-docs\" in mcp_json[\"mcpServers\"]\n\n    claude_settings = json.loads((tmp_path / \".claude\" / \"settings.json\").read_text())\n    assert \"mcp__llama-index-docs\" in claude_settings[\"permissions\"][\"allow\"]\n    assert claude_settings[\"enableAllProjectMcpServers\"] is True\n\n    # Cursor config\n    cursor_mcp = json.loads((tmp_path / \".cursor\" / \"mcp.json\").read_text())\n    assert \"llama-index-docs\" in cursor_mcp[\"mcpServers\"]\n\n    # Codex config\n    codex_toml = (tmp_path / \".codex\" / \"config.toml\").read_text()\n    assert \"llama-index-docs\" in codex_toml\n\n\ndef test_init_non_interactive_requires_template(tmp_path: Path) -> None:\n    \"\"\"Test that init in non-interactive mode requires a template to be specified.\"\"\"\n    runner = CliRunner()\n\n    result = runner.invoke(\n        app,\n        [\"init\"],\n    )\n\n    # Should exit with error, listing templates and hinting at --template\n    assert result.exit_code == 1\n    assert \"basic\" in result.output\n    assert \"Pass --template to choose one\" in result.output\n\n\ndef test_init_non_interactive_defaults_directory(tmp_path: Path) -> None:\n    \"\"\"Test that init defaults to template name for directory in non-interactive mode.\"\"\"\n    runner = CliRunner()\n\n    def _mock_run_copy(\n        repo_url: str, dst: Path, quiet: bool = True, *args: Any, **kwargs: Any\n    ) -> None:\n        Path(dst).mkdir(parents=True, exist_ok=True)\n        (Path(dst) / \"README.md\").write_text(\"# App\\n\")\n\n    def _mock_subprocess_run(\n        cmd: list[str],\n        check: bool = True,\n        capture_output: bool = False,\n        text: bool = False,\n    ) -> MagicMock:\n        return MagicMock(returncode=0, stdout=\"\")\n\n    with (\n        patch(\"copier.run_copy\", side_effect=_mock_run_copy),\n        patch(\n            \"llama_agents.cli.commands.init.subprocess.run\",\n            side_effect=_mock_subprocess_run,\n        ),\n        patch(\"llama_agents.cli.commands.init._copy_scaffold\"),\n    ):\n        # Change to tmp_path to avoid creating directories in the real workspace\n        original_cwd = Path.cwd()\n        try:\n            os.chdir(tmp_path)\n\n            result = runner.invoke(\n                app,\n                [\"init\", \"--template\", \"basic-ui\"],\n            )\n\n            assert result.exit_code == 0, result.output\n            # Should default to template name\n            assert (tmp_path / \"basic-ui\").exists()\n            assert \"no directory provided; defaulting to basic-ui\" in result.output\n        finally:\n            os.chdir(original_cwd)\n\n\ndef test_init_force_flag_skips_confirmation(tmp_path: Path) -> None:\n    \"\"\"Test that --force flag skips overwrite confirmation.\"\"\"\n    runner = CliRunner()\n\n    target_dir = tmp_path / \"my-app\"\n    target_dir.mkdir()\n    (target_dir / \"existing.txt\").write_text(\"existing content\")\n\n    def _mock_run_copy(\n        repo_url: str, dst: Path, quiet: bool = True, *args: Any, **kwargs: Any\n    ) -> None:\n        Path(dst).mkdir(parents=True, exist_ok=True)\n        (Path(dst) / \"README.md\").write_text(\"# App\\n\")\n\n    def _mock_subprocess_run(\n        cmd: list[str],\n        check: bool = True,\n        capture_output: bool = False,\n        text: bool = False,\n    ) -> MagicMock:\n        return MagicMock(returncode=0, stdout=\"\")\n\n    with (\n        patch(\"copier.run_copy\", side_effect=_mock_run_copy),\n        patch(\n            \"llama_agents.cli.commands.init.subprocess.run\",\n            side_effect=_mock_subprocess_run,\n        ),\n        patch(\"llama_agents.cli.commands.init._copy_scaffold\"),\n    ):\n        result = runner.invoke(\n            app,\n            [\n                \"init\",\n                \"--template\",\n                \"basic-ui\",\n                \"--dir\",\n                str(target_dir),\n                \"--force\",\n            ],\n        )\n\n        assert result.exit_code == 0, result.output\n        # Directory should be overwritten\n        assert not (target_dir / \"existing.txt\").exists()\n        assert (target_dir / \"README.md\").exists()\n\n\ndef test_init_existing_directory_no_force_exits(tmp_path: Path) -> None:\n    \"\"\"Test that init exits when directory exists without --force in non-interactive mode.\"\"\"\n    runner = CliRunner()\n\n    target_dir = tmp_path / \"my-app\"\n    target_dir.mkdir()\n\n    result = runner.invoke(\n        app,\n        [\n            \"init\",\n            \"--template\",\n            \"basic-ui\",\n            \"--dir\",\n            str(target_dir),\n        ],\n    )\n\n    # Should exit with error\n    assert result.exit_code == 1\n    assert \"--force\" in result.output or \"force\" in result.output.lower()\n"
  },
  {
    "path": "packages/llamactl/tests/test_interactive.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nfrom unittest.mock import patch\n\nimport click\nimport pytest\nfrom llama_agents.cli.interactive import require_or_list_choices, select_or_exit\n\n\ndef test_select_or_exit_interactive_returns_selected_item() -> None:\n    with patch(\"llama_agents.cli.interactive._blessed_select\", return_value=1):\n        selected = select_or_exit(\n            [(1, \"one\"), (2, \"two\")],\n            \"Pick one\",\n            \"--item\",\n            interactive=True,\n        )\n\n    assert selected == 2\n\n\ndef test_select_or_exit_interactive_cancel_raises() -> None:\n    with patch(\"llama_agents.cli.interactive._blessed_select\", return_value=None):\n        with pytest.raises(click.ClickException, match=\"Cancelled\"):\n            select_or_exit([(1, \"one\")], \"Pick one\", \"--item\", interactive=True)\n\n\ndef test_select_or_exit_non_interactive_lists_items_and_hints(\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    with pytest.raises(click.ClickException) as exc_info:\n        select_or_exit(\n            [(1, \"one\"), (2, \"two\")],\n            \"Pick one\",\n            \"--item\",\n            hint_command=\"llamactl things list\",\n            interactive=False,\n        )\n\n    captured = capsys.readouterr()\n    assert \"Pick one\" in captured.err\n    assert \"- one\" in captured.err\n    assert \"- two\" in captured.err\n    assert \"--item\" in str(exc_info.value)\n    assert \"llamactl things list\" in str(exc_info.value)\n\n\ndef test_select_or_exit_empty_raises_custom_message() -> None:\n    with pytest.raises(click.ClickException, match=\"Nothing available\"):\n        select_or_exit(\n            [],\n            \"Pick one\",\n            \"--item\",\n            empty_message=\"Nothing available\",\n            interactive=True,\n        )\n\n\ndef test_select_or_exit_falls_back_when_blessed_unavailable(\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    with patch(\n        \"llama_agents.cli.interactive._blessed_select\",\n        side_effect=ImportError(\"no blessed\"),\n    ):\n        with pytest.raises(click.ClickException, match=\"--item\"):\n            select_or_exit(\n                [(1, \"one\"), (2, \"two\")],\n                \"Pick one\",\n                \"--item\",\n                interactive=True,\n            )\n\n    captured = capsys.readouterr()\n    assert \"- one\" in captured.err\n\n\ndef test_require_or_list_choices_lists_and_hints(\n    capsys: pytest.CaptureFixture[str],\n) -> None:\n    with pytest.raises(click.ClickException) as exc_info:\n        require_or_list_choices(\n            [(\"abc123\", \"abc123 - running\"), (\"def456\", \"def456 - stopped\")],\n            hint_command=\"llamactl deployments delete <deployment_id>\",\n        )\n\n    captured = capsys.readouterr()\n    assert \"abc123 - running\" in captured.err\n    assert \"def456 - stopped\" in captured.err\n    assert \"llamactl deployments delete <deployment_id>\" in str(exc_info.value)\n\n\ndef test_require_or_list_choices_empty_raises() -> None:\n    with pytest.raises(click.ClickException, match=\"No deployments\"):\n        require_or_list_choices(\n            [],\n            hint_command=\"llamactl deployments delete <deployment_id>\",\n            empty_message=\"No deployments\",\n        )\n"
  },
  {
    "path": "packages/llamactl/tests/test_local_context.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``cli.local_context.gather_local_context``.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.cli.local_context import (\n    LocalContext,\n    gather_local_context,\n    normalize_git_url_to_http,\n)\n\n\ndef test_normalize_https_strip_creds_and_suffix() -> None:\n    assert (\n        normalize_git_url_to_http(\"https://user:pass@github.com/user/repo.git\")\n        == \"https://github.com/user/repo\"\n    )\n\n\ndef test_normalize_scp_style() -> None:\n    assert (\n        normalize_git_url_to_http(\"git@github.com:user/repo.git\")\n        == \"https://github.com/user/repo\"\n    )\n\n\ndef test_normalize_strips_ssh_port() -> None:\n    assert (\n        normalize_git_url_to_http(\"ssh://git@bitbucket.org:7999/team/repo.git\")\n        == \"https://bitbucket.org/team/repo\"\n    )\n\n\ndef test_normalize_preserves_https_port() -> None:\n    assert (\n        normalize_git_url_to_http(\"https://git.example.com:8443/org/repo.git\")\n        == \"https://git.example.com:8443/org/repo\"\n    )\n\n\ndef test_normalize_bare_host_path() -> None:\n    assert (\n        normalize_git_url_to_http(\"gitlab.com/group/sub/repo.git\")\n        == \"https://gitlab.com/group/sub/repo\"\n    )\n\n\ndef test_normalize_rewrites_http_to_https() -> None:\n    assert (\n        normalize_git_url_to_http(\"http://github.com/user/repo\")\n        == \"https://github.com/user/repo\"\n    )\n\n\ndef test_normalize_scp_style_no_dot_git_suffix() -> None:\n    assert (\n        normalize_git_url_to_http(\"github.com:user/repo\")\n        == \"https://github.com/user/repo\"\n    )\n\n\ndef testpick_preferred_remote_prefers_github() -> None:\n    from llama_agents.cli.local_context import pick_preferred_remote\n\n    assert (\n        pick_preferred_remote(\n            [\n                \"ssh://git@bitbucket.org/team/repo.git\",\n                \"git@github.com:user/repo.git\",\n            ]\n        )\n        == \"https://github.com/user/repo\"\n    )\n\n\ndef testpick_preferred_remote_dedupes_and_handles_empty() -> None:\n    from llama_agents.cli.local_context import pick_preferred_remote\n\n    assert pick_preferred_remote([]) is None\n    assert (\n        pick_preferred_remote(\n            [\n                \"git@github.com:user/repo.git\",\n                \"https://github.com/user/repo.git\",\n            ]\n        )\n        == \"https://github.com/user/repo\"\n    )\n\n\ndef test_gather_outside_git_repo_returns_safe_defaults(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    monkeypatch.setattr(\"llama_agents.cli.local_context.is_git_repo\", lambda: False)\n\n    ctx = gather_local_context()\n\n    assert isinstance(ctx, LocalContext)\n    assert ctx.is_git_repo is False\n    assert ctx.repo_url is None\n    assert ctx.git_ref is None\n    assert ctx.available_secrets == {}\n    assert ctx.required_secret_names == []\n    assert ctx.deployment_file_path is None\n    # Missing config files → silent default; no warning is emitted.\n    assert ctx.warnings == []\n\n\ndef test_gather_in_git_repo_with_env_and_config(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    (tmp_path / \"llama_deploy.yaml\").write_text(\n        \"\"\"\nname: my-app\nworkflows:\n  svc: \"module.workflow:flow\"\nrequired_env_vars: [\"API_KEY\", \"PORT\"]\n\"\"\".strip()\n    )\n    (tmp_path / \".env\").write_text(\"API_KEY=secret\\nPORT=8080\\n\")\n\n    monkeypatch.setattr(\"llama_agents.cli.local_context.is_git_repo\", lambda: True)\n    monkeypatch.setattr(\n        \"llama_agents.cli.local_context.list_remotes\",\n        lambda: [\n            \"ssh://git@bitbucket.org/team/repo.git\",\n            \"git@github.com:user/repo.git\",\n        ],\n    )\n    monkeypatch.setattr(\n        \"llama_agents.cli.local_context.get_current_branch\", lambda: \"develop\"\n    )\n    monkeypatch.setattr(\"llama_agents.cli.local_context.get_git_root\", lambda: tmp_path)\n\n    ctx = gather_local_context()\n\n    assert ctx.is_git_repo is True\n    # github URL is preferred over bitbucket.\n    assert ctx.repo_url == \"https://github.com/user/repo\"\n    assert ctx.git_ref == \"develop\"\n    assert ctx.generate_name == \"my-app\"\n    assert ctx.available_secrets == {\"API_KEY\": \"secret\", \"PORT\": \"8080\"}\n    assert sorted(ctx.required_secret_names) == [\"API_KEY\", \"PORT\"]\n    # cwd == git_root → no nested deployment_file_path.\n    assert ctx.deployment_file_path is None\n\n\ndef test_gather_subdir_of_git_repo_records_relative_path(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    sub = tmp_path / \"services\" / \"app\"\n    sub.mkdir(parents=True)\n    (sub / \"llama_deploy.yaml\").write_text(\n        \"\"\"\nname: app\nworkflows:\n  svc: \"x.y:flow\"\n\"\"\".strip()\n    )\n    monkeypatch.chdir(sub)\n\n    monkeypatch.setattr(\"llama_agents.cli.local_context.is_git_repo\", lambda: True)\n    monkeypatch.setattr(\"llama_agents.cli.local_context.list_remotes\", lambda: [])\n    monkeypatch.setattr(\n        \"llama_agents.cli.local_context.get_current_branch\", lambda: \"main\"\n    )\n    monkeypatch.setattr(\"llama_agents.cli.local_context.get_git_root\", lambda: tmp_path)\n\n    ctx = gather_local_context()\n\n    assert ctx.deployment_file_path == str(Path(\"services\") / \"app\")\n\n\ndef test_gather_with_invalid_config_records_warning(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.chdir(tmp_path)\n    (tmp_path / \"llama_deploy.yaml\").write_text(\"not: [valid: yaml\")\n\n    monkeypatch.setattr(\"llama_agents.cli.local_context.is_git_repo\", lambda: False)\n    ctx = gather_local_context()\n    assert any(\"Could not parse\" in w for w in ctx.warnings)\n\n\ndef test_gather_skips_default_named_config(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    \"\"\"When the config name is the default, ``generate_name`` is left unset.\"\"\"\n    monkeypatch.chdir(tmp_path)\n    (tmp_path / \"llama_deploy.yaml\").write_text(\n        \"\"\"\nworkflows:\n  svc: \"x.y:flow\"\n\"\"\".strip()\n    )\n    monkeypatch.setattr(\"llama_agents.cli.local_context.is_git_repo\", lambda: False)\n    ctx = gather_local_context()\n    assert ctx.generate_name is None\n"
  },
  {
    "path": "packages/llamactl/tests/test_log_format.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for the structlog body parser and plain renderer.\n\nPhase 2 of the llamactl Slice A redesign.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom llama_agents.cli.log_format import parse_log_body, render_plain\n\n\ndef test_parse_plain_string_passes_through() -> None:\n    parsed = parse_log_body(\"plain stdout line\")\n    assert parsed.structured is False\n    assert parsed.event == \"plain stdout line\"\n    assert render_plain(parsed) == \"plain stdout line\"\n\n\ndef test_parse_invalid_json_passes_through() -> None:\n    parsed = parse_log_body(\"{not json\")\n    assert parsed.structured is False\n    assert render_plain(parsed) == \"{not json\"\n\n\ndef test_parse_dict_without_event_passes_through() -> None:\n    parsed = parse_log_body('{\"level\": \"info\"}')\n    assert parsed.structured is False\n\n\ndef test_parse_structlog_extracts_fields() -> None:\n    line = (\n        '{\"event\": \"request done\", \"level\": \"info\", '\n        '\"timestamp\": \"2026-04-26T12:34:56.789Z\", '\n        '\"logger\": \"app.api\", \"request_id\": \"abc\", \"duration_ms\": 42}'\n    )\n    parsed = parse_log_body(line)\n    assert parsed.structured is True\n    assert parsed.event == \"request done\"\n    assert parsed.level == \"info\"\n    assert parsed.logger == \"app.api\"\n    assert parsed.request_id == \"abc\"\n    assert parsed.extras == {\"duration_ms\": 42}\n\n\ndef test_render_plain_structured_layout() -> None:\n    parsed = parse_log_body(\n        '{\"event\": \"ok\", \"level\": \"warning\", '\n        '\"timestamp\": \"2026-04-26T12:34:56.000Z\", \"logger\": \"x\"}'\n    )\n    rendered = render_plain(parsed)\n    # Time portion only (not the full ISO date)\n    assert \"12:34:56.000\" in rendered\n    assert \"WARNING\" in rendered\n    assert \"x\" in rendered\n    assert \"ok\" in rendered\n    # No date prefix\n    assert \"2026-04-26\" not in rendered\n"
  },
  {
    "path": "packages/llamactl/tests/test_migrations_concurrency.py",
    "content": "\"\"\"Tests for concurrent migration protection in _migrations.py.\"\"\"\n\nimport sqlite3\nimport tempfile\nimport threading\nimport time\nfrom collections.abc import Generator\nfrom pathlib import Path\n\nimport pytest\nfrom llama_agents.cli.config._migrations import (\n    _file_lock,\n    _iter_migration_files,\n    _parse_target_version,\n    run_migrations,\n)\n\n\ndef _latest_migration_version() -> int:\n    \"\"\"Determine the latest schema version from migration files.\"\"\"\n    versions = []\n    for path in _iter_migration_files():\n        v = _parse_target_version(path.read_text())\n        if v is not None:\n            versions.append(v)\n    return max(versions)\n\n\n@pytest.fixture\ndef temp_db() -> Generator[Path, None, None]:\n    \"\"\"Create a temporary database file.\"\"\"\n    with tempfile.TemporaryDirectory() as temp_dir:\n        db_path = Path(temp_dir) / \"test.db\"\n        yield db_path\n\n\ndef test_file_lock_serializes_access(temp_db: Path) -> None:\n    \"\"\"Test that file lock prevents concurrent access.\"\"\"\n    lock_path = temp_db.with_suffix(\".db.lock\")\n    results: list[str] = []\n    lock_held = threading.Event()\n\n    def thread_a() -> None:\n        with _file_lock(lock_path):\n            results.append(\"a_start\")\n            lock_held.set()\n            time.sleep(0.1)\n            results.append(\"a_end\")\n\n    def thread_b() -> None:\n        lock_held.wait()  # Wait for thread_a to acquire lock\n        with _file_lock(lock_path):\n            results.append(\"b_start\")\n            results.append(\"b_end\")\n\n    t1 = threading.Thread(target=thread_a)\n    t2 = threading.Thread(target=thread_b)\n\n    t1.start()\n    t2.start()\n    t1.join()\n    t2.join()\n\n    # Thread B should only start after thread A finishes\n    assert results == [\"a_start\", \"a_end\", \"b_start\", \"b_end\"]\n\n\ndef test_run_migrations_with_lock_prevents_duplicate_migrations(temp_db: Path) -> None:\n    \"\"\"Test that concurrent migrations don't cause duplicate schema changes.\"\"\"\n    # Create the initial database with schema version 0\n    with sqlite3.connect(temp_db) as conn:\n        conn.execute(\"PRAGMA user_version=0\")\n        conn.commit()\n\n    errors: list[Exception] = []\n    success_count: list[int] = []\n    barrier = threading.Barrier(2)\n\n    def run_migration() -> None:\n        try:\n            barrier.wait()  # Synchronize both threads to start at the same time\n            with sqlite3.connect(temp_db) as conn:\n                run_migrations(conn, temp_db)\n            success_count.append(1)\n        except Exception as e:\n            errors.append(e)\n\n    t1 = threading.Thread(target=run_migration)\n    t2 = threading.Thread(target=run_migration)\n\n    t1.start()\n    t2.start()\n    t1.join()\n    t2.join()\n\n    # Both should complete without errors\n    assert len(errors) == 0, f\"Errors occurred: {errors}\"\n    assert len(success_count) == 2\n\n    # Verify the schema version is correct\n    with sqlite3.connect(temp_db) as conn:\n        version = conn.execute(\"PRAGMA user_version\").fetchone()[0]\n        # Should be at version 2 (the latest migration)\n        assert version == _latest_migration_version()\n\n\ndef test_run_migrations_without_lock_path_still_works(temp_db: Path) -> None:\n    \"\"\"Test that migrations work when db_path is not provided (no locking).\"\"\"\n    with sqlite3.connect(temp_db) as conn:\n        conn.execute(\"PRAGMA user_version=0\")\n        conn.commit()\n        run_migrations(conn, db_path=None)\n\n    with sqlite3.connect(temp_db) as conn:\n        version = conn.execute(\"PRAGMA user_version\").fetchone()[0]\n        assert version == _latest_migration_version()\n"
  },
  {
    "path": "packages/llamactl/tests/test_organizations_cli.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``llamactl organizations get`` output modes.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom unittest.mock import MagicMock, patch\n\nimport yaml\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\nfrom llama_agents.core.schema.projects import OrgSummary\n\n\ndef test_organizations_get_text_lists_orgs() -> None:\n    runner = CliRunner()\n    orgs = [\n        OrgSummary(org_id=\"org-a\", org_name=\"Acme\", is_default=True),\n        OrgSummary(org_id=\"org-b\", org_name=\"Beta\"),\n    ]\n    with (\n        patch(\n            \"llama_agents.cli.commands.organizations.probe_organizations_support\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.commands.organizations._list_organizations\",\n            return_value=orgs,\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"organizations\", \"get\"])\n    assert result.exit_code == 0, result.output\n    assert \"ORG_ID\" in result.output\n    assert \"NAME\" in result.output\n    assert \"DEFAULT\" in result.output\n    assert \"ACTIVE\" in result.output\n    assert result.output.splitlines()[0].startswith(\"NAME\")\n    assert \"org-a\" in result.output\n    assert \"Acme\" in result.output\n    assert \"yes\" in result.output\n    assert \"no\" in result.output\n    # ANSI / Rich markup should not leak.\n    assert \"\\x1b[\" not in result.output\n\n\ndef test_organizations_get_json() -> None:\n    runner = CliRunner()\n    orgs = [\n        OrgSummary(org_id=\"org-a\", org_name=\"Acme\", is_default=True),\n        OrgSummary(org_id=\"org-b\", org_name=\"Beta\"),\n    ]\n    with (\n        patch(\n            \"llama_agents.cli.commands.organizations.probe_organizations_support\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.commands.organizations._list_organizations\",\n            return_value=orgs,\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"organizations\", \"get\", \"-o\", \"json\"])\n    assert result.exit_code == 0, result.output\n    data = json.loads(result.output)\n    assert isinstance(data, list)\n    assert list(data[0]) == [\"org_id\", \"org_name\", \"is_default\", \"active\"]\n    assert {d[\"org_id\"] for d in data} == {\"org-a\", \"org-b\"}\n    default = next(d for d in data if d[\"org_id\"] == \"org-a\")\n    assert default[\"is_default\"] is True\n    assert default[\"active\"] is True\n\n\ndef test_organizations_get_yaml() -> None:\n    runner = CliRunner()\n    orgs = [OrgSummary(org_id=\"org-a\", org_name=\"Acme\", is_default=True)]\n    with (\n        patch(\n            \"llama_agents.cli.commands.organizations.probe_organizations_support\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.commands.organizations._list_organizations\",\n            return_value=orgs,\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"organizations\", \"get\", \"-o\", \"yaml\"])\n    assert result.exit_code == 0, result.output\n    data = yaml.safe_load(result.output)\n    assert isinstance(data, list)\n    assert data[0][\"org_id\"] == \"org-a\"\n\n\ndef test_organizations_get_unsupported_text_warns() -> None:\n    runner = CliRunner()\n    with (\n        patch(\n            \"llama_agents.cli.commands.organizations.probe_organizations_support\",\n            return_value=False,\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"organizations\", \"get\"])\n    assert result.exit_code == 0, result.output\n    assert \"does not support organizations\" in result.output\n\n\ndef test_organizations_get_unsupported_json_emits_empty_list() -> None:\n    \"\"\"Structured outputs should be parseable even on unsupported servers.\"\"\"\n    runner = CliRunner()\n    with (\n        patch(\n            \"llama_agents.cli.commands.organizations.probe_organizations_support\",\n            return_value=False,\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"organizations\", \"get\", \"-o\", \"json\"])\n    assert result.exit_code == 0, result.output\n    assert json.loads(result.output) == []\n\n\ndef test_organizations_get_does_not_offer_wide_output() -> None:\n    result = CliRunner().invoke(app, [\"organizations\", \"get\", \"-o\", \"wide\"])\n    assert result.exit_code != 0\n    assert \"'wide' is not one of 'text', 'json', 'yaml'\" in result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_param_types.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\n\nimport click\nimport llama_agents.cli.config.env_service as es\nimport llama_agents.cli.param_types as pt\nimport pytest\nfrom click.shell_completion import CompletionItem\nfrom llama_agents.cli.param_types import (\n    DeploymentType,\n    EnvironmentType,\n    GitShaType,\n    ProfileType,\n    ProjectType,\n    TemplateType,\n)\n\n\n@pytest.fixture()\ndef ctx() -> click.Context:\n    \"\"\"A minimal Click context for shell_complete calls.\"\"\"\n    cmd = click.Command(\"test\")\n    return click.Context(cmd)\n\n\n@pytest.fixture()\ndef param() -> click.Parameter:\n    return click.Argument([\"test_arg\"])\n\n\ndef test_deployment_type_returns_fetched_items(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.setattr(\n        pt,\n        \"_fetch_deployments\",\n        lambda project_id_override=None: [\n            CompletionItem(\"my-app\"),\n            CompletionItem(\"staging\"),\n        ],\n    )\n\n    dt = DeploymentType()\n    items = dt.shell_complete(ctx, param, \"\")\n    assert len(items) == 2\n    assert items[0].value == \"my-app\"\n\n    # Filter by prefix\n    items = dt.shell_complete(ctx, param, \"my\")\n    assert len(items) == 1\n    assert items[0].value == \"my-app\"\n\n\ndef test_deployment_type_passes_project_override_to_fetch(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    project_overrides: list[str | None] = []\n\n    def _fetch_deployments(\n        project_id_override: str | None = None,\n    ) -> list[CompletionItem]:\n        project_overrides.append(project_id_override)\n        return [CompletionItem(\"project-app\")]\n\n    monkeypatch.setattr(pt, \"_fetch_deployments\", _fetch_deployments)\n    ctx.params[\"project\"] = \"project-123\"\n\n    dt = DeploymentType()\n    items = dt.shell_complete(ctx, param, \"\")\n\n    assert [item.value for item in items] == [\"project-app\"]\n    assert project_overrides == [\"project-123\"]\n\n\ndef test_deployment_type_fetch_failure(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    def _boom(project_id_override: str | None = None) -> list[CompletionItem]:\n        raise RuntimeError(\"API down\")\n\n    monkeypatch.setattr(pt, \"_fetch_deployments\", _boom)\n\n    dt = DeploymentType()\n    items = dt.shell_complete(ctx, param, \"\")\n    assert items == []\n\n\ndef test_profile_type_returns_profiles(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    @dataclass\n    class FakeAuth:\n        name: str\n        api_url: str\n\n    class FakeAuthService:\n        def list_profiles(self) -> list[FakeAuth]:\n            return [\n                FakeAuth(name=\"prod\", api_url=\"https://api.prod.example.com\"),\n                FakeAuth(name=\"dev\", api_url=\"https://api.dev.example.com\"),\n            ]\n\n    class FakeService:\n        def current_auth_service(self) -> FakeAuthService:\n            return FakeAuthService()\n\n    monkeypatch.setattr(es, \"service\", FakeService())\n\n    prof = ProfileType()\n    items = prof.shell_complete(ctx, param, \"\")\n    assert len(items) == 2\n    assert items[0].value == \"prod\"\n    assert items[0].help == \"https://api.prod.example.com\"\n\n    items = prof.shell_complete(ctx, param, \"dev\")\n    assert len(items) == 1\n    assert items[0].value == \"dev\"\n\n\ndef test_project_type_returns_fetched_items(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    monkeypatch.setattr(\n        pt,\n        \"_fetch_projects\",\n        lambda: [\n            CompletionItem(\"proj_abc\", help=\"My Project (3 deployments)\"),\n            CompletionItem(\"proj_def\", help=\"Staging (1 deployment)\"),\n        ],\n    )\n\n    proj = ProjectType()\n    items = proj.shell_complete(ctx, param, \"\")\n    assert len(items) == 2\n    assert items[0].value == \"proj_abc\"\n\n\ndef test_environment_type_returns_environments(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    @dataclass\n    class FakeEnv:\n        api_url: str\n        requires_auth: bool = True\n\n    class FakeService:\n        def list_environments(self) -> list[FakeEnv]:\n            return [\n                FakeEnv(api_url=\"https://api.prod.example.com\"),\n                FakeEnv(api_url=\"https://api.dev.example.com\"),\n            ]\n\n        def get_current_environment(self) -> FakeEnv:\n            return FakeEnv(api_url=\"https://api.prod.example.com\")\n\n    monkeypatch.setattr(es, \"service\", FakeService())\n\n    et = EnvironmentType()\n    items = et.shell_complete(ctx, param, \"\")\n    assert len(items) == 2\n    # Current env should have \"(current)\" help\n    assert items[0].help == \"(current)\"\n    assert items[1].help == \"\"\n\n\ndef test_template_type_returns_all_templates(\n    ctx: click.Context, param: click.Parameter\n) -> None:\n    tt = TemplateType()\n    items = tt.shell_complete(ctx, param, \"\")\n    assert len(items) == 12  # 6 UI + 6 headless\n\n    # Filter\n    items = tt.shell_complete(ctx, param, \"basic\")\n    assert len(items) == 2  # basic-ui and basic\n\n\ndef test_template_type_case_insensitive(\n    ctx: click.Context, param: click.Parameter\n) -> None:\n    tt = TemplateType()\n    items = tt.shell_complete(ctx, param, \"RAG\")\n    assert len(items) == 1\n    assert items[0].value == \"rag\"\n\n\ndef test_git_sha_type_no_deployment_id(\n    ctx: click.Context, param: click.Parameter\n) -> None:\n    ctx.params = {}\n    gt = GitShaType()\n    items = gt.shell_complete(ctx, param, \"\")\n    assert items == []\n\n\ndef test_git_sha_type_with_deployment_id(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    ctx.params = {\"deployment_id\": \"my-deploy\"}\n    monkeypatch.setattr(\n        pt,\n        \"_fetch_deployment_history\",\n        lambda dep_id, project_id_override=None: [\n            CompletionItem(\"abc1234\", help=\"2026-01-01T00:00:00\"),\n            CompletionItem(\"def5678\", help=\"2026-01-02T00:00:00\"),\n        ],\n    )\n\n    gt = GitShaType()\n    items = gt.shell_complete(ctx, param, \"\")\n    assert len(items) == 2\n\n    items = gt.shell_complete(ctx, param, \"abc\")\n    assert len(items) == 1\n    assert items[0].value == \"abc1234\"\n\n\ndef test_git_sha_type_passes_project_override_to_fetch(\n    ctx: click.Context, param: click.Parameter, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    project_overrides: list[str | None] = []\n\n    def _fetch_deployment_history(\n        deployment_id: str, project_id_override: str | None = None\n    ) -> list[CompletionItem]:\n        project_overrides.append(project_id_override)\n        return [CompletionItem(\"abc1234\")]\n\n    monkeypatch.setattr(pt, \"_fetch_deployment_history\", _fetch_deployment_history)\n    ctx.params = {\"deployment_id\": \"my-deploy\", \"project\": \"project-123\"}\n\n    gt = GitShaType()\n    items = gt.shell_complete(ctx, param, \"\")\n\n    assert [item.value for item in items] == [\"abc1234\"]\n    assert project_overrides == [\"project-123\"]\n"
  },
  {
    "path": "packages/llamactl/tests/test_pkg.py",
    "content": "import os\nfrom pathlib import Path\n\nimport click\nimport pytest\nfrom llama_agents.cli.commands.pkg import (\n    _check_deployment_config,\n    _create_file_for_container,\n)\nfrom llama_agents.cli.pkg import (\n    DEFAULT_DOCKER_IGNORE,\n    build_dockerfile_content,\n    infer_python_version,\n)\n\n\n@pytest.fixture()\ndef pyproject_toml() -> str:\n    return \"\"\"\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"test\"\nversion = \"0.1.0\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nrequires-python = \">=3.12,<4\"\ndependencies = [\n    \"llama-index-workflows>=2.7.1\",\n]\n\n[tool.hatch.build.targets.wheel]\nonly-include = [\"src/test_agent\"]\n\n[tool.hatch.build.targets.wheel.sources]\n\"src\" = \"\"\n\n[tool.llamadeploy.workflows]\ntest = \"test_agent.workflow.main:workflow\"\n\n[tool.llamadeploy]\nname = \"test-agent\"\nenv_files = [\".env\"]\nllama_cloud = true\n\"\"\"\n\n\n@pytest.fixture()\ndef pyproject_toml_with_ui(pyproject_toml: str) -> str:\n    return pyproject_toml + '\\n[tool.llamadeploy.ui]\\ndirectory = \"./ui\"\\n'\n\n\n@pytest.fixture()\ndef dockerfile() -> str:\n    return \"\"\"\nFROM python:3.14-slim-trixie\n\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/\n\nWORKDIR /app\n\nCOPY . /app/\n\nENV PATH=/root/.local/bin:$PATH\n\nRUN uv sync --locked\nRUN uv tool install llamactl\n\nEXPOSE 4502\n\nENTRYPOINT [ \"uv\", \"run\", \"llamactl\", \"serve\", \"--host\", \"0.0.0.0\", \"--port\", \"4502\" ]\n\"\"\"\n\n\ndef test_infer_python_version(tmp_path: Path, pyproject_toml: str) -> None:\n    with open(tmp_path / \".python-version\", \"w\") as f:\n        f.write(\"3.13\\n\")\n    with open(tmp_path / \"pyproject.toml\", \"w\") as f:\n        f.write(pyproject_toml)\n    assert infer_python_version(tmp_path) == \"3.13\"\n    os.remove(tmp_path / \".python-version\")\n    assert infer_python_version(tmp_path) == \"3.12\"\n\n\ndef test_build_dockerfile_content(dockerfile: str) -> None:\n    assert build_dockerfile_content(\"3.14\", 4502) == dockerfile\n\n\ndef test__check_deployment_config(\n    pyproject_toml: str, pyproject_toml_with_ui: str, tmp_path: Path\n) -> None:\n    cwd = Path.cwd()\n    os.chdir(tmp_path)\n    try:\n        (tmp_path / \".env\").touch()\n        with open(tmp_path / \"pyproject.toml\", \"w\") as f:\n            f.write(pyproject_toml)\n        conf_dir = _check_deployment_config(tmp_path)\n        assert str(conf_dir) == str(tmp_path)\n        with open(tmp_path / \"pyproject.toml\", \"w\") as f:\n            f.write(pyproject_toml_with_ui)\n        with pytest.raises(click.ClickException):\n            _check_deployment_config(tmp_path)\n    except Exception as e:\n        raise e\n    finally:\n        os.chdir(cwd)\n\n\ndef test__create_file_for_container(\n    dockerfile: str, tmp_path: Path, pyproject_toml: str\n) -> None:\n    cwd = Path.cwd()\n    os.chdir(tmp_path)\n    (tmp_path / \".env\").touch()\n    with open(tmp_path / \"pyproject.toml\", \"w\") as f:\n        f.write(pyproject_toml)\n    _create_file_for_container(\n        output_file=\"Dockerfile\",\n        deployment_file=tmp_path,\n        python_version=\"3.14\",\n        port=4502,\n    )\n    try:\n        assert (tmp_path / \"Dockerfile\").exists()\n        assert (tmp_path / \".dockerignore\").exists()\n        assert (tmp_path / \"Dockerfile\").read_text(encoding=\"utf-8\") == dockerfile\n        content = (tmp_path / \".dockerignore\").read_text()\n        assert DEFAULT_DOCKER_IGNORE in content\n        with pytest.raises(click.ClickException):\n            _create_file_for_container(\n                output_file=\"Dockerfile\",\n                deployment_file=tmp_path,\n            )\n        _create_file_for_container(\n            output_file=\"Containerfile.llamactl\",\n            deployment_file=tmp_path,\n            exclude=(\".env.local\",),\n            overwrite=True,\n        )\n        expected_default_content = build_dockerfile_content(\"3.12\", 4501)\n        assert (tmp_path / \"Containerfile.llamactl\").read_text(\n            encoding=\"utf-8\"\n        ) == expected_default_content\n        content = (tmp_path / \".dockerignore\").read_text()\n        assert DEFAULT_DOCKER_IGNORE in content\n        assert \".env.local\" in content\n    except Exception as e:\n        raise e\n    finally:\n        os.chdir(cwd)\n"
  },
  {
    "path": "packages/llamactl/tests/test_projects_cli.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nimport json\nfrom unittest.mock import MagicMock, patch\n\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\n\n\ndef test_projects_get_lists_projects_json() -> None:\n    runner = CliRunner()\n    projects = [\n        MagicMock(project_id=\"proj-a\", project_name=\"Project A\", deployment_count=2),\n        MagicMock(project_id=\"proj-b\", project_name=\"Project B\", deployment_count=0),\n    ]\n    with (\n        patch(\n            \"llama_agents.cli.commands.projects.validate_authenticated_profile\",\n            return_value=MagicMock(project_id=\"proj-a\"),\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._discover_organization\",\n            return_value=None,\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._list_projects\", return_value=projects\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"projects\", \"get\", \"-o\", \"json\"])\n\n    assert result.exit_code == 0, result.output\n    data = json.loads(result.output)\n    assert [item[\"project_id\"] for item in data] == [\"proj-a\", \"proj-b\"]\n    assert list(data[0]) == [\n        \"project_id\",\n        \"project_name\",\n        \"deployment_count\",\n        \"active\",\n    ]\n    assert data[0][\"active\"] is True\n\n\ndef test_projects_get_text_puts_name_first_and_uses_yes_no() -> None:\n    runner = CliRunner()\n    projects = [\n        MagicMock(project_id=\"proj-a\", project_name=\"Project A\", deployment_count=2),\n        MagicMock(project_id=\"proj-b\", project_name=\"Project B\", deployment_count=0),\n    ]\n    with (\n        patch(\n            \"llama_agents.cli.commands.projects.validate_authenticated_profile\",\n            return_value=MagicMock(project_id=\"proj-a\"),\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._discover_organization\",\n            return_value=None,\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._list_projects\", return_value=projects\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"projects\", \"get\"])\n\n    assert result.exit_code == 0, result.output\n    assert result.output.splitlines()[0].startswith(\"NAME\")\n    assert \"Project A\" in result.output\n    assert \"yes\" in result.output\n    assert \"no\" in result.output\n\n\ndef test_projects_get_single_project() -> None:\n    runner = CliRunner()\n    projects = [\n        MagicMock(project_id=\"proj-a\", project_name=\"Project A\", deployment_count=2),\n        MagicMock(project_id=\"proj-b\", project_name=\"Project B\", deployment_count=0),\n    ]\n    with (\n        patch(\n            \"llama_agents.cli.commands.projects.validate_authenticated_profile\",\n            return_value=MagicMock(project_id=\"proj-a\"),\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._discover_organization\",\n            return_value=None,\n        ),\n        patch(\n            \"llama_agents.cli.commands.projects._list_projects\", return_value=projects\n        ),\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n    ):\n        mock_service.current_auth_service.return_value = MagicMock()\n        result = runner.invoke(app, [\"projects\", \"get\", \"proj-b\", \"-o\", \"json\"])\n\n    assert result.exit_code == 0, result.output\n    data = json.loads(result.output)\n    assert data[\"project_id\"] == \"proj-b\"\n    assert data[\"active\"] is False\n\n\ndef test_auth_project_no_longer_exists() -> None:\n    result = CliRunner().invoke(app, [\"auth\", \"project\"])\n    assert result.exit_code != 0\n    assert \"Use `llamactl projects` instead.\" in result.output\n\n\ndef test_projects_get_does_not_offer_wide_output() -> None:\n    result = CliRunner().invoke(app, [\"projects\", \"get\", \"-o\", \"wide\"])\n    assert result.exit_code != 0\n    assert \"'wide' is not one of 'text', 'json', 'yaml'\" in result.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_redact.py",
    "content": "from llama_agents.cli.utils.redact import redact_api_key\n\n\ndef test_redact_api_key_long() -> None:\n    value = \"sk-1234567890abcdef\"\n    masked = redact_api_key(value)\n    assert masked.startswith(\"sk-123\")\n    assert masked.endswith(\"cdef\")\n    assert \"****\" in masked\n\n\ndef test_redact_api_key_short() -> None:\n    value = \"abc1234\"\n    masked = redact_api_key(value)\n    assert masked.startswith(\"abc123\")\n    assert masked.endswith(\"34\")\n    assert \"****\" in masked\n\n\ndef test_redact_api_key_none_and_empty() -> None:\n    assert redact_api_key(None) == \"-\"\n    assert redact_api_key(\"\") == \"-\"\n"
  },
  {
    "path": "packages/llamactl/tests/test_render.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for the plain-whitespace table renderer.\"\"\"\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nimport click\nfrom click.testing import CliRunner\nfrom llama_agents.cli.render import (\n    format_iso_z,\n    gh_short,\n    render_table,\n    short_sha,\n    star_marker,\n)\n\n\ndef _capture(rows: list[dict[str, str]], columns: list[tuple[str, str]]) -> str:\n    @click.command()\n    def _cmd() -> None:\n        render_table(rows, columns)\n\n    result = CliRunner().invoke(_cmd, [])\n    assert result.exit_code == 0, result.output\n    return result.output\n\n\ndef test_render_table_emits_headers_and_rows() -> None:\n    output = _capture(\n        rows=[\n            {\"name\": \"alpha\", \"phase\": \"Running\"},\n            {\"name\": \"beta\", \"phase\": \"Suspended\"},\n        ],\n        columns=[(\"NAME\", \"name\"), (\"PHASE\", \"phase\")],\n    )\n    lines = output.splitlines()\n    assert lines[0].startswith(\"NAME\")\n    assert \"PHASE\" in lines[0]\n    assert \"alpha\" in lines[1]\n    assert \"Running\" in lines[1]\n    assert \"beta\" in lines[2]\n    assert \"Suspended\" in lines[2]\n\n\ndef test_render_table_no_ansi_escapes_or_truncation() -> None:\n    long_value = \"https://github.com/run-llama/template-workflow-classify-extract-sec\"\n    output = _capture(\n        rows=[{\"name\": \"a\", \"repo\": long_value}],\n        columns=[(\"NAME\", \"name\"), (\"REPO\", \"repo\")],\n    )\n    assert \"\\x1b[\" not in output\n    assert \"…\" not in output\n    assert long_value in output\n\n\ndef test_render_table_column_width_uses_widest_cell() -> None:\n    output = _capture(\n        rows=[\n            {\"name\": \"short\", \"phase\": \"Pending\"},\n            {\"name\": \"much-longer-name\", \"phase\": \"RollingOut\"},\n        ],\n        columns=[(\"NAME\", \"name\"), (\"PHASE\", \"phase\")],\n    )\n    lines = output.splitlines()\n    # Header column 2 starts at the same column as data column 2.\n    header_phase_start = lines[0].index(\"PHASE\")\n    row1_phase_start = lines[1].index(\"Pending\")\n    row2_phase_start = lines[2].index(\"RollingOut\")\n    assert header_phase_start == row1_phase_start == row2_phase_start\n\n\ndef test_render_table_handles_empty_rows() -> None:\n    output = _capture(rows=[], columns=[(\"NAME\", \"name\"), (\"PHASE\", \"phase\")])\n    # Header row only; trailing whitespace stripped.\n    assert output.strip().split(\"\\n\") == [\"NAME  PHASE\"]\n\n\ndef test_gh_short_translates_github_urls() -> None:\n    assert (\n        gh_short(\"https://github.com/run-llama/template-workflow\")\n        == \"gh:run-llama/template-workflow\"\n    )\n\n\ndef test_gh_short_passes_through_non_github() -> None:\n    assert gh_short(\"https://gitlab.com/x/y\") == \"https://gitlab.com/x/y\"\n    assert gh_short(\"internal://repo\") == \"internal://repo\"\n\n\ndef test_format_iso_z_tz_aware_utc() -> None:\n    dt = datetime(2026, 4, 25, 15, 1, 15, tzinfo=timezone.utc)\n    assert format_iso_z(dt) == \"2026-04-25T15:01:15Z\"\n\n\ndef test_format_iso_z_tz_aware_non_utc_is_converted() -> None:\n    # Pacific (UTC-8 standard time): 07:01:15-08:00 == 15:01:15Z\n    from datetime import timedelta\n    from datetime import timezone as _tz\n\n    pst = _tz(timedelta(hours=-8))\n    dt = datetime(2026, 4, 25, 7, 1, 15, tzinfo=pst)\n    assert format_iso_z(dt) == \"2026-04-25T15:01:15Z\"\n\n\ndef test_format_iso_z_naive_is_treated_as_utc() -> None:\n    # Documented behavior: naive datetimes are assumed to already be UTC.\n    dt = datetime(2026, 4, 25, 15, 1, 15)\n    assert format_iso_z(dt) == \"2026-04-25T15:01:15Z\"\n\n\ndef test_short_sha_truncates_to_seven() -> None:\n    assert short_sha(\"a\" * 40) == \"aaaaaaa\"\n\n\ndef test_short_sha_passthrough_on_short_input() -> None:\n    assert short_sha(\"abc\") == \"abc\"\n    assert short_sha(\"\") == \"\"\n\n\ndef test_star_marker_active_and_inactive() -> None:\n    assert star_marker(True) == \"*\"\n    assert star_marker(False) == \"\"\n"
  },
  {
    "path": "packages/llamactl/tests/test_retry.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for run_with_network_retries classifier behavior.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import AsyncMock, patch\n\nimport httpx\nimport pytest\nfrom llama_agents.cli.utils.retry import run_with_network_retries\n\n\n@pytest.fixture(autouse=True)\ndef _no_sleep():\n    \"\"\"Skip real back-off sleeps so these tests stay fast.\"\"\"\n    with patch(\"tenacity.nap.time.sleep\"), patch(\"asyncio.sleep\", AsyncMock()):\n        yield\n\n\nasync def _raising(exc: BaseException, counter: list[int]):\n    counter.append(1)\n    raise exc\n\n\n@pytest.mark.asyncio\nasync def test_idempotent_true_retries_read_timeout() -> None:\n    counter: list[int] = []\n    with pytest.raises(httpx.ReadTimeout):\n        await run_with_network_retries(\n            lambda: _raising(httpx.ReadTimeout(\"slow\"), counter),\n            max_attempts=3,\n        )\n    assert len(counter) == 3\n\n\n@pytest.mark.asyncio\nasync def test_idempotent_true_retries_connect_error() -> None:\n    counter: list[int] = []\n    with pytest.raises(httpx.ConnectError):\n        await run_with_network_retries(\n            lambda: _raising(httpx.ConnectError(\"refused\"), counter),\n            max_attempts=3,\n        )\n    assert len(counter) == 3\n\n\n@pytest.mark.asyncio\nasync def test_idempotent_false_does_not_retry_read_timeout() -> None:\n    \"\"\"Read-phase error — request may have reached the server. No retry.\"\"\"\n    counter: list[int] = []\n    with pytest.raises(httpx.ReadTimeout):\n        await run_with_network_retries(\n            lambda: _raising(httpx.ReadTimeout(\"slow\"), counter),\n            max_attempts=3,\n            idempotent=False,\n        )\n    assert len(counter) == 1\n\n\n@pytest.mark.asyncio\nasync def test_idempotent_false_does_not_retry_remote_protocol_error() -> None:\n    \"\"\"RemoteProtocolError may have happened after the server accepted the\n    request — no retry for non-idempotent callers.\"\"\"\n    counter: list[int] = []\n    with pytest.raises(httpx.RemoteProtocolError):\n        await run_with_network_retries(\n            lambda: _raising(httpx.RemoteProtocolError(\"eof\"), counter),\n            max_attempts=3,\n            idempotent=False,\n        )\n    assert len(counter) == 1\n\n\n@pytest.mark.asyncio\nasync def test_idempotent_false_retries_connect_error() -> None:\n    counter: list[int] = []\n    with pytest.raises(httpx.ConnectError):\n        await run_with_network_retries(\n            lambda: _raising(httpx.ConnectError(\"refused\"), counter),\n            max_attempts=3,\n            idempotent=False,\n        )\n    assert len(counter) == 3\n\n\n@pytest.mark.asyncio\nasync def test_idempotent_false_retries_connect_timeout() -> None:\n    counter: list[int] = []\n    with pytest.raises(httpx.ConnectTimeout):\n        await run_with_network_retries(\n            lambda: _raising(httpx.ConnectTimeout(\"no syn-ack\"), counter),\n            max_attempts=3,\n            idempotent=False,\n        )\n    assert len(counter) == 3\n\n\n@pytest.mark.asyncio\nasync def test_idempotent_false_retries_pool_timeout() -> None:\n    counter: list[int] = []\n    with pytest.raises(httpx.PoolTimeout):\n        await run_with_network_retries(\n            lambda: _raising(httpx.PoolTimeout(\"saturated\"), counter),\n            max_attempts=3,\n            idempotent=False,\n        )\n    assert len(counter) == 3\n\n\n@pytest.mark.asyncio\nasync def test_never_retries_http_status_error() -> None:\n    \"\"\"HTTPStatusError is a server response, not a transient failure.\"\"\"\n    counter: list[int] = []\n    resp = httpx.Response(500, request=httpx.Request(\"POST\", \"https://x\"))\n    err = httpx.HTTPStatusError(\"500\", request=resp.request, response=resp)\n    with pytest.raises(httpx.HTTPStatusError):\n        await run_with_network_retries(\n            lambda: _raising(err, counter),\n            max_attempts=3,\n        )\n    assert len(counter) == 1\n\n\n@pytest.mark.asyncio\nasync def test_succeeds_on_second_attempt() -> None:\n    counter: list[int] = []\n\n    async def op() -> str:\n        counter.append(1)\n        if len(counter) < 2:\n            raise httpx.ConnectError(\"first try\")\n        return \"ok\"\n\n    result = await run_with_network_retries(op, max_attempts=3, idempotent=False)\n    assert result == \"ok\"\n    assert len(counter) == 2\n"
  },
  {
    "path": "packages/llamactl/tests/test_serve_llama_cloud.py",
    "content": "import os\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\nfrom llama_agents.cli.config.schema import Auth\n\n\ndef _write_yaml(tmpdir: Path, llama_cloud: bool) -> Path:\n    cfg = tmpdir / \"llama_deploy.yaml\"\n    cfg.write_text(\n        (\n            \"name: test\\n\"\n            f\"llama_cloud: {'true' if llama_cloud else 'false'}\\n\"\n            \"workflows:\\n  default: tests.fake_module:fake_workflow\\n\"\n        ),\n        encoding=\"utf-8\",\n    )\n    # minimal python project structure for appserver prepare\n    (tmpdir / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n    (tmpdir / \"tests\").mkdir(exist_ok=True)\n    (tmpdir / \"tests\" / \"__init__.py\").write_text(\"\", encoding=\"utf-8\")\n    (tmpdir / \"tests\" / \"fake_module.py\").write_text(\n        \"from workflows import Workflow\\n\\nfake_workflow = Workflow()\\n\",\n        encoding=\"utf-8\",\n    )\n    return cfg\n\n\n@pytest.fixture(autouse=True)\ndef _isolate_env(monkeypatch: pytest.MonkeyPatch) -> None:\n    # ensure tests don't leak credentials\n    monkeypatch.delenv(\"LLAMA_CLOUD_API_KEY\", raising=False)\n    monkeypatch.delenv(\"LLAMA_CLOUD_BASE_URL\", raising=False)\n    monkeypatch.delenv(\"LLAMA_CLOUD_USE_PROFILE\", raising=False)\n    monkeypatch.delenv(\"LLAMA_AGENTS_PROJECT_ID\", raising=False)\n    monkeypatch.delenv(\"LLAMA_DEPLOY_PROJECT_ID\", raising=False)\n\n\ndef test_injects_api_key_from_profile(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    cfg = _write_yaml(tmp_path, llama_cloud=True)\n\n    authed = Auth(\n        id=\"123\",\n        name=\"test\",\n        api_url=\"https://api.cloud.llamaindex.ai\",\n        project_id=\"proj-1\",\n        api_key=\"ABC123\",\n    )\n\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n    ):\n        mock_service.current_auth_service().get_current_profile.return_value = authed\n\n        runner = CliRunner()\n        res = runner.invoke(\n            app, [\"serve\", str(cfg), \"--no-install\", \"--no-reload\", \"--no-open-browser\"]\n        )\n        assert res.exit_code == 0, res.output\n        assert os.environ.get(\"LLAMA_CLOUD_API_KEY\") == \"ABC123\"\n        assert os.environ.get(\"LLAMA_AGENTS_PROJECT_ID\") == \"proj-1\"\n        assert os.environ.get(\"LLAMA_DEPLOY_PROJECT_ID\") == \"proj-1\"\n\n\ndef test_prompts_login_when_interactive(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    cfg = _write_yaml(tmp_path, llama_cloud=True)\n\n    authed = Auth(\n        id=\"123\",\n        name=\"test\",\n        api_url=\"https://api.cloud.llamaindex.ai\",\n        project_id=\"proj-1\",\n        api_key=\"ZZZ999\",\n    )\n\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.serve.click.confirm\") as mock_confirm,\n        patch(\n            \"llama_agents.cli.commands.serve.validate_authenticated_profile\"\n        ) as mock_validate,\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n        patch(\n            \"llama_agents.cli.commands.serve.is_interactive_session\", return_value=True\n        ),\n    ):\n        mock_service.current_auth_service().get_current_profile.return_value = None\n        mock_confirm.return_value = True\n        mock_validate.return_value = authed\n\n        runner = CliRunner()\n        res = runner.invoke(\n            app,\n            [\n                \"serve\",\n                str(cfg),\n                \"--no-install\",\n                \"--no-reload\",\n                \"--no-open-browser\",\n            ],\n        )\n        assert res.exit_code == 0, res.output\n        assert os.environ.get(\"LLAMA_CLOUD_API_KEY\") == \"ZZZ999\"\n        assert os.environ.get(\"LLAMA_AGENTS_PROJECT_ID\") == \"proj-1\"\n        assert os.environ.get(\"LLAMA_DEPLOY_PROJECT_ID\") == \"proj-1\"\n\n\ndef test_interactive_serve_uses_env_key_without_profile_choice(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    cfg = _write_yaml(tmp_path, llama_cloud=True)\n    monkeypatch.setenv(\"LLAMA_CLOUD_API_KEY\", \"ENV123\")\n    monkeypatch.setenv(\"LLAMA_AGENTS_PROJECT_ID\", \"env-project\")\n\n    profile = Auth(\n        id=\"123\",\n        name=\"test\",\n        api_url=\"https://api.cloud.llamaindex.ai\",\n        project_id=\"profile-project\",\n        api_key=\"PROFILE123\",\n    )\n\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.cli.commands.serve.select_or_exit\") as mock_select,\n        patch(\n            \"llama_agents.cli.commands.serve.validate_authenticated_profile\"\n        ) as mock_validate,\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n        patch(\n            \"llama_agents.cli.commands.serve.is_interactive_session\", return_value=True\n        ),\n    ):\n        mock_service.current_auth_service().get_current_profile.return_value = profile\n\n        runner = CliRunner()\n        res = runner.invoke(\n            app,\n            [\n                \"serve\",\n                str(cfg),\n                \"--no-install\",\n                \"--no-reload\",\n                \"--no-open-browser\",\n            ],\n        )\n\n    assert res.exit_code == 0, res.output\n    assert os.environ.get(\"LLAMA_CLOUD_API_KEY\") == \"ENV123\"\n    assert os.environ.get(\"LLAMA_AGENTS_PROJECT_ID\") == \"env-project\"\n    assert os.environ.get(\"LLAMA_DEPLOY_PROJECT_ID\") == \"env-project\"\n    mock_select.assert_not_called()\n    mock_validate.assert_not_called()\n\n\ndef test_serve_use_profile_env_bypasses_env_key(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    cfg = _write_yaml(tmp_path, llama_cloud=True)\n    monkeypatch.setenv(\"LLAMA_CLOUD_API_KEY\", \"ENV123\")\n    monkeypatch.setenv(\"LLAMA_AGENTS_PROJECT_ID\", \"env-project\")\n    monkeypatch.setenv(\"LLAMA_CLOUD_USE_PROFILE\", \"1\")\n\n    profile = Auth(\n        id=\"123\",\n        name=\"test\",\n        api_url=\"https://api.cloud.llamaindex.ai\",\n        project_id=\"profile-project\",\n        api_key=\"PROFILE123\",\n    )\n\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n    ):\n        mock_service.current_auth_service().get_current_profile.return_value = profile\n\n        runner = CliRunner()\n        res = runner.invoke(\n            app, [\"serve\", str(cfg), \"--no-install\", \"--no-reload\", \"--no-open-browser\"]\n        )\n\n    assert res.exit_code == 0, res.output\n    assert os.environ.get(\"LLAMA_CLOUD_API_KEY\") == \"PROFILE123\"\n    assert os.environ.get(\"LLAMA_AGENTS_PROJECT_ID\") == \"profile-project\"\n    assert os.environ.get(\"LLAMA_DEPLOY_PROJECT_ID\") == \"profile-project\"\n\n\ndef test_injects_project_id_from_env_config(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    cfg = tmp_path / \"llama_deploy.yaml\"\n    cfg.write_text(\n        (\n            \"name: test\\n\"\n            \"llama_cloud: true\\n\"\n            \"env:\\n  LLAMA_AGENTS_PROJECT_ID: proj-from-config\\n\"\n            \"workflows:\\n  default: tests.fake_module:fake_workflow\\n\"\n        ),\n        encoding=\"utf-8\",\n    )\n    # minimal python project structure for appserver prepare\n    (tmp_path / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n    (tmp_path / \"tests\").mkdir(exist_ok=True)\n    (tmp_path / \"tests\" / \"__init__.py\").write_text(\"\", encoding=\"utf-8\")\n    (tmp_path / \"tests\" / \"fake_module.py\").write_text(\n        \"from workflows import Workflow\\n\\nfake_workflow = Workflow()\\n\",\n        encoding=\"utf-8\",\n    )\n\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n        patch(\n            \"llama_agents.cli.commands.serve.is_interactive_session\", return_value=False\n        ),\n    ):\n        # No profile necessary for this path; simulate no logged-in profile\n        mock_service.current_auth_service().get_current_profile.return_value = None\n\n        runner = CliRunner()\n        res = runner.invoke(\n            app,\n            [\n                \"serve\",\n                str(cfg),\n                \"--no-install\",\n                \"--no-reload\",\n                \"--no-open-browser\",\n            ],\n        )\n        assert res.exit_code == 0, res.output\n        assert os.environ.get(\"LLAMA_AGENTS_PROJECT_ID\") == \"proj-from-config\"\n        assert os.environ.get(\"LLAMA_DEPLOY_PROJECT_ID\") == \"proj-from-config\"\n\n\ndef test_warns_non_interactive_without_key(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    cfg = _write_yaml(tmp_path, llama_cloud=True)\n    with (\n        patch(\"llama_agents.cli.config.env_service.service\") as mock_service,\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n        patch(\n            \"llama_agents.cli.commands.serve.is_interactive_session\", return_value=False\n        ),\n    ):\n        mock_service.current_auth_service().get_current_profile.return_value = None\n\n        runner = CliRunner()\n        res = runner.invoke(\n            app,\n            [\n                \"serve\",\n                str(cfg),\n                \"--no-install\",\n                \"--no-reload\",\n                \"--no-open-browser\",\n            ],\n        )\n        assert res.exit_code == 0, res.output\n        # Not set\n        assert os.environ.get(\"LLAMA_CLOUD_API_KEY\") is None\n        # Warning present\n        assert (\n            \"warning: LLAMA_CLOUD_API_KEY is not set and no logged-in profile was found; the app may not work\"\n            in res.output\n        )\n"
  },
  {
    "path": "packages/llamactl/tests/test_serve_summary.py",
    "content": "import os\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\nfrom click.testing import CliRunner\nfrom llama_agents.cli.app import app\n\n\n@pytest.fixture(autouse=True)\ndef _no_color(monkeypatch: pytest.MonkeyPatch) -> None:\n    monkeypatch.delenv(\"FORCE_COLOR\", raising=False)\n    monkeypatch.setenv(\"NO_COLOR\", \"1\")\n    # Reset the global Rich console so it picks up NO_COLOR\n    from rich import get_console\n\n    monkeypatch.setattr(get_console(), \"_color_system\", None)\n    monkeypatch.setattr(get_console(), \"no_color\", True)\n\n\ndef _write_yaml(tmpdir: Path) -> Path:\n    cfg = tmpdir / \"llama_deploy.yaml\"\n    cfg.write_text(\n        (\n            \"name: test\\n\"\n            \"llama_cloud: true\\n\"\n            \"workflows:\\n  default: tests.fake_module:fake_workflow\\n\"\n        ),\n        encoding=\"utf-8\",\n    )\n    # minimal python project structure for appserver prepare\n    (tmpdir / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n    (tmpdir / \"tests\").mkdir(exist_ok=True)\n    (tmpdir / \"tests\" / \"__init__.py\").write_text(\"\", encoding=\"utf-8\")\n    (tmpdir / \"tests\" / \"fake_module.py\").write_text(\n        \"from workflows import Workflow\\n\\nfake_workflow = Workflow()\\n\",\n        encoding=\"utf-8\",\n    )\n    return cfg\n\n\ndef test_connection_summary_uses_redaction(tmp_path: Path) -> None:\n    cfg = _write_yaml(tmp_path)\n    # Set env with spaces to verify cleaning and masking\n    os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"abc 123 456 789 000\"\n    os.environ[\"LLAMA_AGENTS_PROJECT_ID\"] = \"proj-1\"\n    os.environ[\"LLAMA_CLOUD_BASE_URL\"] = \"https://api.example.local\"\n\n    with (\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n        patch(\n            \"llama_agents.cli.commands.serve.is_interactive_session\", return_value=False\n        ),\n    ):\n        runner = CliRunner()\n        res = runner.invoke(\n            app,\n            [\n                \"serve\",\n                str(cfg),\n                \"--no-install\",\n                \"--no-reload\",\n                \"--no-open-browser\",\n            ],\n        )\n        assert res.exit_code == 0, res.output\n        # Expect first 6, mask, and last 4 of cleaned token\n        assert \"abc123****9000\" in res.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_serve_without_git.py",
    "content": "from pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\nfrom click.testing import CliRunner\nfrom dulwich.errors import NotGitRepository\nfrom llama_agents.cli.app import app\n\n\ndef _write_minimal_yaml(tmpdir: Path) -> Path:\n    cfg = tmpdir / \"llama_deploy.yaml\"\n    cfg.write_text(\n        (\n            \"name: test\\n\"\n            \"llama_cloud: false\\n\"\n            \"workflows:\\n  default: tests.fake_module:fake_workflow\\n\"\n        ),\n        encoding=\"utf-8\",\n    )\n    # minimal python project structure for appserver prepare pre-check\n    (tmpdir / \"pyproject.toml\").write_text(\n        \"[project]\\nname='x'\\nversion='0.0.0'\\n\", encoding=\"utf-8\"\n    )\n    (tmpdir / \"tests\").mkdir(exist_ok=True)\n    (tmpdir / \"tests\" / \"__init__.py\").write_text(\"\", encoding=\"utf-8\")\n    (tmpdir / \"tests\" / \"fake_module.py\").write_text(\n        \"from workflows import Workflow\\n\\nfake_workflow = Workflow()\\n\",\n        encoding=\"utf-8\",\n    )\n    return cfg\n\n\ndef test_serve_does_not_crash_without_git(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    # Ensure no leaked env causes branching behavior\n    monkeypatch.delenv(\"LLAMA_CLOUD_API_KEY\", raising=False)\n    monkeypatch.delenv(\"LLAMA_AGENTS_PROJECT_ID\", raising=False)\n    monkeypatch.delenv(\"LLAMA_DEPLOY_PROJECT_ID\", raising=False)\n\n    cfg = _write_minimal_yaml(tmp_path)\n\n    # Simulate \"no git repo discoverable\" — the dulwich-backed helpers\n    # raise NotGitRepository when there is no .git directory along the\n    # current path.\n    with (\n        patch(\n            \"llama_agents.core.git.git_util.Repo.discover\",\n            side_effect=NotGitRepository(\"no git here\"),\n        ),\n        patch(\"llama_agents.appserver.app.prepare_server\"),\n        patch(\"llama_agents.appserver.app.start_server_in_target_venv\"),\n    ):\n        runner = CliRunner()\n        res = runner.invoke(\n            app, [\"serve\", str(cfg), \"--no-install\", \"--no-reload\", \"--no-open-browser\"]\n        )\n        assert res.exit_code == 0, res.output\n"
  },
  {
    "path": "packages/llamactl/tests/test_session_utils.py",
    "content": "from unittest.mock import patch\n\nfrom llama_agents.cli.interactive import is_interactive_session\n\n\ndef test_is_interactive_false_when_not_tty() -> None:\n    with (\n        patch(\n            \"llama_agents.cli.interactive.sys.stdin.isatty\",\n            return_value=False,\n        ),\n        patch(\n            \"llama_agents.cli.interactive.sys.stdout.isatty\",\n            return_value=True,\n        ),\n        patch(\"llama_agents.cli.interactive.os.environ\", {}),\n    ):\n        assert is_interactive_session() is False\n\n\ndef test_is_interactive_false_when_term_dumb() -> None:\n    with (\n        patch(\n            \"llama_agents.cli.interactive.sys.stdin.isatty\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.interactive.sys.stdout.isatty\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.interactive.os.environ\",\n            {\"TERM\": \"dumb\"},\n        ),\n    ):\n        assert is_interactive_session() is False\n\n\ndef test_is_interactive_false_when_ci_set() -> None:\n    with (\n        patch(\n            \"llama_agents.cli.interactive.sys.stdin.isatty\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.interactive.sys.stdout.isatty\",\n            return_value=True,\n        ),\n        patch(\"llama_agents.cli.interactive.os.environ\", {\"CI\": \"true\"}),\n    ):\n        assert is_interactive_session() is False\n\n\ndef test_is_interactive_true_in_tty() -> None:\n    with (\n        patch(\n            \"llama_agents.cli.interactive.sys.stdin.isatty\",\n            return_value=True,\n        ),\n        patch(\n            \"llama_agents.cli.interactive.sys.stdout.isatty\",\n            return_value=True,\n        ),\n        patch(\"llama_agents.cli.interactive.os.environ\", {}),\n    ):\n        assert is_interactive_session() is True\n"
  },
  {
    "path": "packages/llamactl/tests/test_yaml_template.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for ``cli.yaml_template.render``.\"\"\"\n\nfrom __future__ import annotations\n\nimport yaml as pyyaml\nfrom llama_agents.cli.display import (\n    SECRET_MASK,\n    DeploymentDisplay,\n    DeploymentSpec,\n    DeploymentStatus,\n    Doc,\n    TrailingDoc,\n)\nfrom llama_agents.cli.yaml_template import render\n\n\ndef _full_display() -> DeploymentDisplay:\n    return DeploymentDisplay(\n        name=\"my-app\",\n        generate_name=\"My App\",\n        spec=DeploymentSpec(\n            repo_url=\"https://github.com/example/repo\",\n            deployment_file_path=\"llama_deploy.yaml\",\n            git_ref=\"main\",\n            appserver_version=\"0.5.0\",\n            suspended=False,\n            secrets={\"OPENAI_API_KEY\": \"sk-x\"},\n            personal_access_token=None,\n        ),\n        status=DeploymentStatus(phase=\"Running\", project_id=\"proj_default\"),\n    )\n\n\ndef test_render_omits_status_unconditionally() -> None:\n    out = render(_full_display())\n    assert \"status:\" not in out\n    assert \"Running\" not in out\n    assert \"phase\" not in out\n\n\ndef test_render_emits_name_and_spec_in_declaration_order() -> None:\n    out = render(_full_display())\n    name_idx = out.index(\"name:\")\n    spec_idx = out.index(\"spec:\")\n    assert name_idx < spec_idx\n    body = out[spec_idx:]\n    fields_in_order = [\n        \"repo_url:\",\n        \"deployment_file_path:\",\n        \"git_ref:\",\n        \"appserver_version:\",\n        \"suspended:\",\n        \"secrets:\",\n    ]\n    last = -1\n    for f in fields_in_order:\n        idx = body.index(f)\n        assert idx > last, f\"{f} out of declaration order in:\\n{body}\"\n        last = idx\n\n\ndef test_render_attaches_doc_marker_text_above_each_set_field() -> None:\n    out = render(_full_display())\n    assert \"## Stable id for the deployment\" in out\n    assert '## \"\" = push your local working tree (use for new deployments).' in out\n\n\ndef test_render_omits_generate_name_when_not_scaffolded() -> None:\n    \"\"\"``scaffold_generate_name=False`` (default) skips the field entirely —\n    no comment block, no commented-out line.\"\"\"\n    out = render(_full_display())\n    assert \"generate_name\" not in out\n    assert \"name takes precedence\" not in out\n\n\ndef test_render_emits_generate_name_commented_when_scaffolded() -> None:\n    \"\"\"``scaffold_generate_name=True`` emits the two-line identity explainer\n    once, then commented identity-tier examples.\"\"\"\n    out = render(_full_display(), scaffold_generate_name=True)\n    lines = out.splitlines()\n    assert lines[0].startswith(\"## \")\n    assert lines[1].startswith(\"## \")\n    assert not lines[2].startswith(\"## \")\n    assert \"## Stable id for the deployment\" not in out\n    assert \"name: my-app\" in out\n    assert \"# generate_name: My App\" in out\n    # Commented-out (not authoritative).\n    assert \"\\ngenerate_name:\" not in out\n\n\ndef test_render_uses_name_example_for_unset_top_level_keys() -> None:\n    \"\"\"``name=None`` and (with scaffold) ``generate_name=None`` both fall back\n    to the ``name_example`` argument.\"\"\"\n    out = render(\n        DeploymentDisplay(name=None, spec=DeploymentSpec()),\n        name_example=\"cwd-name\",\n        scaffold_generate_name=True,\n    )\n    assert \"# name: cwd-name\" in out\n    assert \"# generate_name: cwd-name\" in out\n\n\ndef test_render_partial_spec_emits_unset_fields_as_commented_examples() -> None:\n    \"\"\"Unset (not required) spec fields render as commented-out one-liners\n    with a doc above, in declaration order inside the spec block.\"\"\"\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(),\n    )\n    out = render(display)\n    assert \"  # repo_url:\" in out\n    assert \"  # git_ref: main\" in out\n    assert \"  # secrets:\" in out\n    assert \"    # MY_SECRET: ${MY_SECRET}\" in out\n    git_ref_line = next(\n        line for line in out.splitlines() if line.startswith(\"  # git_ref:\")\n    )\n    assert git_ref_line == \"  # git_ref: main\"\n\n\ndef test_render_required_unset_puts_marker_on_own_line() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(),\n    )\n    out = render(display, required=(\"repo_url\",))\n    assert \"  repo_url: ~\" in out\n    repo_idx = out.index(\"  repo_url: ~\")\n    before = out[:repo_idx]\n    # REQUIRED is on its own line, not concatenated with the first doc line.\n    assert \"  ## REQUIRED.\\n\" in before\n    assert '  ## \"\" = push your local working tree (use for new deployments).' in before\n\n\ndef test_render_unset_top_level_name_is_commented() -> None:\n    \"\"\"``DeploymentDisplay.name=None`` renders the top-level key commented-out\n    (the server slugifies an id when ``name`` is unset on apply).\"\"\"\n    display = DeploymentDisplay(\n        name=None,\n        spec=DeploymentSpec(),\n    )\n    out = render(display)\n    assert \"\\n# name: my-app\" in out or out.startswith(\"# name: my-app\")\n    # No bare ``name:`` line at the top level.\n    assert \"\\nname:\" not in out\n    assert not out.startswith(\"name:\")\n\n\ndef test_render_alternatives_emit_commented_line_under_set_field() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(repo_url=\"\"),\n    )\n    out = render(\n        display,\n        field_alternatives={\n            \"repo_url\": (\n                \"https://github.com/owner/repo\",\n                \"auto-detected from your git remotes\",\n            )\n        },\n    )\n    repo_idx = out.index('  repo_url: \"\"')\n    after = out[repo_idx:]\n    next_line = after.splitlines()[1]\n    assert next_line.startswith(\"  # repo_url: https://github.com/owner/repo\")\n    assert next_line.endswith(\"## auto-detected from your git remotes\")\n    parsed = pyyaml.safe_load(out)\n    assert parsed[\"spec\"][\"repo_url\"] == \"\"\n\n\ndef test_render_field_alternative_note_uses_template_marker() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(repo_url=\"\"),\n    )\n    out = render(\n        display,\n        field_alternatives={\n            \"repo_url\": (\n                \"https://github.com/owner/repo\",\n                \"auto-detected from your git remotes\",\n            )\n        },\n    )\n    alt_line = next(\n        line for line in out.splitlines() if line.startswith(\"  # repo_url:\")\n    )\n    assert \"  ## auto-detected from your git remotes\" in alt_line\n    assert \"  # auto-detected from your git remotes\" not in alt_line\n\n\ndef test_render_alternatives_ignored_for_unset_field() -> None:\n    \"\"\"An alternative on a field that's not set is silently ignored — the\n    alternative is conceptually 'a different value than the one you have',\n    not 'an example'.\"\"\"\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(),\n    )\n    out = render(\n        display,\n        field_alternatives={\n            \"repo_url\": (\"https://github.com/owner/repo\", \"detected\"),\n        },\n    )\n    assert \"# detected\" not in out\n\n\ndef test_render_doc_and_trailing_doc_emit_separately() -> None:\n    \"\"\"A ``Doc`` block can render above one field while a ``TrailingDoc``\n    marker renders on another field's scalar line.\"\"\"\n    out = render(_full_display())\n    head = out.split(\"repo_url:\", 1)[0]\n    assert '  ## \"\" = push your local working tree (use for new deployments).' in head\n    deploy_path_line = next(\n        line for line in out.splitlines() if line.startswith(\"  deployment_file_path:\")\n    )\n    assert deploy_path_line.endswith(\"## pyproject.toml or llama_deploy.yaml\")\n\n\ndef test_render_head_lines_emit_at_top_with_prefix() -> None:\n    out = render(\n        _full_display(),\n        head=(\"Edit, then run: llamactl deployments apply -f <file>\",),\n    )\n    first = out.splitlines()[0]\n    assert first.startswith(\"## \")\n    assert \"Edit, then run\" in first\n\n\ndef test_render_head_blank_line_emits_bare_marker() -> None:\n    out = render(\n        _full_display(),\n        head=(\"first\", \"\", \"third\"),\n    )\n    lines = out.splitlines()\n    assert lines[0] == \"## first\"\n    assert lines[1] == \"##\"\n    assert lines[2] == \"## third\"\n\n\ndef test_render_secret_comments_attach_inside_secrets_block() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(\n            secrets={\"API_KEY\": \"${API_KEY}\", \"OTHER\": \"x\"},\n        ),\n    )\n    out = render(display, secret_comments={\"API_KEY\": \"from your .env\"})\n    secrets_idx = out.index(\"secrets:\")\n    block = out[secrets_idx:]\n    api_line = next(line for line in block.splitlines() if \"API_KEY:\" in line)\n    assert api_line.startswith(\"    API_KEY: ${API_KEY}\")\n    assert api_line.endswith(\"## from your .env\")\n    assert \"    ## from your .env\" not in block\n\n\ndef test_render_trailing_doc_is_independent_from_doc() -> None:\n    repo_markers = DeploymentSpec.model_fields[\"repo_url\"].metadata\n    path_markers = DeploymentSpec.model_fields[\"deployment_file_path\"].metadata\n    assert any(isinstance(marker, Doc) for marker in repo_markers)\n    assert not any(isinstance(marker, TrailingDoc) for marker in repo_markers)\n    assert not any(isinstance(marker, Doc) for marker in path_markers)\n    assert any(isinstance(marker, TrailingDoc) for marker in path_markers)\n\n    out = render(\n        DeploymentDisplay(\n            name=\"my-app\",\n            spec=DeploymentSpec(deployment_file_path=\".\"),\n        )\n    )\n    deploy_path_line = next(\n        line\n        for line in out.splitlines()\n        if line.startswith('  deployment_file_path: \".\"')\n    )\n    assert deploy_path_line.endswith(\"## pyproject.toml or llama_deploy.yaml\")\n\n\ndef test_render_blank_lines_break_head_identity_and_spec_groups() -> None:\n    out = render(\n        _full_display(),\n        head=(\"Edit, then run: llamactl deployments apply -f <file>\",),\n    )\n    lines = out.splitlines()\n    assert lines[0] == \"## Edit, then run: llamactl deployments apply -f <file>\"\n    assert lines[1] == \"\"\n\n    name_idx = lines.index(\"name: my-app\")\n    spec_idx = lines.index(\"spec:\")\n    assert lines[name_idx + 1] == \"\"\n    assert lines[spec_idx - 1] == \"\"\n\n    repo_idx = next(i for i, line in enumerate(lines) if line.startswith(\"  repo_url:\"))\n    deployment_file_group_idx = next(\n        i\n        for i, line in enumerate(lines)\n        if \"deployment_file_path:\" in line\n        or \"pyproject.toml or llama_deploy.yaml\" in line\n    )\n    assert lines[deployment_file_group_idx - 1] == \"\"\n    assert deployment_file_group_idx > repo_idx\n\n\ndef test_render_empty_string_repo_url_is_double_quoted() -> None:\n    \"\"\"Empty repo_url is meaningful (push-mode signal). Render explicit ``\"\"``.\"\"\"\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(repo_url=\"\"),\n    )\n    out = render(display)\n    assert 'repo_url: \"\"' in out\n\n\ndef test_render_deployment_file_path_dot_is_quoted() -> None:\n    \"\"\"Bare-dot path renders as ``\".\"`` so the value reads as a path string.\"\"\"\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(deployment_file_path=\".\"),\n    )\n    out = render(display)\n    assert 'deployment_file_path: \".\"' in out\n\n\ndef test_render_strips_secret_mask_sentinels() -> None:\n    \"\"\"``SECRET_MASK`` values inside ``secrets`` are dropped before render —\n    the mask must never round-trip into apply input.\"\"\"\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(\n            secrets={\"REAL\": \"${REAL}\", \"MASKED\": SECRET_MASK},\n        ),\n    )\n    out = render(display, strip_mask_sentinels=True)\n    assert SECRET_MASK not in out\n    assert \"MASKED\" not in out\n    # The remaining real secret is still rendered.\n    assert \"REAL: ${REAL}\" in out\n\n\ndef test_render_drops_secrets_block_when_only_masks() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(\n            secrets={\"ONLY_MASKED\": SECRET_MASK},\n        ),\n    )\n    out = render(display, strip_mask_sentinels=True)\n    # No uncommented secrets block — falls into the unset/example branch.\n    assert \"  secrets:\\n    ONLY_MASKED\" not in out\n    assert SECRET_MASK not in out\n    # Commented-out example is what surfaces.\n    assert \"  # secrets:\" in out\n\n\ndef test_render_can_preserve_masked_secret_names_for_editor() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(\n            secrets={\"MY_SECRET\": SECRET_MASK},\n            personal_access_token=SECRET_MASK,\n        ),\n    )\n\n    out = render(display, strip_mask_sentinels=False)\n\n    assert \"  secrets:\\n    MY_SECRET: '********'\" in out\n    assert \"  personal_access_token: '********'\" in out\n\n\ndef test_render_strips_personal_access_token_mask() -> None:\n    display = DeploymentDisplay(\n        name=\"my-app\",\n        spec=DeploymentSpec(personal_access_token=SECRET_MASK),\n    )\n    out = render(display, strip_mask_sentinels=True)\n    assert SECRET_MASK not in out\n    # Falls through to the commented example.\n    assert \"  # personal_access_token:\" in out\n\n\ndef test_render_output_is_yaml_safe_loadable() -> None:\n    out = render(_full_display())\n    parsed = pyyaml.safe_load(out)\n    assert parsed[\"name\"] == \"my-app\"\n    assert \"display_name\" not in parsed[\"spec\"]\n    assert \"generate_name\" not in parsed[\"spec\"]\n    assert \"status\" not in parsed\n\n\ndef test_render_output_with_required_and_alternatives_is_yaml_safe_loadable() -> None:\n    display = DeploymentDisplay(\n        name=None,\n        spec=DeploymentSpec(repo_url=\"\"),\n    )\n    out = render(\n        display,\n        head=(\"hint\",),\n        required=(\"git_ref\",),\n        field_alternatives={\"repo_url\": (\"https://example.com/x\", \"detected\")},\n    )\n    parsed = pyyaml.safe_load(out)\n    assert \"name\" not in parsed\n    assert parsed[\"spec\"][\"git_ref\"] is None\n    assert parsed[\"spec\"][\"repo_url\"] == \"\"\n\n\ndef test_render_idempotent_for_same_input() -> None:\n    a = render(_full_display())\n    b = render(_full_display())\n    assert a == b\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"packages/*\"\n  - \"charts/*\"\n  - \"operator\"\n  - \"docs\"\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[dependency-groups]\ndev = [\n  \"pre-commit>=4.4.0\",\n  \"pytest>=9.0.3,<10\",\n  \"pytest-asyncio>=1.0.0\",\n  \"pytest-cov>=7.0.0\",\n  \"pytest-timeout>=2.4.0\",\n  \"pytest-xdist>=3.8.0\",\n  \"ruff>=0.14.5\",\n  \"ty>=0.0.15\"\n]\n\n[project]\nname = \"llama-agents-dev\"\nversion = \"0.1.0\"\ndescription = \"Monorepo workspace for the LlamaIndex Workflows packages.\"\nreadme = \"README.md\"\nlicense = \"MIT\"\nrequires-python = \">=3.10\"\ndependencies = [\n  \"types-pyyaml>=6.0.12.20250822\",\n  \"click>=8.1.7\",\n  \"httpx>=0.28.1\",\n  \"packaging>=24.1\",\n  \"pydantic>=2.12.3\",\n  \"rich>=14.0.0\",\n  \"tomlkit>=0.13.3\",\n  \"tomli>=2.3.0\",\n  \"ruamel.yaml>=0.18.0\"\n]\n\n[project.scripts]\ndev = \"dev_cli:main\"\n\n[tool.basedpyright]\ntypeCheckingMode = \"standard\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-index-workflows\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-client\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-server\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-index-utils-workflow\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-core\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-control-plane\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-appserver\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llamactl\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-agentcore\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"src/dev_cli\"\npythonVersion = \"3.14\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-integration-tests\"\npythonVersion = \"3.13\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"packages/llama-agents-dbos\"\npythonVersion = \"3.10\"\n\n[[tool.basedpyright.executionEnvironments]]\nroot = \"examples\"\npythonVersion = \"3.14\"\nreportMissingImports = false\n\n[tool.codespell]\nignore-words-list = \"NotIn\"\n\n[tool.coverage.run]\nomit = [\"**/tests/*\"]\n\n[tool.hatch.build.targets.wheel]\npackages = [\"src/dev_cli\"]\n\n[tool.mypy]\nexplicit_package_bases = true\nexclude = [\n  \"packages/llama-agents-server/tests/\",\n  \"packages/llama-agents-client/tests/\",\n  \"packages/llama-index-utils-workflow/tests/\",\n  \"tests/dev_cli/\",\n  \"packages/llama-index-workflows/src/llama_agents/\",\n  \"packages/llama-index-workflows/tests/test_llama_agents_alias.py\"\n]\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"module\"\nasyncio_default_test_loop_scope = \"module\"\ntestpaths = [\"tests/dev_cli\"]\naddopts = \"-nauto --timeout=10\"\n\n[tool.ruff.lint]\nselect = [\n  # default flake8 rules\n  \"E4\",\n  \"E7\",\n  \"E9\",\n  \"F\", # Pyflakes rules https://docs.astral.sh/ruff/rules/#pyflakes-f\n  \"I\", # sort imports https://docs.astral.sh/ruff/rules/#isort-i\n  \"UP007\", # Use X | Y for type annotations\n  \"UP045\", # Use X | None for type annotations\n  \"ANN001\", # Missing type annotation for function argument {name}\n  \"ANN002\", # Missing type annotation for *{name}\n  \"ANN003\", # Missing type annotation for **{name}\n  \"T201\" # no print statements\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"scripts/*\" = [\"T201\"]\n\"operator/dev.py\" = [\"T201\"]\n\"examples/**/*\" = [\"T201\"]\n\"packages/*/tests/**/*\" = [\n  \"T201\"\n]\n\"tests/dev_cli/**/*\" = [\n  \"T201\"\n]\n\"*.ipynb\" = [\"T201\", \"ANN001\", \"ANN002\", \"ANN003\"]\n\"__main__.py\" = [\"T201\"]\n\n[tool.ty.rules]\nunresolved-import = \"error\"\n\n[tool.ty.src]\n# Exclude llama_agents.workflows alias (dynamic import aliasing not understood by static analyzers)\nexclude = [\"**/uv.lock\", \"**/.gitignore\", \"**/llama_agents/workflows/**\", \"**/test_llama_agents_alias.py\"]\n\n[tool.uv]\n# boto3 is constrained to the range aioboto3/aiobotocore supports so that\n# the workspace can resolve both llama-agents-agentcore (which declares\n# boto3>=1.42.75) and llama-agents-control-plane (which uses aioboto3).\n# When llama-agents-agentcore is installed standalone the override does not\n# apply, so it will pull the newer boto3 it needs.\noverride-dependencies = [\"boto3>=1.40.46,<1.40.62\"]\n\n[tool.uv.sources]\nllama-agents-integration-tests = {workspace = true}\nllama-index-utils-workflow = {workspace = true}\nllama-index-workflows = {workspace = true}\nllama-agents-client = {workspace = true}\nllama-agents-server = {workspace = true}\nllama-agents-dbos = {workspace = true}\nllama-agents-core = {workspace = true}\nllama-agents-control-plane = {workspace = true}\nllama-agents-appserver = {workspace = true}\nllamactl = {workspace = true}\nllama-agents-agentcore = {workspace = true}\n\n[tool.uv.workspace]\nexclude = [\n]\nmembers = [\n  # core\n  \"packages/llama-index-workflows\",\n  # extensions\n  \"packages/llama-agents-client\",\n  \"packages/llama-agents-server\",\n  \"packages/llama-index-utils-workflow\",\n  # cloud\n  \"packages/llama-agents-core\",\n  \"packages/llama-agents-control-plane\",\n  \"packages/llama-agents-appserver\",\n  \"packages/llamactl\",\n  \"packages/llama-agents-agentcore\",\n  # integrations\n  \"packages/llama-agents-dbos\",\n  # internal\n  \"docs/api_docs\",\n  \"packages/llama-agents-integration-tests\",\n  # examples\n  \"examples/k8s-otel\"\n]\n"
  },
  {
    "path": "scripts/process_manifests.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nProcess generated manifests from kubebuilder and create Helm templates.\nThis preserves Helm templating while keeping kubebuilder annotations as source of truth.\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\nimport yaml\n\n# Resolve repo root relative to this script's location (scripts/ or ../scripts/ from operator/)\nREPO_ROOT = Path(__file__).resolve().parent.parent\n\n\ndef process_rbac() -> bool:\n    \"\"\"Process RBAC manifest from kubebuilder output.\"\"\"\n    rbac_file = REPO_ROOT / \"operator/config/rbac/role.yaml\"\n    output_file = REPO_ROOT / \"charts/llama-agents/templates/rbac.yaml\"\n\n    if not rbac_file.exists():\n        print(f\"Error: {rbac_file} not found\")\n        return False\n\n    # Read generated RBAC\n    with open(rbac_file) as f:\n        rbac = yaml.safe_load(f)\n\n    if rbac.get(\"kind\") != \"ClusterRole\":\n        print(f\"Error: Expected ClusterRole, got {rbac.get('kind')}\")\n        return False\n\n    # Create Helm template with conditional and proper metadata.\n    # Split mode (apps namespace != release namespace): apps-ns Role carries\n    # every rule except leader-election leases, release-ns Role carries only\n    # the leases rule (controller-runtime places the Lease in the operator\n    # pod's own namespace). Single-namespace mode collapses to one Role.\n    template = \"\"\"{{- if .Values.rbac.create }}\n{{- $appsNs := include \"llama-agents.apps.namespace\" . -}}\n{{- $releaseNs := .Release.Namespace -}}\n{{- $split := include \"llama-agents.apps.splitNamespace\" . -}}\n{{- $saName := include \"llama-agents.serviceAccountName\" . -}}\n{{- /*\nSplit mode (apps != release): apps-ns Role with everything except leases,\nrelease-ns Role with only leases (leader-election Lease lives in the\noperator pod's namespace). Single-namespace mode: one combined Role.\n*/ -}}\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {{ $saName }}\n  namespace: {{ $appsNs }}\n  {{- with .Values.rbac.roleAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nrules:\n\"\"\"\n\n    # Add the generated rules. The leader-election leases rule is conditional:\n    # in split mode it moves to a separate release-ns Role below.\n    for rule in rbac.get(\"rules\", []):\n        api_groups = rule.get(\"apiGroups\", [])\n        resources = rule.get(\"resources\", [])\n        verbs = rule.get(\"verbs\", [])\n\n        is_leases = api_groups == [\"coordination.k8s.io\"] and resources == [\"leases\"]\n        if is_leases:\n            template += \"{{- if not $split }}\\n\"\n        template += f\"- apiGroups: {api_groups}\\n\"\n        template += f\"  resources: {resources}\\n\"\n        template += f\"  verbs: {verbs}\\n\"\n        if is_leases:\n            template += \"{{- end }}\\n\"\n\n    # Add RoleBinding (namespace-scoped) instead of ClusterRoleBinding\n    template += \"\"\"---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {{ $saName }}\n  namespace: {{ $appsNs }}\n  {{- with .Values.rbac.roleBindingAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: {{ $saName }}\nsubjects:\n- kind: ServiceAccount\n  name: {{ $saName }}\n  namespace: {{ $releaseNs }}\n{{- if $split }}\n---\n# Leader-election Lease lives in the operator pod's namespace.\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {{ $saName }}-leader-election\n  namespace: {{ $releaseNs }}\n  {{- with .Values.rbac.roleAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nrules:\n- apiGroups: ['coordination.k8s.io']\n  resources: ['leases']\n  verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch']\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {{ $saName }}-leader-election\n  namespace: {{ $releaseNs }}\n  {{- with .Values.rbac.roleBindingAnnotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: {{ $saName }}-leader-election\nsubjects:\n- kind: ServiceAccount\n  name: {{ $saName }}\n  namespace: {{ $releaseNs }}\n{{- end }}\n{{- end }}\n\"\"\"\n\n    # Write to Helm template\n    with open(output_file, \"w\") as f:\n        f.write(template)\n\n    print(f\"✅ Generated RBAC template with {len(rbac.get('rules', []))} rules\")\n    return True\n\n\ndef process_crds() -> bool:\n    \"\"\"Process CRD manifests from kubebuilder output.\"\"\"\n    crd_files = [\n        REPO_ROOT\n        / \"operator/config/crd/bases/deploy.llamaindex.ai_llamadeployments.yaml\",\n        REPO_ROOT\n        / \"operator/config/crd/bases/deploy.llamaindex.ai_llamadeploymenttemplates.yaml\",\n    ]\n\n    # Output 1: Raw CRD files in main chart's crds/ directory (install-only, no templating)\n    crds_dir = REPO_ROOT / \"charts/llama-agents/crds\"\n    crds_dir.mkdir(exist_ok=True)\n\n    # Output 2: Raw CRD files in CRD chart's files/ directory (Helm template adds annotations)\n    crd_chart_files_dir = REPO_ROOT / \"charts/llama-agents-crds/files\"\n    crd_chart_files_dir.mkdir(parents=True, exist_ok=True)\n\n    for crd_file in crd_files:\n        if not crd_file.exists():\n            print(f\"Error: {crd_file} not found\")\n            return False\n        content = crd_file.read_text()\n\n        # Copy raw CRD to both destinations\n        (crds_dir / crd_file.name).write_text(content)\n        (crd_chart_files_dir / crd_file.name).write_text(content)\n\n    print(\"Generated CRD files in crds/ directory (install-only)\")\n    print(\"Generated CRD files in llama-agents-crds/files/ (for CRD chart)\")\n    return True\n\n\ndef main() -> None:\n    \"\"\"Main entry point.\"\"\"\n    print(\"Processing kubebuilder-generated manifests...\")\n\n    success = True\n\n    # Process RBAC\n    if not process_rbac():\n        success = False\n\n    # Process CRDs\n    if not process_crds():\n        success = False\n\n    if success:\n        print(\"✅ All manifests processed successfully\")\n    else:\n        print(\"❌ Some manifests failed to process\")\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/sync-docs-to-developer-hub.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nDOCS_REPO_ARG=\"${1:?Usage: $0 /path/to/developer-hub-repo}\"\n# Resolve to an absolute path up front: later commands `cd` before using this,\n# so a relative path would resolve against the wrong directory.\nmkdir -p \"$DOCS_REPO_ARG\"\nDOCS_REPO=\"$(cd \"$DOCS_REPO_ARG\" && pwd)\"\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/..\" && pwd)\"\n\n# --- Markdown docs ---\nSOURCE_DIR=\"$REPO_ROOT/docs/src/content/docs/llamaagents\"\nDEST_DIR=\"$DOCS_REPO/src/content/docs/python/llamaagents\"\n\necho \"=== Syncing markdown docs ===\"\nmkdir -p \"$DEST_DIR\"\n\nrsync -av --delete \\\n  --include='*/' \\\n  --include='*.md' \\\n  --include='*.mdx' \\\n  --include='*.yml' \\\n  --include='*.png' \\\n  --include='*.jpg' \\\n  --include='*.jpeg' \\\n  --include='*.svg' \\\n  --exclude='*' \\\n  \"$SOURCE_DIR/\" \"$DEST_DIR/\"\n\n# --- API reference (mkdocs HTML) ---\nAPI_DOCS_DIR=\"$REPO_ROOT/docs/api_docs\"\nAPI_DEST_DIR=\"$DOCS_REPO/api-reference/python/workflows\"\n\necho \"=== Building workflows API reference ===\"\ncd \"$API_DOCS_DIR\"\nuv sync --locked\nuv run mkdocs build -d \"$API_DEST_DIR\"\n\necho \"Docs sync complete.\"\n"
  },
  {
    "path": "src/dev_cli/__init__.py",
    "content": "from __future__ import annotations\n\nfrom .cli import _maybe_inject_pytest_subcommand, cli, pytest_cmd\n\n__all__ = [\"cli\", \"main\", \"pytest_main\"]\n\n\ndef main() -> None:\n    \"\"\"Console script entry point.\"\"\"\n    _maybe_inject_pytest_subcommand()\n    cli()\n\n\ndef pytest_main() -> None:\n    \"\"\"Shortcut entry point for 'dev pytest'.\"\"\"\n    pytest_cmd()\n"
  },
  {
    "path": "src/dev_cli/changesets.py",
    "content": "\"\"\"\nThis is a script called by the changeset bot. Normally changeset can do the following things, but this is a mixed ts and python repo, so we need to do some extra things.\n\nThere's 2 things this does:\n- Versioning: Makes changes that may be committed with the newest version.\n- Releasing/Tagging: After versions are changed, we check each package to see if its released, and if not, we release it and tag it.\n\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nimport subprocess\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass\nfrom dataclasses import field as dataclasses_field\nfrom io import StringIO\nfrom pathlib import Path\nfrom typing import Annotated, Any, Generator, List, Literal, cast\n\nimport click\nimport tomlkit\nfrom packaging.version import Version\nfrom pydantic import BaseModel, Field, computed_field\nfrom ruamel.yaml import YAML\n\nPlatform = Literal[\"linux/amd64\", \"linux/arm64\"]\nPLATFORMS: tuple[Platform, ...] = (\"linux/amd64\", \"linux/arm64\")\n\n# Valid PEP 440 pre-release labels. Semver pre-release identifiers must use\n# these same labels (e.g. 1.2.3-a.4, not 1.2.3-alpha.4) so that the\n# conversion is purely structural.\n_PEP440_LABELS = {\"a\", \"b\", \"rc\"}\n\n_SEMVER_PRERELEASE_RE = re.compile(r\"^(\\d+\\.\\d+\\.\\d+)-([a-zA-Z]+)\\.(\\d+)$\")\n\n\ndef run_command(\n    cmd: List[str], cwd: Path | None = None, env: dict[str, str] | None = None\n) -> None:\n    \"\"\"Run a command, streaming output to the console, and raise on failure.\"\"\"\n    subprocess.run(cmd, check=True, text=True, cwd=cwd or Path.cwd(), env=env)\n\n\ndef run_and_capture(\n    cmd: List[str], cwd: Path | None = None, env: dict[str, str] | None = None\n) -> str:\n    \"\"\"Run a command and return stdout as text, raising on failure.\"\"\"\n    result = subprocess.run(\n        cmd,\n        check=True,\n        text=True,\n        cwd=cwd or Path.cwd(),\n        env=env,\n        capture_output=True,\n    )\n    return result.stdout\n\n\ndef semver_to_pep440(version: str) -> str:\n    \"\"\"Convert a semver version string to PEP 440 format.\n\n    Only PEP 440-compatible pre-release labels are accepted (a, b, rc):\n        1.2.3-a.4   -> 1.2.3a4\n        1.2.3-b.1   -> 1.2.3b1\n        1.2.3-rc.2  -> 1.2.3rc2\n\n    Non-prerelease versions pass through unchanged.\n    \"\"\"\n    match = _SEMVER_PRERELEASE_RE.match(version)\n    if not match:\n        return version\n\n    base, label, num = match.groups()\n    if label not in _PEP440_LABELS:\n        raise ValueError(\n            f\"Unsupported pre-release label '{label}' in version '{version}'. \"\n            f\"Use a PEP 440 label: {', '.join(sorted(_PEP440_LABELS))}\"\n        )\n    return f\"{base}{label}{num}\"\n\n\ndef pep440_to_semver(version: str) -> str:\n    \"\"\"Convert a PEP 440 version string to semver format.\n\n    Pre-release versions are converted:\n        1.2.3a4   -> 1.2.3-a.4\n        1.2.3b1   -> 1.2.3-b.1\n        1.2.3rc2  -> 1.2.3-rc.2\n\n    Non-prerelease versions pass through unchanged.\n    \"\"\"\n    v = Version(version)\n    base = \".\".join(str(x) for x in v.release)\n    if v.pre is None:\n        return base\n\n    label, num = v.pre\n    return f\"{base}-{label}.{num}\"\n\n\nclass DockerConfig(BaseModel):\n    dockerfile: str\n    imageName: str\n    target: str | None = None\n    platforms: list[Platform] = Field(default_factory=list)\n    # GHA buildx cache mode. ``max`` caches every intermediate stage\n    # (expensive to upload but best reuse); ``min`` only caches the\n    # final layers. Use ``min`` for small/static binary builds (e.g.\n    # the operator) where uploading the base image + builder layers\n    # to GHA cache costs more than a clean rebuild.\n    cacheMode: Literal[\"min\", \"max\"] = \"max\"\n\n\nclass HelmConfig(BaseModel):\n    registry: str\n\n\n# syncValues type: filename -> {dot_path -> template_string}\nSyncValues = dict[str, dict[str, str]]\n\n\nclass PublishConfig(BaseModel):\n    \"\"\"Per-type publish toggles.  All default to ``True``; set to ``False``\n    to suppress a specific publish channel even when the corresponding\n    config (docker/helm/pyproject.toml) exists.\"\"\"\n\n    pypi: bool = True\n    docker: bool = True\n    helm: bool = True\n\n\nclass PackageJsonFile(BaseModel):\n    \"\"\"Schema for a package.json file on disk.\"\"\"\n\n    name: str\n    docker: DockerConfig | None = None\n    helm: HelmConfig | None = None\n    publish: PublishConfig = Field(default_factory=PublishConfig)\n    syncValues: dict[str, dict[str, str]] = Field(default_factory=dict)\n    postVersion: list[str] = Field(default_factory=list)\n\n\n@dataclass\nclass PackageJson:\n    name: str\n    version: str\n    path: Path\n    private: bool\n    docker: DockerConfig | None = None\n    helm: HelmConfig | None = None\n    publish: PublishConfig = dataclasses_field(default_factory=PublishConfig)\n    syncValues: SyncValues = dataclasses_field(default_factory=dict)\n    postVersion: list[str] = dataclasses_field(default_factory=list)\n\n    def should_publish_pypi(self) -> bool:\n        return not self.private and self.publish.pypi\n\n    def should_publish_docker(self) -> bool:\n        return not self.private and self.publish.docker\n\n    def should_publish_helm(self) -> bool:\n        return not self.private and self.publish.helm\n\n\n# --- syncValues engine ---\n\n# Template pattern: {package-name:property} or {self:property}\n_TEMPLATE_RE = re.compile(r\"\\{([^}:]+):([^}]+)\\}\")\n\n\ndef _resolve_template(\n    template: str,\n    self_pkg: PackageJson,\n    workspace_packages: dict[str, PackageJson],\n) -> str:\n    \"\"\"Resolve a syncValues template string like ``{pkg:dockerTag}``.\"\"\"\n\n    def _replace(match: re.Match[str]) -> str:\n        pkg_ref, prop = match.group(1), match.group(2)\n        template_str = f\"{{{pkg_ref}:{prop}}}\"\n        if pkg_ref == \"self\":\n            pkg = self_pkg\n        else:\n            pkg = workspace_packages.get(pkg_ref)\n            if pkg is None:\n                raise ValueError(\n                    f\"syncValues template '{template_str}' references \"\n                    f\"unknown package '{pkg_ref}'\"\n                )\n        if prop == \"version\":\n            return pkg.version\n        if prop == \"pep440Version\":\n            return semver_to_pep440(pkg.version)\n        if prop == \"dockerTag\":\n            if pkg.docker is None:\n                raise ValueError(\n                    f\"syncValues template '{template_str}' references \"\n                    f\"dockerTag but package '{pkg.name}' has no docker config\"\n                )\n            return pkg.version\n        raise ValueError(\n            f\"syncValues template '{template_str}' uses unknown property '{prop}'\"\n        )\n\n    return _TEMPLATE_RE.sub(_replace, template)\n\n\ndef _apply_dot_path_updates(data: Any, updates: dict[str, str]) -> bool:\n    \"\"\"Walk dot-paths and set values in a nested dict-like structure.\n\n    Returns True if any value changed.\n    \"\"\"\n    changed = False\n    for dot_path, value in updates.items():\n        keys = dot_path.split(\".\")\n        obj = data\n        for key in keys[:-1]:\n            if key not in obj:\n                obj[key] = {}\n            obj = obj[key]\n        last_key = keys[-1]\n        old = obj.get(last_key)\n        if old is None or str(old) != value:\n            obj[last_key] = value\n            changed = True\n    return changed\n\n\ndef _write_yaml_values(file_path: Path, updates: dict[str, str]) -> bool:\n    \"\"\"Set dot-path values in a YAML file using ruamel.yaml (comment-preserving).\n\n    Returns True if the file was changed.\n    \"\"\"\n    if not file_path.exists():\n        return False\n\n    yaml = YAML()\n    yaml.preserve_quotes = True  # type: ignore[assignment]\n    data = yaml.load(file_path.read_text())\n\n    if not _apply_dot_path_updates(data, updates):\n        return False\n\n    stream = StringIO()\n    yaml.dump(data, stream)\n    file_path.write_text(stream.getvalue())\n    return True\n\n\ndef _write_toml_values(file_path: Path, updates: dict[str, str]) -> bool:\n    \"\"\"Set dot-path values in a TOML file using tomlkit (comment-preserving).\n\n    Returns True if the file was changed.\n    \"\"\"\n    if not file_path.exists():\n        return False\n\n    doc = tomlkit.parse(file_path.read_text())\n\n    if not _apply_dot_path_updates(doc, updates):\n        return False\n\n    file_path.write_text(tomlkit.dumps(doc))\n    return True\n\n\ndef _write_file_values(file_path: Path, updates: dict[str, str]) -> bool:\n    \"\"\"Dispatch to the correct writer based on file extension.\"\"\"\n    suffix = file_path.suffix\n    if suffix in (\".yaml\", \".yml\"):\n        return _write_yaml_values(file_path, updates)\n    if suffix == \".toml\":\n        return _write_toml_values(file_path, updates)\n    raise ValueError(f\"Unsupported file type '{suffix}' for syncValues: {file_path}\")\n\n\ndef apply_sync_values(\n    pkg: PackageJson,\n    workspace_packages: dict[str, PackageJson],\n) -> bool:\n    \"\"\"Apply all sync entries (explicit + implicit) for a package.\n\n    Explicit entries come from ``syncValues`` in package.json. Implicit\n    entries are added for helm packages (Chart.yaml version) and packages\n    with a pyproject.toml (project.version in PEP 440 form). Writers\n    skip gracefully when the target file doesn't exist.\n\n    Returns True if any file was changed.\n    \"\"\"\n    all_entries: dict[str, dict[str, str]] = {}\n\n    for filename, paths in pkg.syncValues.items():\n        resolved = {\n            dp: _resolve_template(tpl, pkg, workspace_packages)\n            for dp, tpl in paths.items()\n        }\n        all_entries.setdefault(filename, {}).update(resolved)\n\n    if pkg.helm is not None:\n        all_entries.setdefault(\"Chart.yaml\", {}).setdefault(\"version\", pkg.version)\n\n    if (pkg.path / \"pyproject.toml\").exists():\n        all_entries.setdefault(\"pyproject.toml\", {}).setdefault(\n            \"project.version\", semver_to_pep440(pkg.version)\n        )\n\n    changed = False\n    for filename, updates in all_entries.items():\n        file_path = pkg.path / filename\n        if _write_file_values(file_path, updates):\n            click.echo(f\"Updated {file_path}\")\n            changed = True\n\n    return changed\n\n\ndef _read_package_json_config(package_dir: Path) -> PackageJsonFile | None:\n    \"\"\"Parse custom fields from a package.json file using Pydantic.\"\"\"\n    package_json_path = package_dir / \"package.json\"\n    if not package_json_path.exists():\n        return None\n    data = json.loads(package_json_path.read_text())\n    return PackageJsonFile.model_validate(data)\n\n\ndef get_pnpm_workspace_packages() -> list[PackageJson]:\n    \"\"\"Return directories for all workspace packages from pnpm list JSON output.\"\"\"\n    output = run_and_capture([\"pnpm\", \"list\", \"-r\", \"--depth=-1\", \"--json\"])\n\n    package_json = cast(list[dict[str, Any]], json.loads(output))\n    packages: list[PackageJson] = []\n    for data in package_json:\n        pkg_path = Path(data[\"path\"])\n        config = _read_package_json_config(pkg_path)\n        packages.append(\n            PackageJson(\n                name=data[\"name\"],\n                version=data[\"version\"],\n                path=pkg_path,\n                private=data.get(\"private\", True),\n                docker=config.docker if config else None,\n                helm=config.helm if config else None,\n                publish=config.publish if config else PublishConfig(),\n                syncValues=config.syncValues if config else {},\n                postVersion=config.postVersion if config else [],\n            )\n        )\n    return packages\n\n\ndef _pypi_packages(packages: list[PackageJson]) -> Generator[Path, None, None]:\n    \"\"\"Yield pyproject.toml paths for packages that should be published to PyPI.\"\"\"\n    for package in packages:\n        if not package.should_publish_pypi():\n            continue\n        pyproject = package.path / \"pyproject.toml\"\n        if pyproject.exists():\n            yield pyproject\n\n\ndef lock_python_dependencies() -> None:\n    \"\"\"Lock Python dependencies.\"\"\"\n    try:\n        run_command([\"uv\", \"lock\"])\n        click.echo(\"Locked Python dependencies\")\n    except subprocess.CalledProcessError as e:\n        click.echo(f\"Warning: Failed to lock Python dependencies: {e}\", err=True)\n\n\n@click.group()\ndef cli() -> None:\n    \"\"\"Changeset-based version management for llama-cloud-services.\"\"\"\n    pass\n\n\ndef current_version(pyproject: Path) -> tuple[str, str]:\n    \"\"\"Return (package_name, version_str) taken from the given pyproject.toml.\"\"\"\n    toml_doc, py_doc = PyProjectContainer.parse(pyproject.read_text())\n    name = py_doc.project.name\n    version = str(Version(py_doc.project.version))  # normalise\n    return name, version\n\n\ndef is_published(\n    name: str, version: str, index_url: str = \"https://pypi.org/pypi\"\n) -> bool:\n    \"\"\"\n    True  → `<name>==<version>` exists on the given index\n    False → package missing *or* version missing\n    \"\"\"\n    url = f\"{index_url.rstrip('/')}/{name}/json\"\n    try:\n        data = json.load(urllib.request.urlopen(url))\n    except urllib.error.HTTPError as e:  # 404 → package not published at all\n        if e.code == 404:\n            return False\n        raise  # any other error should surface\n    return version in data[\"releases\"]  # keys are version strings\n\n\nDOCKER_REGISTRY = \"docker.io\"\n\n\ndef is_rc_version(version: str) -> bool:\n    \"\"\"Return True if version is a pre-release (RC, alpha, or beta).\"\"\"\n    return bool(re.search(r\"(-rc|-a|-b|rc\\d|a\\d|b\\d)\", version))\n\n\ndef is_docker_image_published(repository: str, tag: str) -> bool:\n    \"\"\"Check if a Docker image tag exists on Docker Hub.\n\n    Returns True if the tag exists, False if not (404).\n    Raises on unexpected HTTP errors.\n    \"\"\"\n    url = f\"https://hub.docker.com/v2/repositories/{repository}/tags/{tag}\"\n    try:\n        urllib.request.urlopen(url)\n    except urllib.error.HTTPError as e:\n        if e.code == 404:\n            return False\n        raise\n    return True\n\n\ndef docker_image_tags(image: DockerConfig, version: str, is_rc: bool) -> list[str]:\n    \"\"\"Generate the full list of Docker tags for an image.\"\"\"\n    repo = f\"{DOCKER_REGISTRY}/{image.imageName}\"\n    tags = [f\"{repo}:{version}\"]\n    if not is_rc:\n        tags.append(f\"{repo}:latest\")\n        major_minor = \".\".join(version.split(\".\")[:2])\n        tags.append(f\"{repo}:{major_minor}\")\n    return tags\n\n\ndef is_helm_chart_published(chart_name: str, version: str) -> bool:\n    \"\"\"Check if a Helm chart version is already published in the OCI registry.\"\"\"\n    url = (\n        f\"https://hub.docker.com/v2/repositories/llamaindex/{chart_name}/tags/{version}\"\n    )\n    try:\n        urllib.request.urlopen(url)\n    except urllib.error.HTTPError as e:\n        if e.code == 404:\n            return False\n        raise\n    return True\n\n\n# ---------------------------------------------------------------------------\n# Publish plan: a declarative description of the work needed to release\n# the current workspace. ``build_publish_plan`` emits it; the\n# ``execute_*_action`` helpers consume one entry at a time. ``dev\n# changeset-publish`` runs them all sequentially for local releases; CI\n# fans the same actions out into a GitHub Actions matrix so docker\n# builds run natively (amd64 / arm64) in parallel.\n# ---------------------------------------------------------------------------\n\n\nclass PypiAction(BaseModel):\n    kind: Literal[\"pypi\"] = \"pypi\"\n    package: str\n    version: str\n    path: str  # package dir relative to repo root\n\n    @computed_field  # type: ignore[prop-decorator]\n    @property\n    def id(self) -> str:\n        return f\"pypi:{self.package}\"\n\n\nclass DockerBuildAction(BaseModel):\n    kind: Literal[\"docker\"] = \"docker\"\n    package: str\n    image: str  # imageName without registry\n    dockerfile: str\n    target: str | None = None\n    platform: Platform\n    version: str\n    build_tag: str  # full registry/repo:version-<arch>\n    cache_scope: str  # GHA buildx cache scope (per package + arch)\n    cache_mode: Literal[\"min\", \"max\"] = \"max\"  # GHA buildx cache-to mode\n\n    @computed_field  # type: ignore[prop-decorator]\n    @property\n    def id(self) -> str:\n        return f\"docker:{self.image}|{self.platform}\"\n\n\nclass DockerManifestAction(BaseModel):\n    kind: Literal[\"docker-manifest\"] = \"docker-manifest\"\n    package: str\n    image: str\n    version: str\n    final_tags: list[str]  # full tags that should resolve to the manifest\n    source_tags: list[str]  # per-arch tags produced by docker-build jobs\n\n    @computed_field  # type: ignore[prop-decorator]\n    @property\n    def id(self) -> str:\n        return f\"docker-manifest:{self.image}\"\n\n\nclass HelmAction(BaseModel):\n    kind: Literal[\"helm\"] = \"helm\"\n    package: str\n    chart_path: str\n    version: str\n    registry: str\n\n    @computed_field  # type: ignore[prop-decorator]\n    @property\n    def id(self) -> str:\n        return f\"helm:{self.package}\"\n\n\nPublishAction = Annotated[\n    PypiAction | DockerBuildAction | DockerManifestAction | HelmAction,\n    Field(discriminator=\"kind\"),\n]\n\n\nclass PublishPlan(BaseModel):\n    pypi: list[PypiAction] = Field(default_factory=list)\n    docker_builds: list[DockerBuildAction] = Field(default_factory=list)\n    docker_manifests: list[DockerManifestAction] = Field(default_factory=list)\n    helm: list[HelmAction] = Field(default_factory=list)\n\n    def all_actions(self) -> list[PublishAction]:\n        return [*self.pypi, *self.docker_builds, *self.docker_manifests, *self.helm]\n\n    def find(self, action_id: str) -> PublishAction:\n        for action in self.all_actions():\n            if action.id == action_id:\n                return action\n        raise KeyError(f\"No action with id {action_id!r} in plan\")\n\n    @computed_field  # type: ignore[prop-decorator]\n    @property\n    def has_work(self) -> bool:\n        return bool(\n            self.pypi or self.docker_builds or self.docker_manifests or self.helm\n        )\n\n\ndef _platform_suffix(platform: str) -> str:\n    \"\"\"Map a docker platform string to a short tag suffix.\"\"\"\n    # \"linux/amd64\" -> \"amd64\", \"linux/arm64/v8\" -> \"arm64\"\n    parts = platform.split(\"/\")\n    if len(parts) >= 2:\n        return parts[1]\n    return platform.replace(\"/\", \"-\")\n\n\ndef plan_pypi(packages: list[PackageJson]) -> list[PypiAction]:\n    \"\"\"Return PyPI actions for packages whose version is not yet published.\"\"\"\n    actions: list[PypiAction] = []\n    repo_root = Path.cwd()\n    for pyproject in _pypi_packages(packages):\n        name, version = current_version(pyproject)\n        if is_published(name, version):\n            continue\n        try:\n            rel = pyproject.parent.relative_to(repo_root)\n            path_str = str(rel)\n        except ValueError:\n            path_str = str(pyproject.parent)\n        actions.append(PypiAction(package=name, version=version, path=path_str))\n    return actions\n\n\ndef plan_docker(\n    packages: list[PackageJson],\n) -> tuple[list[DockerBuildAction], list[DockerManifestAction]]:\n    \"\"\"Return (per-arch build actions, manifest-merge actions) for unpublished images.\"\"\"\n    builds: list[DockerBuildAction] = []\n    manifests: list[DockerManifestAction] = []\n\n    for pkg in packages:\n        image = pkg.docker\n        if image is None or not pkg.should_publish_docker():\n            continue\n        version = pkg.version\n        if is_docker_image_published(image.imageName, version):\n            continue\n\n        rc = is_rc_version(version)\n        final_tags = docker_image_tags(image, version, rc)\n        repo = f\"{DOCKER_REGISTRY}/{image.imageName}\"\n        source_tags: list[str] = []\n        for platform in image.platforms:\n            suffix = _platform_suffix(platform)\n            build_tag = f\"{repo}:{version}-{suffix}\"\n            source_tags.append(build_tag)\n            builds.append(\n                DockerBuildAction(\n                    package=pkg.name,\n                    image=image.imageName,\n                    dockerfile=image.dockerfile,\n                    target=image.target,\n                    platform=platform,\n                    version=version,\n                    build_tag=build_tag,\n                    cache_scope=f\"{pkg.name}-{suffix}\",\n                    cache_mode=image.cacheMode,\n                )\n            )\n        manifests.append(\n            DockerManifestAction(\n                package=pkg.name,\n                image=image.imageName,\n                version=version,\n                final_tags=final_tags,\n                source_tags=source_tags,\n            )\n        )\n\n    return builds, manifests\n\n\ndef plan_helm(packages: list[PackageJson]) -> list[HelmAction]:\n    \"\"\"Return Helm chart actions for charts not yet pushed to the registry.\"\"\"\n    actions: list[HelmAction] = []\n    repo_root = Path.cwd()\n    for pkg in packages:\n        if pkg.helm is None or not pkg.should_publish_helm():\n            continue\n        if is_helm_chart_published(pkg.name, pkg.version):\n            continue\n        try:\n            chart_path = str(pkg.path.relative_to(repo_root))\n        except ValueError:\n            chart_path = str(pkg.path)\n        actions.append(\n            HelmAction(\n                package=pkg.name,\n                chart_path=chart_path,\n                version=pkg.version,\n                registry=pkg.helm.registry,\n            )\n        )\n    return actions\n\n\ndef build_publish_plan(packages: list[PackageJson]) -> PublishPlan:\n    \"\"\"Scan workspace packages and produce a complete PublishPlan.\"\"\"\n    builds, manifests = plan_docker(packages)\n    return PublishPlan(\n        pypi=plan_pypi(packages),\n        docker_builds=builds,\n        docker_manifests=manifests,\n        helm=plan_helm(packages),\n    )\n\n\ndef execute_action(action: PublishAction, dry_run: bool = False) -> None:\n    \"\"\"Execute a single publish action, dispatching on its kind.\"\"\"\n    if isinstance(action, PypiAction):\n        _execute_pypi(action, dry_run)\n    elif isinstance(action, DockerBuildAction):\n        _execute_docker_build(action, dry_run)\n    elif isinstance(action, DockerManifestAction):\n        _execute_docker_manifest(action, dry_run)\n    elif isinstance(action, HelmAction):\n        _execute_helm(action, dry_run)\n    else:  # pragma: no cover - exhaustive\n        raise TypeError(f\"Unknown action: {action!r}\")\n\n\ndef _execute_pypi(action: PypiAction, dry_run: bool) -> None:\n    \"\"\"Build and publish a single PyPI package.\n\n    In a uv workspace ``uv build`` always writes artifacts to the\n    workspace-root ``dist/`` regardless of which directory it was\n    invoked from, so we build with ``--package`` and then publish the\n    specific files by glob from the repo root. ``uv publish`` picks up\n    ``UV_PUBLISH_TOKEN`` from the environment; without it, it falls\n    back to PyPI trusted publishing.\n    \"\"\"\n    click.echo(f\"Publishing PyPI package {action.package}@{action.version}\")\n    if dry_run:\n        click.echo(\"  dry run, skipping uv build / uv publish\")\n        return\n    # Guard against stale plans: the plan is generated in a different job\n    # from a potentially different working tree than this one, so confirm\n    # the checked-out package still matches the version we intend to\n    # publish before invoking uv build (which silently builds whatever\n    # version is on disk).\n    pyproject = Path.cwd() / action.path / \"pyproject.toml\"\n    _, on_disk = current_version(pyproject)\n    if on_disk != action.version:\n        raise RuntimeError(\n            f\"Plan expects {action.package}@{action.version} but \"\n            f\"{pyproject} is at {on_disk}. The plan was generated from a \"\n            f\"different workspace state than the current checkout.\"\n        )\n    run_command([\"uv\", \"build\", \"--package\", action.package])\n    dist_prefix = action.package.replace(\"-\", \"_\")\n    pattern = f\"dist/{dist_prefix}-{action.version}*\"\n    files = sorted(Path.cwd().glob(pattern))\n    if not files:\n        raise RuntimeError(\n            f\"uv build produced no artifacts matching {pattern} for \"\n            f\"{action.package}@{action.version}\"\n        )\n    run_command([\"uv\", \"publish\", *[str(f) for f in files]])\n\n\ndef _execute_docker_build(action: DockerBuildAction, dry_run: bool) -> None:\n    \"\"\"Build and push a single-arch docker image for one platform.\"\"\"\n    click.echo(f\"Building {action.image} ({action.platform}) -> {action.build_tag}\")\n    cmd = [\n        \"docker\",\n        \"buildx\",\n        \"build\",\n        \"--push\",\n        \"--file\",\n        action.dockerfile,\n        \"--platform\",\n        action.platform,\n        \"--tag\",\n        action.build_tag,\n    ]\n    if action.target:\n        cmd.extend([\"--target\", action.target])\n    # GHA buildx cache. Only emits cache directives under GitHub Actions\n    # with the runtime token exposed (see ``crazy-max/ghaction-github-runtime``).\n    # ``compression=zstd`` shrinks the exported cache blobs, and\n    # ``ignore-error=true`` keeps a flaky cache push from failing the\n    # whole job (the image is already pushed to the registry by then).\n    if os.environ.get(\"ACTIONS_RUNTIME_TOKEN\") and os.environ.get(\"ACTIONS_CACHE_URL\"):\n        cmd.extend(\n            [\n                \"--cache-from\",\n                f\"type=gha,scope={action.cache_scope}\",\n                \"--cache-to\",\n                (\n                    f\"type=gha,mode={action.cache_mode},scope={action.cache_scope}\"\n                    \",compression=zstd,ignore-error=true\"\n                ),\n            ]\n        )\n    cmd.append(\".\")\n    if dry_run:\n        click.echo(f\"  dry run: {' '.join(cmd)}\")\n        return\n    run_command(cmd)\n\n\ndef _execute_docker_manifest(action: DockerManifestAction, dry_run: bool) -> None:\n    \"\"\"Combine per-arch tags into a multi-arch manifest under each final tag.\"\"\"\n    click.echo(\n        f\"Creating manifest for {action.image}:{action.version} -> {action.final_tags}\"\n    )\n    cmd = [\"docker\", \"buildx\", \"imagetools\", \"create\"]\n    for tag in action.final_tags:\n        cmd.extend([\"--tag\", tag])\n    cmd.extend(action.source_tags)\n    if dry_run:\n        click.echo(f\"  dry run: {' '.join(cmd)}\")\n        return\n    run_command(cmd)\n\n\ndef _execute_helm(action: HelmAction, dry_run: bool) -> None:\n    \"\"\"Package and push a single Helm chart.\"\"\"\n    tgz = f\"{action.package}-{action.version}.tgz\"\n    click.echo(f\"Publishing Helm chart {tgz} -> {action.registry}\")\n    if dry_run:\n        click.echo(f\"  dry run: helm package {action.chart_path}\")\n        click.echo(f\"  dry run: helm push {tgz} {action.registry}\")\n        return\n    run_command([\"helm\", \"package\", action.chart_path])\n    run_command([\"helm\", \"push\", tgz, action.registry])\n\n\nif __name__ == \"__main__\":\n    cli()\n\n\nclass PyProjectContainer(BaseModel):\n    project: PyProject\n\n    @classmethod\n    def parse(cls, text: str) -> tuple[Any, PyProjectContainer]:\n        doc = tomlkit.parse(text)\n        return doc, PyProjectContainer.model_validate(doc)\n\n\nclass PyProject(BaseModel):\n    name: str\n    version: str\n    dependencies: list[str] = Field(default_factory=list)\n"
  },
  {
    "path": "src/dev_cli/cli.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"CLI entry point for dev.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\n\nimport click\n\nfrom . import gha, git_utils, index_html, versioning\nfrom .commands.changesets_cmd import changeset_publish, changeset_version\nfrom .commands.publish_cmd import changeset_plan, publish_action\nfrom .commands.pytest_cmd import pytest_cmd\nfrom .commands.skills_cmd import sync_skills\n\n# Known subcommands for argument injection\n_KNOWN_SUBCOMMANDS = {\n    \"pytest\",\n    \"changeset-version\",\n    \"changeset-publish\",\n    \"changeset-plan\",\n    \"publish-action\",\n    \"compute-tag-metadata\",\n    \"update-index-html\",\n    \"sync-skills\",\n}\n\n\ndef _maybe_inject_pytest_subcommand() -> None:\n    \"\"\"Inject 'pytest' subcommand if no known subcommand is present.\n\n    This enables `dev -k test_foo` to work as shorthand for `dev pytest -k test_foo`.\n    \"\"\"\n    if len(sys.argv) < 2:\n        # No args: `dev` -> `dev pytest`\n        sys.argv.insert(1, \"pytest\")\n        return\n\n    # Check if first arg is a known subcommand or --help\n    first_arg = sys.argv[1]\n    if first_arg in _KNOWN_SUBCOMMANDS or first_arg in (\"--help\", \"-h\"):\n        return\n\n    # Not a known subcommand, inject pytest\n    sys.argv.insert(1, \"pytest\")\n\n\n@click.group()\ndef cli() -> None:\n    \"\"\"Developer tooling for the llama-agents repository.\n\n    Run without a subcommand to execute pytest across all packages.\n    \"\"\"\n\n\n@cli.command(\"compute-tag-metadata\")\n@click.option(\n    \"--tag\",\n    required=True,\n    help=\"Full git tag to inspect (e.g. llama-index-workflows@v1.2.3).\",\n)\n@click.option(\"--output\", type=click.Path(), default=None)\ndef compute_tag_metadata(tag: str, output: Path | None) -> None:\n    \"\"\"Compute semantic metadata and change classification for a tag.\n\n    Writes tag_suffix, semver, change_type, and change_description to outputs.\n    \"\"\"\n    try:\n        metadata = versioning.infer_tag_metadata(tag)\n    except ValueError as exc:\n        raise click.BadParameter(str(exc)) from exc\n\n    suffix, semver = versioning.compute_suffix_and_version(tag, metadata.tag_prefix)\n\n    tags = git_utils.list_tags(Path.cwd(), metadata.tag_glob)\n    previous = git_utils.previous_tag(metadata.normalized, tags)\n    previous_version = (\n        versioning.extract_semver(previous, metadata.tag_prefix) if previous else None\n    )\n    change_type = versioning.detect_change_type(semver, previous_version)\n    change_description = \"\"\n\n    click.echo(f\"Current tag: {metadata.normalized}\")\n    if previous:\n        click.echo(f\"Previous tag: {previous}\")\n    else:\n        click.echo(\"No previous tag found\")\n    click.echo(f\"Version: {semver}\")\n    click.echo(f\"Change type: {change_type}\")\n\n    gha.write_outputs(\n        {\n            \"tag_suffix\": suffix,\n            \"semver\": semver,\n            \"change_type\": change_type,\n            \"change_description\": change_description,\n        },\n        output_path=output,\n    )\n\n\n@cli.command(\"update-index-html\")\n@click.option(\"--js-url\", required=True, help=\"URL for the JS bundle.\")\n@click.option(\"--css-url\", required=True, help=\"URL for the CSS bundle.\")\n@click.option(\"--index-path\", default=None, help=\"Path to the index.html file.\")\ndef update_index_html_cmd(js_url: str, css_url: str, index_path: str | None) -> None:\n    \"\"\"Update index.html with new JS and CSS URLs.\"\"\"\n    try:\n        index_html.update_index_html(js_url, css_url, index_path)\n    except (FileNotFoundError, index_html.IndexHtmlError) as exc:\n        raise click.ClickException(str(exc)) from exc\n\n\ncli.add_command(changeset_version)\ncli.add_command(changeset_publish)\ncli.add_command(changeset_plan)\ncli.add_command(publish_action)\ncli.add_command(pytest_cmd)\ncli.add_command(sync_skills)\n"
  },
  {
    "path": "src/dev_cli/commands/__init__.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\nfrom __future__ import annotations\n\nfrom .changesets_cmd import changeset_publish, changeset_version\nfrom .pytest_cmd import pytest_cmd\nfrom .skills_cmd import sync_skills\n\n__all__ = [\n    \"changeset_publish\",\n    \"changeset_version\",\n    \"pytest_cmd\",\n    \"sync_skills\",\n]\n"
  },
  {
    "path": "src/dev_cli/commands/changesets_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Changeset commands for versioning and publishing.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nimport click\n\nfrom .. import changesets\n\n\n@click.command(\"changeset-version\")\ndef changeset_version() -> None:\n    \"\"\"Apply changeset versions, then sync versions for co-located Python packages.\n\n    - Runs changesets to bump package.json versions.\n    - Discovers all workspace packages via pnpm.\n    - For any directory containing both package.json and pyproject.toml, and with\n      package.json private: false, set pyproject [project].version to match the JS version.\n    - If a pyproject is updated, run `uv sync` in the root directory to update the lock file.\n    \"\"\"\n    repo_root = Path(__file__).parents[3]\n    os.chdir(repo_root)\n\n    changesets.run_command([\"npx\", \"@changesets/cli\", \"version\"])\n\n    packages = changesets.get_pnpm_workspace_packages()\n    version_map = {pkg.name: pkg for pkg in packages}\n    any_changed = False\n    for pkg in packages:\n        changed = changesets.apply_sync_values(pkg, version_map)\n        any_changed = any_changed or changed\n        if changed and pkg.postVersion:\n            for cmd in pkg.postVersion:\n                click.echo(f\"Running postVersion script: {cmd}\")\n                changesets.run_command([\"sh\", \"-c\", cmd], cwd=pkg.path)\n\n    if any_changed:\n        click.echo(\"Running uv sync to update lock file...\")\n        changesets.run_command([\"uv\", \"sync\"], cwd=repo_root)\n\n\n@click.command(\"changeset-publish\")\n@click.option(\"--tag\", is_flag=True, help=\"Tag the packages after publishing\")\n@click.option(\"--dry-run\", is_flag=True, help=\"Dry run the publish\")\ndef changeset_publish(tag: bool, dry_run: bool) -> None:\n    \"\"\"Plan and publish all packages locally.\n\n    Builds a publish plan from the current workspace, then runs every\n    action sequentially. Same code path the CI fan-out uses, just with\n    no parallelism.\n    \"\"\"\n    os.chdir(Path(__file__).parents[3])\n\n    plan = changesets.build_publish_plan(changesets.get_pnpm_workspace_packages())\n    for action in plan.all_actions():\n        changesets.execute_action(action, dry_run=dry_run)\n\n    if tag:\n        if dry_run:\n            click.echo(\"Dry run, skipping tag. Would run:\")\n            click.echo(\"  npx @changesets/cli tag\")\n            click.echo(\"  git push --tags\")\n        else:\n            changesets.run_command([\"npx\", \"@changesets/cli\", \"tag\"])\n            changesets.run_command([\"git\", \"push\", \"--tags\"])\n"
  },
  {
    "path": "src/dev_cli/commands/publish_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"CI publish commands.\n\n``changeset-plan`` scans the workspace and emits ``publish-plan.json``;\n``publish-action`` consumes that plan and runs exactly one entry from\nit, selected by its ``action.id``. The CI workflow expands the plan's\nlists into a matrix and calls ``publish-action`` once per shard so\nindependent work — most importantly amd64 and arm64 docker builds —\nruns in parallel on native runners. For local releases, ``dev\nchangeset-publish`` runs the same actions sequentially without going\nthrough this command.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nimport click\n\nfrom .. import changesets, gha\n\n\ndef _repo_root() -> Path:\n    return Path(__file__).parents[3]\n\n\n@click.command(\"changeset-plan\")\n@click.option(\n    \"--output\",\n    type=click.Path(dir_okay=False, path_type=Path),\n    default=Path(\"publish-plan.json\"),\n    show_default=True,\n    help=\"Where to write the publish plan JSON file.\",\n)\ndef changeset_plan(output: Path) -> None:\n    \"\"\"Scan the workspace and emit a publish plan JSON file.\n\n    Writes GitHub Actions step outputs (``pypi``, ``docker_builds``,\n    ``docker_manifests``, ``helm``, ``has_work``) so downstream jobs can\n    expand them into ``strategy.matrix``. An empty plan is valid and\n    expected when there is nothing to publish.\n    \"\"\"\n    os.chdir(_repo_root())\n    plan = changesets.build_publish_plan(changesets.get_pnpm_workspace_packages())\n\n    output.write_text(plan.model_dump_json(indent=2))\n    click.echo(f\"Wrote publish plan to {output}\\n\")\n    click.echo(\"=== Publish plan ===\")\n    click.echo(f\"  pypi:             {len(plan.pypi)}\")\n    for a in plan.pypi:\n        click.echo(f\"    - {a.package}@{a.version}\")\n    click.echo(f\"  docker builds:    {len(plan.docker_builds)}\")\n    for b in plan.docker_builds:\n        click.echo(f\"    - {b.image}:{b.version} ({b.platform})\")\n    click.echo(f\"  docker manifests: {len(plan.docker_manifests)}\")\n    for m in plan.docker_manifests:\n        click.echo(f\"    - {m.image}:{m.version} -> {m.final_tags}\")\n    click.echo(f\"  helm:             {len(plan.helm)}\")\n    for h in plan.helm:\n        click.echo(f\"    - {h.package}@{h.version}\")\n    click.echo(\"====================\")\n\n    gha.write_outputs(plan.model_dump(mode=\"json\"))\n\n\n@click.command(\"publish-action\")\n@click.option(\n    \"--plan\",\n    \"plan_path\",\n    type=click.Path(exists=True, dir_okay=False, path_type=Path),\n    required=True,\n    help=\"Path to publish-plan.json produced by ``changeset-plan``.\",\n)\n@click.option(\n    \"--id\",\n    \"action_id\",\n    required=True,\n    help=\"Action id from the plan, e.g. 'pypi:my-pkg' or 'docker:foo/bar|linux/amd64'.\",\n)\n@click.option(\"--dry-run\", is_flag=True, help=\"Log the action instead of running it.\")\ndef publish_action(plan_path: Path, action_id: str, dry_run: bool) -> None:\n    \"\"\"Execute one entry from a publish plan, selected by its action id.\"\"\"\n    os.chdir(_repo_root())\n    plan = changesets.PublishPlan.model_validate_json(plan_path.read_text())\n    try:\n        action = plan.find(action_id)\n    except KeyError as e:\n        raise click.ClickException(str(e)) from e\n    changesets.execute_action(action, dry_run=dry_run)\n"
  },
  {
    "path": "src/dev_cli/commands/pytest_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Pytest command for running tests across packages.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nimport shutil\nimport signal\nimport subprocess\nimport sys\nimport threading\nimport time\nfrom concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, as_completed, wait\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nimport click\nfrom rich.console import Console\nfrom rich.live import Live\nfrom rich.spinner import Spinner\nfrom rich.table import Table\nfrom rich.text import Text\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:\n    import tomli as tomllib\n\n\n@dataclass\nclass PackageInfo:\n    \"\"\"A package with tests to run.\"\"\"\n\n    path: Path\n    name: str\n\n    @classmethod\n    def from_path(cls, path: Path) -> PackageInfo:\n        \"\"\"Create a PackageInfo from a directory path, reading name from pyproject.toml.\"\"\"\n        pyproject_path = path / \"pyproject.toml\"\n        if pyproject_path.exists():\n            try:\n                data = tomllib.loads(pyproject_path.read_text(encoding=\"utf-8\"))\n                name = data.get(\"project\", {}).get(\"name\", path.name)\n            except Exception:\n                name = path.name\n        else:\n            name = path.name\n        return cls(path=path, name=name)\n\n\n# Regex to strip ANSI escape codes from text\n_ANSI_ESCAPE_RE = re.compile(r\"\\x1b\\[[0-9;]*m\")\n\n\ndef extract_test_counts(stdout: str) -> str | None:\n    \"\"\"Extract test count summary from pytest output.\n\n    Parses the final pytest summary line like \"= 42 passed, 1 failed in 3.2s =\"\n    and returns a short string like \"42 passed\" or \"42 passed, 1 failed\".\n\n    Args:\n        stdout: The complete stdout from a pytest run.\n\n    Returns:\n        A short summary string, or None if no summary line found.\n    \"\"\"\n    if not isinstance(stdout, str):\n        return None\n    for line in reversed(stdout.splitlines()):\n        clean = _ANSI_ESCAPE_RE.sub(\"\", line).strip()\n        match = re.match(r\"^=+\\s+(.+?)\\s+in\\s+[\\d.]+s\\s+=+$\", clean)\n        if match:\n            return match.group(1)\n        # Also match lines without timing, e.g. \"= 42 passed =\"\n        match = re.match(r\"^=+\\s+(.+?)\\s+=+$\", clean)\n        if match:\n            inner = match.group(1)\n            # Avoid matching section headers like \"FAILURES\" or \"test session starts\"\n            if re.search(r\"\\d+\\s+(passed|failed|error|skipped|warning)\", inner):\n                return inner\n    return None\n\n\ndef extract_failures_section(stdout: str) -> str | None:\n    \"\"\"Extract the FAILURES section from pytest output.\n\n    Parses pytest stdout to find content between \"=== FAILURES ===\" and the next\n    section header. Returns just the failure stack traces without warnings or\n    other pytest noise.\n\n    Args:\n        stdout: The complete stdout from a pytest run.\n\n    Returns:\n        The extracted failures section as a string, or None if no failures found.\n    \"\"\"\n    lines = stdout.splitlines()\n    in_failures = False\n    failures_lines: list[str] = []\n\n    for line in lines:\n        if \"FAILURES\" in line and line.strip().startswith(\"=\"):\n            in_failures = True\n            failures_lines.append(line)  # Include the header\n            continue\n        if in_failures:\n            # Stop at next section header (warnings, short test summary, etc.)\n            if line.strip().startswith(\"=\") and \"=\" * 10 in line:\n                break\n            failures_lines.append(line)\n\n    return \"\\n\".join(failures_lines).strip() if failures_lines else None\n\n\ndef extract_failed_test_names(stdout: str) -> list[tuple[str, str | None]]:\n    \"\"\"Extract failed test names and reasons from pytest output.\n\n    Looks for the \"short test summary info\" section and extracts FAILED lines,\n    or falls back to parsing the FAILURES section headers.\n\n    Args:\n        stdout: The complete stdout from a pytest run.\n\n    Returns:\n        List of tuples (test_name, reason) where reason may be None.\n    \"\"\"\n    failed_tests: list[tuple[str, str | None]] = []\n    lines = stdout.splitlines()\n\n    # Try to find short test summary section first\n    in_summary = False\n    for line in lines:\n        if \"short test summary info\" in line:\n            in_summary = True\n            continue\n        if in_summary:\n            # Strip ANSI color codes for pattern matching\n            clean_line = _ANSI_ESCAPE_RE.sub(\"\", line).strip()\n            if clean_line.startswith(\"=\") and \"=\" * 10 in clean_line:\n                break\n            if clean_line.startswith(\"FAILED\"):\n                # Extract test name and reason: \"FAILED tests/foo.py::test_bar - reason\"\n                parts = clean_line.split(\" \", 2)\n                if len(parts) >= 2:\n                    test_and_reason = parts[1]\n                    if \" - \" in test_and_reason:\n                        test_name, reason = test_and_reason.split(\" - \", 1)\n                    elif len(parts) >= 3 and parts[2].startswith(\"- \"):\n                        test_name = parts[1]\n                        reason = parts[2][2:]  # Remove \"- \" prefix\n                    else:\n                        test_name = test_and_reason\n                        reason = None\n                    failed_tests.append((test_name, reason))\n\n    # Fall back to parsing FAILURES section headers if no summary found\n    if not failed_tests:\n        for line in lines:\n            # Look for test name headers like \"_____ test_name _____\"\n            stripped = line.strip()\n            if (\n                stripped.startswith(\"_\")\n                and stripped.endswith(\"_\")\n                and len(stripped) > 20\n            ):\n                # Extract the test name from between the underscores\n                test_name = stripped.strip(\"_\").strip()\n                # Verify it contains actual test name characters, not just\n                # spaces/underscores (which would match separator lines like\n                # \"_ _ _ _ _ _ _ _ _\")\n                if test_name and any(c.isalnum() for c in test_name):\n                    failed_tests.append((test_name, None))\n\n    return failed_tests\n\n\ndef discover_test_packages(repo_root: Path) -> list[PackageInfo]:\n    \"\"\"Discover packages that contain a tests/ directory.\n\n    Returns a sorted list of PackageInfo objects, including the repo root\n    if it has tests. Package names are read from pyproject.toml.\n    \"\"\"\n    packages: list[PackageInfo] = []\n\n    # Check repo root for tests (e.g., tests/dev_cli/)\n    root_tests = repo_root / \"tests\"\n    if root_tests.is_dir() and any(root_tests.iterdir()):\n        packages.append(PackageInfo.from_path(repo_root))\n\n    # Check packages/ directory\n    # Only include directories with both tests/ and pyproject.toml\n    # (empty dirs may linger after package removal because git doesn't track empty dirs)\n    packages_dir = repo_root / \"packages\"\n    if packages_dir.exists():\n        for item in packages_dir.iterdir():\n            has_tests = item.is_dir() and (item / \"tests\").is_dir()\n            has_pyproject = (item / \"pyproject.toml\").is_file()\n            if has_tests and has_pyproject:\n                packages.append(PackageInfo.from_path(item))\n\n    return sorted(packages, key=lambda p: p.name)\n\n\n# Active subprocesses tracked for cleanup on interrupt\n_active_procs: list[subprocess.Popen[str]] = []\n_active_procs_lock = threading.Lock()\n\n\ndef _run_tracked(\n    cmd: list[str], env: dict[str, str]\n) -> subprocess.CompletedProcess[str]:\n    \"\"\"Run a subprocess while tracking it for interrupt cleanup.\"\"\"\n    proc = subprocess.Popen(\n        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env\n    )\n    with _active_procs_lock:\n        _active_procs.append(proc)\n    try:\n        stdout, stderr = proc.communicate()\n    except BaseException:\n        proc.kill()\n        proc.wait()\n        raise\n    finally:\n        with _active_procs_lock:\n            try:\n                _active_procs.remove(proc)\n            except ValueError:\n                pass\n    return subprocess.CompletedProcess(cmd, proc.returncode, stdout, stderr)\n\n\ndef _kill_active_procs() -> None:\n    \"\"\"Kill all tracked subprocesses.\"\"\"\n    with _active_procs_lock:\n        for proc in _active_procs:\n            try:\n                proc.kill()\n            except OSError:\n                pass\n        _active_procs.clear()\n\n\ndef run_package_tests(\n    pkg: PackageInfo, pytest_args: tuple[str, ...]\n) -> dict[str, bool | str | float | None]:\n    \"\"\"Run pytest for a single package and return the results.\n\n    Args:\n        pkg: The PackageInfo to run tests for.\n        pytest_args: Additional arguments to pass to pytest.\n\n    Returns:\n        Dictionary with success status, stdout, stderr, and duration.\n    \"\"\"\n    # Pass through terminal settings\n    env = os.environ.copy()\n    env[\"COLUMNS\"] = str(shutil.get_terminal_size().columns)\n    # Enable colors if we're in an interactive terminal\n    if sys.stdout.isatty():\n        env[\"FORCE_COLOR\"] = \"1\"\n\n    start_time = time.time()\n\n    # Ensure dependencies are installed before running tests (--inexact to only add, not remove)\n    sync_cmd = [\"uv\", \"sync\", \"--directory\", str(pkg.path), \"--inexact\"]\n    sync_result = _run_tracked(sync_cmd, env)\n    if sync_result.returncode != 0:\n        duration = time.time() - start_time\n        return {\n            \"success\": False,\n            \"no_tests\": False,\n            \"stdout\": sync_result.stdout,\n            \"stderr\": f\"uv sync failed:\\n{sync_result.stderr}\",\n            \"duration\": duration,\n        }\n\n    cmd = [\"uv\", \"run\", \"--directory\", str(pkg.path), \"pytest\", *pytest_args]\n    result = _run_tracked(cmd, env)\n    duration = time.time() - start_time\n    # Exit code 0 = success, exit code 5 = no tests collected (not a failure)\n    no_tests = result.returncode == 5\n    success = result.returncode == 0 or no_tests\n    return {\n        \"success\": success,\n        \"no_tests\": no_tests,\n        \"stdout\": result.stdout,\n        \"stderr\": result.stderr,\n        \"duration\": duration,\n        \"test_summary\": extract_test_counts(result.stdout),\n    }\n\n\ndef _render_progress_table(\n    packages: list[PackageInfo],\n    results: dict[str, dict[str, bool | str | float | None]],\n    start_times: dict[str, float],\n    spinners: dict[str, Spinner],\n) -> Table:\n    \"\"\"Render the progress display for rich Live.\"\"\"\n    table = Table(show_header=False, box=None, padding=(0, 1))\n    table.add_column(\"name\", style=\"bold\", no_wrap=True)\n    table.add_column(\"time\", justify=\"right\", style=\"dim\", no_wrap=True)\n\n    for pkg in packages:\n        if pkg.name in results:\n            result_data = results[pkg.name]\n            duration = result_data[\"duration\"]\n            test_summary = result_data.get(\"test_summary\")\n            table.add_row(pkg.name, f\"{duration:.1f}s\")\n            if result_data.get(\"no_tests\"):\n                table.add_row(Text(\"  no tests collected\", style=\"yellow\"), \"\")\n            elif test_summary:\n                style = \"green\" if result_data[\"success\"] else \"red\"\n                table.add_row(Text(f\"  {test_summary}\", style=style), \"\")\n            elif result_data[\"success\"]:\n                table.add_row(Text(\"  passed\", style=\"green\"), \"\")\n            else:\n                table.add_row(Text(\"  failed\", style=\"red\"), \"\")\n        elif pkg.name in start_times:\n            elapsed = time.time() - start_times[pkg.name]\n            table.add_row(pkg.name, f\"{elapsed:.1f}s\")\n            spinner = spinners.get(pkg.name, Spinner(\"dots\", style=\"yellow\"))\n            table.add_row(spinner, \"\")\n        else:\n            table.add_row(Text(pkg.name, style=\"dim\"), \"\")\n    return table\n\n\ndef run_tests_with_rich_progress(\n    packages: list[PackageInfo],\n    pytest_args: tuple[str, ...],\n    max_workers: int,\n) -> dict[str, dict[str, bool | str | float | None]]:\n    \"\"\"Run tests with a live rich progress display.\n\n    Shows a live-updating table with package status, spinner for running\n    packages, and elapsed time.\n\n    Args:\n        packages: List of PackageInfo objects to test.\n        pytest_args: Additional arguments to pass to pytest.\n        max_workers: Maximum number of parallel workers.\n\n    Returns:\n        Dictionary mapping package names to their test results.\n    \"\"\"\n    console = Console()\n    results: dict[str, dict[str, bool | str | float | None]] = {}\n    start_times: dict[str, float] = {}\n    spinners: dict[str, Spinner] = {}\n\n    def render() -> Table:\n        return _render_progress_table(packages, results, start_times, spinners)\n\n    with ThreadPoolExecutor(max_workers=max_workers) as executor:\n        # Submit all and track start times\n        futures = {}\n        for pkg in packages:\n            start_times[pkg.name] = time.time()\n            spinners[pkg.name] = Spinner(\"dots\", style=\"yellow\")\n            future = executor.submit(run_package_tests, pkg, pytest_args)\n            futures[future] = pkg\n\n        pending = set(futures.keys())\n\n        with Live(render(), console=console, refresh_per_second=10) as live:\n            while pending:\n                # Wait with short timeout to allow display updates\n                done, pending = wait(pending, timeout=0.1, return_when=FIRST_COMPLETED)\n\n                for future in done:\n                    pkg = futures[future]\n                    result_data = future.result()\n                    results[pkg.name] = result_data\n                    # Remove spinner for completed package\n                    spinners.pop(pkg.name, None)\n\n                live.update(render())\n\n    return results\n\n\ndef _run_tests_verbose(\n    target_packages: list[PackageInfo],\n    pytest_args: tuple[str, ...],\n    max_workers: int,\n    total: int,\n    verbose: bool,\n) -> dict[str, dict[str, bool | str | float | None]]:\n    \"\"\"Run tests with simple sequential output (non-TTY or verbose mode).\"\"\"\n    results: dict[str, dict[str, bool | str | float | None]] = {}\n    with ThreadPoolExecutor(max_workers=max_workers) as executor:\n        futures = {\n            executor.submit(run_package_tests, pkg, pytest_args): pkg\n            for pkg in target_packages\n        }\n        for idx, future in enumerate(as_completed(futures), 1):\n            pkg = futures[future]\n            result_data = future.result()\n            results[pkg.name] = result_data\n\n            if verbose:\n                click.echo(f\"\\n{'=' * 60}\")\n                click.echo(f\"Completed tests in {pkg.name}\")\n                click.echo(\"=\" * 60)\n                if result_data[\"stdout\"]:\n                    click.echo(result_data[\"stdout\"], nl=False)\n                if result_data[\"stderr\"]:\n                    click.echo(result_data[\"stderr\"], nl=False, err=True)\n            else:\n                # Compact progress line (for non-TTY output)\n                test_summary = result_data.get(\"test_summary\")\n                summary_suffix = f\" ({test_summary})\" if test_summary else \"\"\n                if result_data.get(\"no_tests\"):\n                    status = click.style(\"NO TESTS\", fg=\"yellow\")\n                elif result_data[\"success\"]:\n                    status = click.style(f\"PASSED{summary_suffix}\", fg=\"green\")\n                else:\n                    status = click.style(f\"FAILED{summary_suffix}\", fg=\"red\")\n                click.echo(\n                    f\"[{idx}/{total}] {pkg.name}... {status} \"\n                    f\"({result_data['duration']:.1f}s)\"\n                )\n    return results\n\n\n@click.command(\n    \"pytest\",\n    context_settings={\n        \"ignore_unknown_options\": True,\n        \"allow_interspersed_args\": False,\n    },\n)\n@click.option(\n    \"--verbose\",\n    \"-v\",\n    is_flag=True,\n    help=\"Show full pytest output for each package instead of compact progress.\",\n)\n@click.option(\n    \"--package\",\n    \"-p\",\n    \"packages\",\n    multiple=True,\n    help=\"Filter packages by substring match. Can be used multiple times.\",\n)\n@click.option(\n    \"--parallel\",\n    \"-j\",\n    default=10,\n    type=int,\n    help=\"Number of packages to test in parallel. Default 10, use 1 for sequential.\",\n)\n@click.argument(\"pytest_args\", nargs=-1, type=click.UNPROCESSED)\ndef pytest_cmd(\n    verbose: bool,\n    packages: tuple[str, ...],\n    parallel: int,\n    pytest_args: tuple[str, ...],\n) -> None:\n    \"\"\"Run pytest across all packages in the repository.\n\n    Any additional arguments after -- are passed through to pytest.\n\n    Examples:\n        dev pytest                  # Run all tests\n        dev pytest -p workflows     # Packages matching \"workflows\"\n        dev pytest -p server client # Multiple filters\n        dev pytest -- -k test_name  # Pass args to pytest\n    \"\"\"\n    repo_root = Path(__file__).parents[3]\n    all_packages = discover_test_packages(repo_root)\n\n    if not all_packages:\n        click.echo(\"No packages with tests/ directory found.\")\n        sys.exit(0)\n\n    # Filter to specified packages if provided\n    # Per filter: exact match takes precedence, otherwise substring match\n    if packages:\n        available_names = {p.name for p in all_packages}\n        matched: set[str] = set()\n        for filt in packages:\n            if filt in available_names:\n                # Exact match - only add this one\n                matched.add(filt)\n            else:\n                # Substring match\n                matched.update(p.name for p in all_packages if filt in p.name)\n        target_packages = [p for p in all_packages if p.name in matched]\n        if not target_packages:\n            click.echo(f\"No packages matched: {', '.join(packages)}\", err=True)\n            click.echo(f\"Available: {', '.join(sorted(available_names))}\")\n            sys.exit(1)\n    else:\n        target_packages = all_packages\n\n    # Run tests in each package\n    total = len(target_packages)\n    max_workers = min(parallel, len(target_packages))\n    use_rich_progress = sys.stdout.isatty() and not verbose\n\n    # Single package: always show full output directly\n    if total == 1:\n        verbose = True\n        use_rich_progress = False\n\n    # Install a SIGINT handler that exits immediately. The default Python handler\n    # raises KeyboardInterrupt, but ThreadPoolExecutor's shutdown(wait=True) blocks\n    # the main thread in thread joins, preventing the interrupt from being handled\n    # until all subprocess children finish.\n    original_sigint = signal.getsignal(signal.SIGINT)\n\n    def _handle_sigint(signum: int, frame: object) -> None:\n        _kill_active_procs()\n        click.echo(\"\\nInterrupted.\")\n        # Restore default handler so a second Ctrl+C kills immediately\n        signal.signal(signal.SIGINT, signal.SIG_DFL)\n        sys.exit(130)\n\n    signal.signal(signal.SIGINT, _handle_sigint)\n\n    try:\n        if use_rich_progress:\n            results = run_tests_with_rich_progress(\n                target_packages, pytest_args, max_workers\n            )\n        else:\n            results = _run_tests_verbose(\n                target_packages, pytest_args, max_workers, total, verbose\n            )\n    finally:\n        signal.signal(signal.SIGINT, original_sigint)\n\n    # Print summary\n    passed = sum(1 for v in results.values() if v[\"success\"] and not v.get(\"no_tests\"))\n    no_tests = sum(1 for v in results.values() if v.get(\"no_tests\"))\n    failed = len(results) - passed - no_tests\n\n    # Print failures section first (after progress), before summary\n    if failed:\n        click.echo(f\"\\n{'=' * 20} FAILURES {'=' * 20}\")\n        for name, result_data in results.items():\n            if not result_data[\"success\"]:\n                click.echo(f\"\\n[{name}]\")\n                # Extract just the failures section, fall back to full output\n                stdout = str(result_data[\"stdout\"]) if result_data[\"stdout\"] else \"\"\n                failures = extract_failures_section(stdout)\n                if failures:\n                    click.echo(failures)\n                elif stdout:\n                    # Fall back to full output if no FAILURES section found\n                    click.echo(stdout, nl=False)\n                if result_data[\"stderr\"]:\n                    click.echo(result_data[\"stderr\"], nl=False, err=True)\n\n    # Count total individual tests across all packages\n    total_tests = 0\n    for result_data in results.values():\n        ts = result_data.get(\"test_summary\")\n        if ts and isinstance(ts, str):\n            total_tests += sum(\n                int(n)\n                for n in re.findall(r\"(\\d+)\\s+(?:passed|failed|error|skipped)\", ts)\n            )\n\n    if failed or no_tests:\n        # Print summary table only when there are failures or missing tests\n        click.echo(f\"\\n{'=' * 50}\")\n        click.echo(\"Test Summary\")\n        click.echo(\"=\" * 50)\n\n        max_name_len = max(len(name) for name in results)\n        for name, result_data in results.items():\n            test_summary = result_data.get(\"test_summary\")\n            summary_suffix = f\" ({test_summary})\" if test_summary else \"\"\n            if result_data.get(\"no_tests\"):\n                status = click.style(\"NO TESTS\", fg=\"yellow\")\n            elif result_data[\"success\"]:\n                status = click.style(f\"PASSED{summary_suffix}\", fg=\"green\")\n            else:\n                status = click.style(f\"FAILED{summary_suffix}\", fg=\"red\")\n            click.echo(f\"{name.ljust(max_name_len)}  {status}\")\n\n        click.echo(\"=\" * 50)\n        pkg_parts = []\n        if failed:\n            pkg_parts.append(click.style(f\"{failed} failed\", fg=\"red\"))\n        if passed:\n            pkg_parts.append(click.style(f\"{passed} passed\", fg=\"green\"))\n        if no_tests:\n            pkg_parts.append(click.style(f\"{no_tests} no tests\", fg=\"yellow\"))\n        pkg_label = \"package\" if (passed + failed + no_tests) == 1 else \"packages\"\n        summary_line = f\"{', '.join(pkg_parts)} {pkg_label}\"\n        if total_tests:\n            summary_line += f\", {total_tests} tests total\"\n        click.echo(summary_line)\n    else:\n        # All passed — just a short totals line\n        pkg_label = \"package\" if passed == 1 else \"packages\"\n        total_str = f\", {total_tests} tests\" if total_tests else \"\"\n        click.echo(click.style(f\"\\n{passed} {pkg_label} passed{total_str}\", fg=\"green\"))\n\n    # Show failed test names at the very end for quick reference\n    if failed:\n        click.echo(\"\\nFailed tests:\")\n        for name, result_data in results.items():\n            if not result_data[\"success\"]:\n                stdout = str(result_data[\"stdout\"]) if result_data[\"stdout\"] else \"\"\n                test_info = extract_failed_test_names(stdout)\n                for test_name, reason in test_info:\n                    reason_str = f\" - {reason}\" if reason else \"\"\n                    click.echo(\n                        f\"  {click.style('FAILED', fg='red')} [{name}] \"\n                        f\"{test_name}{reason_str}\"\n                    )\n        sys.exit(1)\n"
  },
  {
    "path": "src/dev_cli/commands/skills_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Sync repo-local agent skills into Claude Code's gitignored skills dir.\n\nSkills are checked in under `.agents/skills/<name>/` (the Codex-compatible\nlocation, see https://developers.openai.com/codex/skills). Claude Code reads\nfrom `.claude/skills/<name>/`, which is gitignored. This command symlinks the\ntwo so editing the checked-in copy is what Claude Code sees.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nimport click\n\n\ndef _repo_root() -> Path:\n    # cli is invoked from anywhere in the repo; resolve relative to this file.\n    return Path(__file__).resolve().parents[3]\n\n\n@click.command(\"sync-skills\")\n@click.option(\n    \"--check\",\n    is_flag=True,\n    help=\"Exit non-zero if any link is missing or stale, without modifying anything.\",\n)\ndef sync_skills(check: bool) -> None:\n    \"\"\"Symlink `.agents/skills/<name>` into `.claude/skills/<name>`.\n\n    Idempotent. Refuses to overwrite an existing non-symlink entry.\n    \"\"\"\n    root = _repo_root()\n    src_dir = root / \".agents\" / \"skills\"\n    dst_dir = root / \".claude\" / \"skills\"\n\n    if not src_dir.is_dir():\n        click.echo(\n            f\"no skills source dir at {src_dir.relative_to(root)}, nothing to do\"\n        )\n        return\n\n    drift: list[str] = []\n    created: list[str] = []\n    ok: list[str] = []\n\n    if check:\n        if not dst_dir.is_dir():\n            click.echo(\n                f\"drift    missing destination dir {dst_dir.relative_to(root)}\",\n                err=True,\n            )\n            raise SystemExit(1)\n    else:\n        dst_dir.mkdir(parents=True, exist_ok=True)\n\n    for entry in sorted(src_dir.iterdir()):\n        if not entry.is_dir():\n            continue\n        name = entry.name\n        link = dst_dir / name\n        # Symlink target is relative to the link's parent dir, so the link\n        # survives moving the repo around.\n        target = Path(os.path.relpath(entry, dst_dir))\n\n        if link.is_symlink():\n            current = Path(os.readlink(link))\n            if current == target:\n                ok.append(name)\n                continue\n            if check:\n                drift.append(f\"{name}: stale link -> {current}, want {target}\")\n                continue\n            link.unlink()\n            link.symlink_to(target)\n            created.append(f\"{name} (relinked)\")\n            continue\n\n        if link.exists():\n            drift.append(\n                f\"{name}: {link.relative_to(root)} exists and is not a symlink, \"\n                f\"refusing to overwrite\"\n            )\n            continue\n\n        if check:\n            drift.append(f\"{name}: missing link\")\n            continue\n\n        link.symlink_to(target)\n        created.append(name)\n\n    for name in ok:\n        click.echo(f\"ok       {name}\")\n    for entry in created:\n        click.echo(f\"linked   {entry}\")\n    for entry in drift:\n        click.echo(f\"drift    {entry}\", err=True)\n\n    if drift:\n        raise SystemExit(1)\n"
  },
  {
    "path": "src/dev_cli/gha.py",
    "content": "from __future__ import annotations\n\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Any, Mapping\n\nimport click\n\n\ndef _encode(value: Any) -> str:\n    \"\"\"Encode an output value for GHA. Strings pass through; everything\n    else is JSON-encoded so lists/bools/dicts round-trip cleanly through\n    ``fromJSON()`` in workflow expressions.\"\"\"\n    if isinstance(value, str):\n        return value\n    return json.dumps(value)\n\n\ndef write_outputs(\n    outputs: Mapping[str, Any], output_path: str | Path | None = None\n) -> None:\n    \"\"\"Write GitHub Actions step outputs.\"\"\"\n    target = output_path or os.environ.get(\"GITHUB_OUTPUT\")\n    encoded = {key: _encode(value) for key, value in outputs.items()}\n    if target:\n        with open(target, \"a\", encoding=\"utf-8\") as handle:\n            for key, value in encoded.items():\n                handle.write(f\"{key}={value}\\n\")\n    else:\n        for key, value in encoded.items():\n            click.echo(f\"{key}={value}\")\n"
  },
  {
    "path": "src/dev_cli/git_utils.py",
    "content": "from __future__ import annotations\n\nimport subprocess\nfrom pathlib import Path\nfrom typing import Iterable\n\n\ndef list_tags(repo: str | Path, tag_glob: str) -> list[str]:\n    \"\"\"Return tags that match the provided glob sorted newest first.\"\"\"\n    repo_path = Path(repo)\n    result = subprocess.run(\n        [\n            \"git\",\n            \"-C\",\n            str(repo_path),\n            \"tag\",\n            \"-l\",\n            tag_glob,\n            \"--sort=-version:refname\",\n        ],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n\n    return [line.strip() for line in result.stdout.splitlines() if line.strip()]\n\n\ndef previous_tag(current_tag: str, tags: Iterable[str]) -> str | None:\n    \"\"\"Return the tag immediately after the current entry in the sorted list.\"\"\"\n    tags_list = list(tags)\n    if current_tag in tags_list:\n        idx = tags_list.index(current_tag)\n        if idx + 1 < len(tags_list):\n            return tags_list[idx + 1]\n        return None\n    return tags_list[0] if tags_list else None\n\n\ndef find_previous_tag(repo: str | Path, tag_prefix: str, current_tag: str) -> str:\n    \"\"\"Locate the latest tag that matches a prefix but differs from the current tag.\"\"\"\n    matches = list_tags(repo, f\"{tag_prefix}*\")\n    for candidate in matches:\n        if candidate != current_tag:\n            return candidate\n    return \"\"\n"
  },
  {
    "path": "src/dev_cli/index_html.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom pathlib import Path\n\n\nclass IndexHtmlError(RuntimeError):\n    \"\"\"Raised when index.html cannot be updated.\"\"\"\n\n\nSCRIPT_PATTERN = re.compile(\n    r'<script\\s+type=\"module\"\\s+crossorigin\\s+src=\"[^\"]*\"[^>]*></script>'\n)\nCSS_PATTERN = re.compile(r'<link\\s+rel=\"stylesheet\"\\s+crossorigin\\s+href=\"[^\"]*\"[^>]*>')\n\n\ndef default_index_path() -> Path:\n    \"\"\"Return the default path to the debugger index.html file.\"\"\"\n    return (\n        Path(__file__).resolve().parents[2]\n        / \"packages\"\n        / \"llama-agents-server\"\n        / \"src\"\n        / \"llama_agents\"\n        / \"server\"\n        / \"static\"\n        / \"index.html\"\n    )\n\n\ndef update_index_html(\n    js_url: str, css_url: str, index_path: str | Path | None = None\n) -> None:\n    \"\"\"Replace the debugger asset URLs in index.html.\"\"\"\n    target = Path(index_path) if index_path is not None else default_index_path()\n    if not target.exists():\n        raise FileNotFoundError(f\"index.html not found at {target}\")\n\n    content = target.read_text(encoding=\"utf-8\")\n\n    new_script = f'<script type=\"module\" crossorigin src=\"{js_url}\"></script>'\n    updated_content, script_count = SCRIPT_PATTERN.subn(new_script, content)\n    if script_count == 0:\n        raise IndexHtmlError(\"Could not find script tag in index.html\")\n\n    new_css = f'<link rel=\"stylesheet\" crossorigin href=\"{css_url}\">'\n    updated_content, css_count = CSS_PATTERN.subn(new_css, updated_content)\n    if css_count == 0:\n        raise IndexHtmlError(\"Could not find link tag in index.html\")\n\n    target.write_text(updated_content, encoding=\"utf-8\")\n"
  },
  {
    "path": "src/dev_cli/versioning.py",
    "content": "from __future__ import annotations\n\nimport sys\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nfrom packaging.version import Version\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse:\n    import tomli as tomllib\n\n\nclass VersionMismatchError(ValueError):\n    \"\"\"Raised when two versions do not match.\"\"\"\n\n\ndef read_pyproject_version(pyproject_path: str) -> str:\n    \"\"\"Read the project version from a pyproject.toml file.\"\"\"\n    path = Path(pyproject_path)\n    data = tomllib.loads(path.read_text(encoding=\"utf-8\"))\n    try:\n        project = data[\"project\"]\n    except KeyError as exc:  # pragma: no cover - invalid pyproject structure\n        raise ValueError(\"Missing [project] section in pyproject.toml\") from exc\n\n    try:\n        version = project[\"version\"]\n    except KeyError as exc:\n        raise ValueError(\"Missing version metadata in pyproject.toml\") from exc\n\n    if not isinstance(version, str):  # pragma: no cover - defensive guard\n        raise ValueError(\"Project version must be a string.\")\n    return version\n\n\ndef strip_refs_prefix(tag: str) -> str:\n    \"\"\"Remove the refs/tags/ prefix when present.\"\"\"\n    return tag.replace(\"refs/tags/\", \"\") if tag.startswith(\"refs/tags/\") else tag\n\n\n@dataclass\nclass TagMetadata:\n    \"\"\"Normalized tag metadata for a namespaced package release.\"\"\"\n\n    normalized: str\n    tag_prefix: str\n    tag_glob: str\n\n\ndef infer_tag_metadata(tag: str) -> TagMetadata:\n    \"\"\"Return normalized tag, prefix, and glob for tags of the same package.\n\n    Supported format: \"<namespace>@v<semver>\" (e.g. \"llama-index-workflows@v1.2.3\").\n    \"\"\"\n    normalized = strip_refs_prefix(tag)\n\n    if \"@\" not in normalized:\n        raise ValueError(\n            f\"Invalid tag '{tag}'. Expected format '<namespace>@v<semver>'.\"\n        )\n\n    package, suffix = normalized.split(\"@\", 1)\n    if not suffix.startswith(\"v\"):\n        raise ValueError(\n            f\"Invalid tag '{tag}'. Expected format '<namespace>@v<semver>'.\"\n        )\n\n    tag_prefix = f\"{package}@\"\n    tag_glob = f\"{package}@v*\"\n\n    return TagMetadata(normalized=normalized, tag_prefix=tag_prefix, tag_glob=tag_glob)\n\n\ndef remove_tag_prefix(tag: str, tag_prefix: str) -> str:\n    \"\"\"Remove a package-specific prefix and optional leading v.\"\"\"\n    if tag_prefix:\n        if not tag.startswith(tag_prefix):\n            raise ValueError(f\"Tag {tag} does not match expected prefix {tag_prefix}\")\n        tag = tag[len(tag_prefix) :]\n    return tag\n\n\ndef extract_semver(tag: str, tag_prefix: str) -> str:\n    \"\"\"Return the semantic version encoded in a git tag.\"\"\"\n    suffix = remove_tag_prefix(strip_refs_prefix(tag), tag_prefix)\n    return suffix[1:] if suffix.startswith(\"v\") else suffix\n\n\ndef compute_suffix_and_version(tag: str, tag_prefix: str) -> tuple[str, str]:\n    \"\"\"Return the suffix after the prefix and the semantic version.\"\"\"\n    suffix = remove_tag_prefix(strip_refs_prefix(tag), tag_prefix)\n    semver = suffix[1:] if suffix.startswith(\"v\") else suffix\n    return suffix, semver\n\n\ndef detect_change_type(current_version: str, previous_version: str | None) -> str:\n    \"\"\"Return the semantic change classification between two versions.\"\"\"\n    if not previous_version:\n        return \"major\"\n\n    current = Version(current_version)\n    previous = Version(previous_version)\n\n    if current <= previous:\n        return \"none\"\n\n    current_release = (current.release + (0, 0, 0))[:3]\n    previous_release = (previous.release + (0, 0, 0))[:3]\n\n    if current_release[0] > previous_release[0]:\n        return \"major\"\n    if current_release[1] > previous_release[1]:\n        return \"minor\"\n    if current_release[2] > previous_release[2]:\n        return \"patch\"\n    return \"minor\"\n"
  },
  {
    "path": "tests/dev_cli/conftest.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport subprocess\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\nfrom unittest.mock import Mock\n\nimport pytest\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n\n@pytest.fixture\ndef packages_dir(tmp_path: Path) -> Path:\n    \"\"\"Create a packages directory structure.\"\"\"\n    packages = tmp_path / \"packages\"\n    packages.mkdir()\n    return packages\n\n\n@pytest.fixture\ndef create_test_package(packages_dir: Path) -> Callable[[str], Path]:\n    \"\"\"Factory fixture to create packages with tests subdirectory.\"\"\"\n\n    def _create(name: str) -> Path:\n        pkg = packages_dir / name\n        pkg.mkdir()\n        (pkg / \"tests\").mkdir()\n        return pkg\n\n    return _create\n\n\n@pytest.fixture\ndef git_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:\n    \"\"\"Initialize a git repository and change into it.\"\"\"\n    subprocess.run([\"git\", \"init\"], cwd=tmp_path, check=True, capture_output=True)\n    subprocess.run(\n        [\"git\", \"config\", \"user.email\", \"dev@example.com\"], cwd=tmp_path, check=True\n    )\n    subprocess.run([\"git\", \"config\", \"user.name\", \"Dev User\"], cwd=tmp_path, check=True)\n    monkeypatch.chdir(tmp_path)\n    return tmp_path\n\n\ndef commit_and_tag(repo_path: Path, filename: str, content: str, tag: str) -> None:\n    \"\"\"Create a file, commit it, and tag it.\"\"\"\n    file_path = repo_path / filename\n    file_path.write_text(content)\n    subprocess.run([\"git\", \"add\", filename], cwd=repo_path, check=True)\n    subprocess.run(\n        [\"git\", \"commit\", \"-m\", f\"Add {tag}\"],\n        cwd=repo_path,\n        check=True,\n        capture_output=True,\n    )\n    subprocess.run([\"git\", \"tag\", tag], cwd=repo_path, check=True)\n\n\ndef is_sync_call(cmd: list[str]) -> bool:\n    \"\"\"Check if a subprocess command is a 'uv sync' call vs 'uv run pytest'.\"\"\"\n    return len(cmd) >= 2 and cmd[0] == \"uv\" and cmd[1] == \"sync\"\n\n\ndef sync_success() -> Mock:\n    \"\"\"Return a successful sync result.\"\"\"\n    return Mock(returncode=0, stdout=\"\", stderr=\"\")\n\n\ndef write_pyproject(path: Path, version: str) -> None:\n    \"\"\"Write a minimal pyproject.toml with the given version.\"\"\"\n    path.write_text(\n        f\"\"\"\n[project]\nname = \"example\"\nversion = \"{version}\"\n\"\"\".strip()\n    )\n"
  },
  {
    "path": "tests/dev_cli/test_changesets.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\nfrom __future__ import annotations\n\nimport json\nfrom pathlib import Path\nfrom unittest.mock import Mock, patch\nfrom urllib.error import HTTPError\n\nimport pytest\n\nfrom dev_cli.changesets import (\n    DockerBuildAction,\n    DockerConfig,\n    DockerManifestAction,\n    HelmAction,\n    HelmConfig,\n    PackageJson,\n    PypiAction,\n    PyProjectContainer,\n    _resolve_template,\n    _write_toml_values,\n    _write_yaml_values,\n    apply_sync_values,\n    current_version,\n    docker_image_tags,\n    execute_action,\n    is_docker_image_published,\n    is_helm_chart_published,\n    is_published,\n    is_rc_version,\n    pep440_to_semver,\n    plan_docker,\n    plan_helm,\n    semver_to_pep440,\n)\n\n\ndef test_current_version(tmp_path: Path) -> None:\n    pyproject = tmp_path / \"pyproject.toml\"\n    pyproject.write_text(\n        \"\"\"\n[project]\nname = \"test-package\"\nversion = \"1.2.3\"\ndependencies = []\n\"\"\".strip()\n    )\n    name, version = current_version(pyproject)\n    assert name == \"test-package\"\n    assert version == \"1.2.3\"\n\n\ndef test_current_version_normalizes_version(tmp_path: Path) -> None:\n    pyproject = tmp_path / \"pyproject.toml\"\n    pyproject.write_text(\n        \"\"\"\n[project]\nname = \"test-package\"\nversion = \"01.02.03\"\ndependencies = []\n\"\"\".strip()\n    )\n    name, version = current_version(pyproject)\n    assert name == \"test-package\"\n    assert version == \"1.2.3\"\n\n\ndef test_pyproject_container_parse() -> None:\n    toml_text = \"\"\"\n[project]\nname = \"my-package\"\nversion = \"0.1.0\"\ndependencies = [\"requests>=2.0.0\"]\n\"\"\".strip()\n    toml_doc, py_doc = PyProjectContainer.parse(toml_text)\n    assert py_doc.project.name == \"my-package\"\n    assert py_doc.project.version == \"0.1.0\"\n    assert py_doc.project.dependencies == [\"requests>=2.0.0\"]\n\n\ndef test_is_published_returns_true_when_version_exists() -> None:\n    mock_response = Mock()\n    mock_response.read.return_value = json.dumps(\n        {\"releases\": {\"1.0.0\": [], \"1.1.0\": []}}\n    ).encode()\n\n    with patch(\"urllib.request.urlopen\", return_value=mock_response):\n        result = is_published(\"test-package\", \"1.0.0\")\n        assert result is True\n\n\ndef test_is_published_returns_false_when_version_missing() -> None:\n    mock_response = Mock()\n    mock_response.read.return_value = json.dumps({\"releases\": {\"1.0.0\": []}}).encode()\n\n    with patch(\"urllib.request.urlopen\", return_value=mock_response):\n        result = is_published(\"test-package\", \"1.1.0\")\n        assert result is False\n\n\ndef test_is_published_returns_false_when_package_not_found() -> None:\n    mock_error = HTTPError(\"url\", 404, \"Not Found\", {}, None)  # type: ignore[arg-type]\n\n    with patch(\"urllib.request.urlopen\", side_effect=mock_error):\n        result = is_published(\"nonexistent-package\", \"1.0.0\")\n        assert result is False\n\n\ndef test_is_published_raises_on_other_http_errors() -> None:\n    mock_error = HTTPError(\"url\", 500, \"Internal Server Error\", {}, None)  # type: ignore[arg-type]\n\n    with patch(\"urllib.request.urlopen\", side_effect=mock_error):\n        with pytest.raises(HTTPError) as exc_info:\n            is_published(\"test-package\", \"1.0.0\")\n        assert exc_info.value.code == 500\n\n\ndef test_sync_package_version_updates_version(tmp_path: Path) -> None:\n    package_dir = tmp_path / \"package\"\n    package_dir.mkdir()\n    pyproject = package_dir / \"pyproject.toml\"\n    pyproject.write_text(\n        \"\"\"\n[project]\nname = \"test-package\"\nversion = \"1.0.0\"\ndependencies = []\n\"\"\".strip()\n    )\n\n    pkg = PackageJson(\n        name=\"test-js-package\",\n        version=\"2.0.0\",\n        path=package_dir,\n        private=False,\n    )\n\n    apply_sync_values(pkg, {\"test-js-package\": pkg})\n\n    _, py_doc = PyProjectContainer.parse(pyproject.read_text())\n    assert py_doc.project.version == \"2.0.0\"\n\n\ndef test_sync_package_version_skips_when_no_pyproject(tmp_path: Path) -> None:\n    package_dir = tmp_path / \"package\"\n    package_dir.mkdir()\n\n    pkg = PackageJson(\n        name=\"test-js-package\",\n        version=\"2.0.0\",\n        path=package_dir,\n        private=False,\n    )\n\n    changed = apply_sync_values(pkg, {\"test-js-package\": pkg})\n    assert changed is False\n\n\ndef test_sync_package_version_skips_when_versions_match(tmp_path: Path) -> None:\n    package_dir = tmp_path / \"package\"\n    package_dir.mkdir()\n    pyproject = package_dir / \"pyproject.toml\"\n    original_content = \"\"\"\n[project]\nname = \"test-package\"\nversion = \"2.0.0\"\ndependencies = []\n\"\"\".strip()\n    pyproject.write_text(original_content)\n\n    pkg = PackageJson(\n        name=\"test-js-package\",\n        version=\"2.0.0\",\n        path=package_dir,\n        private=False,\n    )\n\n    apply_sync_values(pkg, {\"test-js-package\": pkg})\n\n    # Content should be unchanged\n    assert pyproject.read_text() == original_content\n\n\ndef test_sync_package_version_converts_semver_prerelease(tmp_path: Path) -> None:\n    package_dir = tmp_path / \"package\"\n    package_dir.mkdir()\n    pyproject = package_dir / \"pyproject.toml\"\n    pyproject.write_text(\n        \"\"\"\n[project]\nname = \"test-package\"\nversion = \"1.0.0\"\ndependencies = []\n\"\"\".strip()\n    )\n\n    pkg = PackageJson(\n        name=\"test-js-package\",\n        version=\"1.2.3-a.4\",\n        path=package_dir,\n        private=False,\n    )\n\n    apply_sync_values(pkg, {\"test-js-package\": pkg})\n\n    _, py_doc = PyProjectContainer.parse(pyproject.read_text())\n    assert py_doc.project.version == \"1.2.3a4\"\n\n\n# -- semver_to_pep440 tests --\n\n\n@pytest.mark.parametrize(\n    (\"semver\", \"expected\"),\n    [\n        (\"1.2.3-a.0\", \"1.2.3a0\"),\n        (\"1.2.3-a.4\", \"1.2.3a4\"),\n        (\"2.0.0-b.1\", \"2.0.0b1\"),\n        (\"0.1.0-rc.3\", \"0.1.0rc3\"),\n        (\"10.20.30-a.99\", \"10.20.30a99\"),\n    ],\n)\ndef test_semver_to_pep440_prerelease(semver: str, expected: str) -> None:\n    assert semver_to_pep440(semver) == expected\n\n\n@pytest.mark.parametrize(\n    \"version\",\n    [\n        \"1.2.3\",\n        \"0.0.1\",\n        \"10.20.30\",\n    ],\n)\ndef test_semver_to_pep440_stable_passthrough(version: str) -> None:\n    assert semver_to_pep440(version) == version\n\n\ndef test_semver_to_pep440_rejects_non_pep440_label() -> None:\n    with pytest.raises(ValueError, match=\"Unsupported pre-release label 'alpha'\"):\n        semver_to_pep440(\"1.2.3-alpha.1\")\n\n\n# -- pep440_to_semver tests --\n\n\n@pytest.mark.parametrize(\n    (\"pep440\", \"expected\"),\n    [\n        (\"1.2.3a0\", \"1.2.3-a.0\"),\n        (\"1.2.3a4\", \"1.2.3-a.4\"),\n        (\"2.0.0b1\", \"2.0.0-b.1\"),\n        (\"0.1.0rc3\", \"0.1.0-rc.3\"),\n        (\"10.20.30a99\", \"10.20.30-a.99\"),\n    ],\n)\ndef test_pep440_to_semver_prerelease(pep440: str, expected: str) -> None:\n    assert pep440_to_semver(pep440) == expected\n\n\n@pytest.mark.parametrize(\n    \"version\",\n    [\n        \"1.2.3\",\n        \"0.0.1\",\n        \"10.20.30\",\n    ],\n)\ndef test_pep440_to_semver_stable_passthrough(version: str) -> None:\n    assert pep440_to_semver(version) == version\n\n\n# -- roundtrip tests --\n\n\n@pytest.mark.parametrize(\n    \"semver\",\n    [\n        \"1.2.3-a.4\",\n        \"2.0.0-b.1\",\n        \"0.1.0-rc.3\",\n    ],\n)\ndef test_roundtrip_semver_to_pep440_and_back(semver: str) -> None:\n    pep440 = semver_to_pep440(semver)\n    assert pep440_to_semver(pep440) == semver\n\n\n@pytest.mark.parametrize(\n    \"pep440\",\n    [\n        \"1.2.3a4\",\n        \"2.0.0b1\",\n        \"0.1.0rc3\",\n    ],\n)\ndef test_roundtrip_pep440_to_semver_and_back(pep440: str) -> None:\n    semver = pep440_to_semver(pep440)\n    assert semver_to_pep440(semver) == pep440\n\n\n# -- chart version sync tests --\n\n\ndef _make_helm_package(\n    path: Path, version: str = \"0.4.14\", name: str = \"llama-agents\"\n) -> PackageJson:\n    return PackageJson(\n        name=name,\n        version=version,\n        path=path,\n        private=True,\n        helm=HelmConfig(registry=\"oci://docker.io/llamaindex\"),\n    )\n\n\ndef test_sync_chart_version_updates_chart_yaml(tmp_path: Path) -> None:\n    chart_yaml = tmp_path / \"Chart.yaml\"\n    chart_yaml.write_text('apiVersion: v2\\nname: llama-agents\\nversion: \"0.4.13\"\\n')\n\n    pkg = _make_helm_package(tmp_path, version=\"0.4.14\")\n    changed = apply_sync_values(pkg, {pkg.name: pkg})\n    assert changed is True\n    content = chart_yaml.read_text()\n    assert \"0.4.14\" in content\n\n\ndef test_sync_chart_version_no_change_when_already_matching(tmp_path: Path) -> None:\n    chart_yaml = tmp_path / \"Chart.yaml\"\n    chart_yaml.write_text('apiVersion: v2\\nname: llama-agents\\nversion: \"0.4.14\"\\n')\n\n    pkg = _make_helm_package(tmp_path, version=\"0.4.14\")\n    changed = apply_sync_values(pkg, {pkg.name: pkg})\n    assert changed is False\n\n\ndef test_sync_chart_version_returns_false_when_no_chart(tmp_path: Path) -> None:\n    pkg = _make_helm_package(tmp_path, version=\"0.4.14\")\n    changed = apply_sync_values(pkg, {pkg.name: pkg})\n    assert changed is False\n\n\ndef test_apply_sync_values_bakes_image_tags(tmp_path: Path) -> None:\n    \"\"\"syncValues on the chart resolves dependency docker tags into values.yaml.\"\"\"\n    chart_yaml = tmp_path / \"Chart.yaml\"\n    chart_yaml.write_text('apiVersion: v2\\nname: llama-agents\\nversion: \"0.4.13\"\\n')\n    values_yaml = tmp_path / \"values.yaml\"\n    values_yaml.write_text(\n        \"\"\"images:\n  controlPlane:\n    repository: llamaindex/llama-agents-control-plane\n    tag: \"0.4.13\"\n    pullPolicy: IfNotPresent\n  operator:\n    repository: llamaindex/llama-agents-operator\n    tag: \"operator-0.4.13\"\n    pullPolicy: IfNotPresent\n  appserver:\n    repository: llamaindex/llama-agents-appserver\n    tag: \"appserver-0.4.13\"\n    pullPolicy: IfNotPresent\n\"\"\"\n    )\n\n    chart_pkg = PackageJson(\n        name=\"llama-agents\",\n        version=\"0.4.14\",\n        path=tmp_path,\n        private=True,\n        helm=HelmConfig(registry=\"oci://docker.io/llamaindex\"),\n        syncValues={\n            \"values.yaml\": {\n                \"images.controlPlane.tag\": \"{llama-agents-control-plane:dockerTag}\",\n                \"images.operator.tag\": \"{llama-agents-operator:dockerTag}\",\n                \"images.appserver.tag\": \"{llama-agents-appserver:dockerTag}\",\n            }\n        },\n    )\n    workspace = {\n        \"llama-agents\": chart_pkg,\n        \"llama-agents-control-plane\": PackageJson(\n            name=\"llama-agents-control-plane\",\n            version=\"0.4.14\",\n            path=Path(\"/fake/cp\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                target=\"controlplane\",\n                imageName=\"llamaindex/llama-agents-control-plane\",\n                platforms=[\"linux/amd64\"],\n            ),\n        ),\n        \"llama-agents-operator\": PackageJson(\n            name=\"llama-agents-operator\",\n            version=\"0.4.15\",\n            path=Path(\"/fake/op\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/operator.Dockerfile\",\n                imageName=\"llamaindex/llama-agents-operator\",\n                platforms=[\"linux/amd64\"],\n            ),\n        ),\n        \"llama-agents-appserver\": PackageJson(\n            name=\"llama-agents-appserver\",\n            version=\"0.4.16\",\n            path=Path(\"/fake/as\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                target=\"appserver\",\n                imageName=\"llamaindex/llama-agents-appserver\",\n                platforms=[\"linux/amd64\"],\n            ),\n        ),\n    }\n\n    changed = apply_sync_values(chart_pkg, workspace)\n    assert changed is True\n    content = values_yaml.read_text()\n    assert \"0.4.14\" in content  # controlPlane\n    assert \"0.4.15\" in content\n    assert \"0.4.16\" in content\n    # Chart.yaml version also updated via implicit helm sync\n    assert \"0.4.14\" in chart_yaml.read_text()\n\n\n# -- is_rc_version tests --\n\n\n@pytest.mark.parametrize(\n    (\"version\", \"expected\"),\n    [\n        (\"0.4.14\", False),\n        (\"1.0.0\", False),\n        (\"0.4.14-rc.1\", True),\n        (\"0.4.14-a.1\", True),\n        (\"0.4.14-b.1\", True),\n        (\"1.0.0rc1\", True),\n        (\"1.0.0a1\", True),\n        (\"1.0.0b1\", True),\n    ],\n)\ndef test_is_rc_version(version: str, expected: bool) -> None:\n    assert is_rc_version(version) == expected\n\n\n# -- is_docker_image_published tests --\n\n\ndef test_is_docker_image_published_returns_true() -> None:\n    with patch(\"urllib.request.urlopen\"):\n        result = is_docker_image_published(\n            \"llamaindex/llama-agents-appserver\", \"0.4.14\"\n        )\n        assert result is True\n\n\ndef test_is_docker_image_published_returns_false_on_404() -> None:\n    mock_error = HTTPError(\"url\", 404, \"Not Found\", {}, None)  # type: ignore[arg-type]\n    with patch(\"urllib.request.urlopen\", side_effect=mock_error):\n        result = is_docker_image_published(\n            \"llamaindex/llama-agents-appserver\", \"0.4.14\"\n        )\n        assert result is False\n\n\ndef test_is_docker_image_published_raises_on_500() -> None:\n    mock_error = HTTPError(\"url\", 500, \"Server Error\", {}, None)  # type: ignore[arg-type]\n    with patch(\"urllib.request.urlopen\", side_effect=mock_error):\n        with pytest.raises(HTTPError) as exc_info:\n            is_docker_image_published(\"llamaindex/llama-agents-appserver\", \"0.4.14\")\n        assert exc_info.value.code == 500\n\n\n# -- docker_image_tags tests --\n\n\ndef test_docker_image_tags_non_rc() -> None:\n    image = DockerConfig(\n        dockerfile=\"docker/Dockerfile\",\n        target=\"controlplane\",\n        platforms=[\"linux/amd64\", \"linux/arm64\"],\n        imageName=\"llamaindex/llama-agents-control-plane\",\n    )\n    tags = docker_image_tags(image, \"0.4.14\", is_rc=False)\n    assert tags == [\n        \"docker.io/llamaindex/llama-agents-control-plane:0.4.14\",\n        \"docker.io/llamaindex/llama-agents-control-plane:latest\",\n        \"docker.io/llamaindex/llama-agents-control-plane:0.4\",\n    ]\n\n\ndef test_docker_image_tags_rc() -> None:\n    image = DockerConfig(\n        dockerfile=\"docker/Dockerfile\",\n        target=\"controlplane\",\n        platforms=[\"linux/amd64\", \"linux/arm64\"],\n        imageName=\"llamaindex/llama-agents-control-plane\",\n    )\n    tags = docker_image_tags(image, \"0.4.14-rc.1\", is_rc=True)\n    assert tags == [\"docker.io/llamaindex/llama-agents-control-plane:0.4.14-rc.1\"]\n\n\ndef test_docker_image_tags_appserver() -> None:\n    image = DockerConfig(\n        dockerfile=\"docker/Dockerfile\",\n        target=\"appserver\",\n        platforms=[\"linux/amd64\", \"linux/arm64\"],\n        imageName=\"llamaindex/llama-agents-appserver\",\n    )\n    tags = docker_image_tags(image, \"0.4.14\", is_rc=False)\n    assert tags == [\n        \"docker.io/llamaindex/llama-agents-appserver:0.4.14\",\n        \"docker.io/llamaindex/llama-agents-appserver:latest\",\n        \"docker.io/llamaindex/llama-agents-appserver:0.4\",\n    ]\n\n\n# -- _read_package_json_config tests --\n\n\ndef test_read_package_json_config_docker(tmp_path: Path) -> None:\n    from dev_cli.changesets import _read_package_json_config\n\n    pkg_json = tmp_path / \"package.json\"\n    pkg_json.write_text(\n        json.dumps(\n            {\n                \"name\": \"test-pkg\",\n                \"docker\": {\n                    \"dockerfile\": \"docker/Dockerfile\",\n                    \"target\": \"myapp\",\n                    \"imageName\": \"llamaindex/test-myapp\",\n                    \"platforms\": [\"linux/amd64\"],\n                },\n            }\n        )\n    )\n    config = _read_package_json_config(tmp_path)\n    assert config is not None\n    assert config.docker is not None\n    assert config.docker.dockerfile == \"docker/Dockerfile\"\n    assert config.docker.target == \"myapp\"\n    assert config.docker.imageName == \"llamaindex/test-myapp\"\n    assert config.docker.platforms == [\"linux/amd64\"]\n\n\ndef test_read_package_json_config_helm(tmp_path: Path) -> None:\n    from dev_cli.changesets import _read_package_json_config\n\n    pkg_json = tmp_path / \"package.json\"\n    pkg_json.write_text(\n        json.dumps(\n            {\"name\": \"my-chart\", \"helm\": {\"registry\": \"oci://docker.io/llamaindex\"}}\n        )\n    )\n    config = _read_package_json_config(tmp_path)\n    assert config is not None\n    assert config.helm is not None\n    assert config.helm.registry == \"oci://docker.io/llamaindex\"\n\n\ndef test_read_package_json_config_defaults(tmp_path: Path) -> None:\n    from dev_cli.changesets import _read_package_json_config\n\n    pkg_json = tmp_path / \"package.json\"\n    pkg_json.write_text(json.dumps({\"name\": \"test-pkg\"}))\n    config = _read_package_json_config(tmp_path)\n    assert config is not None\n    assert config.docker is None\n    assert config.helm is None\n\n\ndef test_read_package_json_config_returns_none_when_no_file(tmp_path: Path) -> None:\n    from dev_cli.changesets import _read_package_json_config\n\n    assert _read_package_json_config(tmp_path) is None\n\n\ndef test_read_package_json_config_docker_missing_dockerfile(tmp_path: Path) -> None:\n    from pydantic import ValidationError\n\n    from dev_cli.changesets import _read_package_json_config\n\n    pkg_json = tmp_path / \"package.json\"\n    pkg_json.write_text(json.dumps({\"name\": \"test-pkg\", \"docker\": {\"target\": \"app\"}}))\n    with pytest.raises(ValidationError):\n        _read_package_json_config(tmp_path)\n\n\ndef test_read_package_json_config_helm_missing_registry(tmp_path: Path) -> None:\n    from pydantic import ValidationError\n\n    from dev_cli.changesets import _read_package_json_config\n\n    pkg_json = tmp_path / \"package.json\"\n    pkg_json.write_text(json.dumps({\"name\": \"my-chart\", \"helm\": {}}))\n    with pytest.raises(ValidationError):\n        _read_package_json_config(tmp_path)\n\n\n# -- plan_docker / execute_docker_build_action tests --\n\n\ndef _make_docker_packages() -> list[PackageJson]:\n    \"\"\"Create test PackageJson instances with Docker configs.\"\"\"\n    return [\n        PackageJson(\n            name=\"llama-agents-control-plane\",\n            version=\"0.4.14\",\n            path=Path(\"/fake/controlplane\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                target=\"controlplane\",\n                platforms=[\"linux/amd64\", \"linux/arm64\"],\n                imageName=\"llamaindex/llama-agents-control-plane\",\n            ),\n        ),\n        PackageJson(\n            name=\"llama-agents-operator\",\n            version=\"0.4.14\",\n            path=Path(\"/fake/operator\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/operator.Dockerfile\",\n                platforms=[\"linux/amd64\"],\n                imageName=\"llamaindex/llama-agents-operator\",\n            ),\n        ),\n        PackageJson(\n            name=\"llama-agents-appserver\",\n            version=\"0.4.14\",\n            path=Path(\"/fake/appserver\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                target=\"appserver\",\n                platforms=[\"linux/amd64\", \"linux/arm64\"],\n                imageName=\"llamaindex/llama-agents-appserver\",\n            ),\n        ),\n    ]\n\n\ndef test_plan_docker_skips_when_already_published() -> None:\n    with patch(\"dev_cli.changesets.is_docker_image_published\", return_value=True):\n        builds, manifests = plan_docker(_make_docker_packages())\n    assert builds == []\n    assert manifests == []\n\n\ndef test_plan_docker_emits_per_platform_builds() -> None:\n    with patch(\"dev_cli.changesets.is_docker_image_published\", return_value=False):\n        builds, manifests = plan_docker(_make_docker_packages())\n    # 2 + 1 + 2 = 5 platform builds, 3 manifests\n    assert len(builds) == 5\n    assert len(manifests) == 3\n    # Each manifest references the matching per-arch build tags\n    cp_manifest = next(\n        m for m in manifests if m.package == \"llama-agents-control-plane\"\n    )\n    assert sorted(cp_manifest.source_tags) == sorted(\n        [\n            \"docker.io/llamaindex/llama-agents-control-plane:0.4.14-amd64\",\n            \"docker.io/llamaindex/llama-agents-control-plane:0.4.14-arm64\",\n        ]\n    )\n\n\ndef test_plan_docker_skips_packages_without_docker() -> None:\n    packages = [\n        PackageJson(\n            name=\"no-docker-pkg\",\n            version=\"1.0.0\",\n            path=Path(\"/fake/no-docker\"),\n            private=True,\n            docker=None,\n        ),\n    ]\n    with patch(\"dev_cli.changesets.is_docker_image_published\") as mock_check:\n        builds, manifests = plan_docker(packages)\n    mock_check.assert_not_called()\n    assert builds == [] and manifests == []\n\n\ndef test_plan_docker_uses_per_package_version() -> None:\n    packages = [\n        PackageJson(\n            name=\"pkg-a\",\n            version=\"1.0.0\",\n            path=Path(\"/fake/a\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                target=\"a\",\n                platforms=[\"linux/amd64\"],\n                imageName=\"llamaindex/test-a\",\n            ),\n        ),\n        PackageJson(\n            name=\"pkg-b\",\n            version=\"2.0.0\",\n            path=Path(\"/fake/b\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                target=\"b\",\n                platforms=[\"linux/amd64\"],\n                imageName=\"llamaindex/test-b\",\n            ),\n        ),\n    ]\n    with patch(\"dev_cli.changesets.is_docker_image_published\", return_value=False):\n        builds, _ = plan_docker(packages)\n    by_pkg = {b.package: b for b in builds}\n    assert by_pkg[\"pkg-a\"].version == \"1.0.0\"\n    assert by_pkg[\"pkg-a\"].build_tag.endswith(\"1.0.0-amd64\")\n    assert by_pkg[\"pkg-b\"].version == \"2.0.0\"\n    assert by_pkg[\"pkg-b\"].build_tag.endswith(\"2.0.0-amd64\")\n\n\ndef _docker_build_action() -> DockerBuildAction:\n    return DockerBuildAction(\n        package=\"pkg\",\n        image=\"llamaindex/test\",\n        dockerfile=\"docker/Dockerfile\",\n        target=\"t\",\n        platform=\"linux/amd64\",\n        version=\"1.0.0\",\n        build_tag=\"docker.io/llamaindex/test:1.0.0-amd64\",\n        cache_scope=\"pkg-amd64\",\n    )\n\n\ndef test_execute_docker_build_action_dry_run_skips_run_command() -> None:\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(_docker_build_action(), dry_run=True)\n    mock_run.assert_not_called()\n\n\ndef test_execute_docker_build_action_invokes_buildx() -> None:\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(_docker_build_action(), dry_run=False)\n    cmd = mock_run.call_args[0][0]\n    assert cmd[:3] == [\"docker\", \"buildx\", \"build\"]\n    assert \"--push\" in cmd\n    assert \"--platform\" in cmd and \"linux/amd64\" in cmd\n    assert \"--tag\" in cmd and \"docker.io/llamaindex/test:1.0.0-amd64\" in cmd\n\n\ndef test_execute_docker_build_action_emits_gha_cache_flags(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setenv(\"ACTIONS_RUNTIME_TOKEN\", \"token\")\n    monkeypatch.setenv(\"ACTIONS_CACHE_URL\", \"https://cache.example.com\")\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(_docker_build_action(), dry_run=False)\n    cmd = mock_run.call_args[0][0]\n    assert \"--cache-from\" in cmd\n    cache_to = cmd[cmd.index(\"--cache-to\") + 1]\n    # Default cache_mode is \"max\"; zstd + ignore-error are always set under GHA.\n    assert \"mode=max\" in cache_to\n    assert \"scope=pkg-amd64\" in cache_to\n    assert \"compression=zstd\" in cache_to\n    assert \"ignore-error=true\" in cache_to\n\n\ndef test_execute_docker_build_action_respects_cache_mode_min(\n    monkeypatch: pytest.MonkeyPatch,\n) -> None:\n    monkeypatch.setenv(\"ACTIONS_RUNTIME_TOKEN\", \"token\")\n    monkeypatch.setenv(\"ACTIONS_CACHE_URL\", \"https://cache.example.com\")\n    action = _docker_build_action().model_copy(update={\"cache_mode\": \"min\"})\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(action, dry_run=False)\n    cmd = mock_run.call_args[0][0]\n    cache_to = cmd[cmd.index(\"--cache-to\") + 1]\n    assert \"mode=min\" in cache_to\n    assert \"mode=max\" not in cache_to\n\n\ndef test_plan_docker_propagates_cache_mode() -> None:\n    packages = [\n        PackageJson(\n            name=\"pkg-min\",\n            path=Path(\"/tmp/pkg-min\"),\n            version=\"1.0.0\",\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                imageName=\"llamaindex/pkg-min\",\n                platforms=[\"linux/amd64\"],\n                cacheMode=\"min\",\n            ),\n        ),\n        PackageJson(\n            name=\"pkg-default\",\n            path=Path(\"/tmp/pkg-default\"),\n            version=\"1.0.0\",\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                imageName=\"llamaindex/pkg-default\",\n                platforms=[\"linux/amd64\"],\n            ),\n        ),\n    ]\n    with patch(\"dev_cli.changesets.is_docker_image_published\", return_value=False):\n        builds, _ = plan_docker(packages)\n    by_pkg = {b.package: b for b in builds}\n    assert by_pkg[\"pkg-min\"].cache_mode == \"min\"\n    assert by_pkg[\"pkg-default\"].cache_mode == \"max\"\n\n\ndef test_execute_docker_manifest_action_combines_source_tags() -> None:\n    action = DockerManifestAction(\n        package=\"pkg\",\n        image=\"llamaindex/test\",\n        version=\"1.0.0\",\n        final_tags=[\n            \"docker.io/llamaindex/test:1.0.0\",\n            \"docker.io/llamaindex/test:latest\",\n        ],\n        source_tags=[\n            \"docker.io/llamaindex/test:1.0.0-amd64\",\n            \"docker.io/llamaindex/test:1.0.0-arm64\",\n        ],\n    )\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(action, dry_run=False)\n    cmd = mock_run.call_args[0][0]\n    assert cmd[:4] == [\"docker\", \"buildx\", \"imagetools\", \"create\"]\n    assert cmd.count(\"--tag\") == 2\n    assert \"docker.io/llamaindex/test:1.0.0-amd64\" in cmd\n    assert \"docker.io/llamaindex/test:1.0.0-arm64\" in cmd\n\n\n# -- _execute_pypi tests --\n\n\ndef _write_pyproject(path: Path, name: str, version: str) -> None:\n    path.write_text(\n        f'[project]\\nname = \"{name}\"\\nversion = \"{version}\"\\ndependencies = []\\n'\n    )\n\n\ndef test_execute_pypi_dry_run_skips_run_command(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    pkg_dir = tmp_path / \"packages\" / \"pkg\"\n    pkg_dir.mkdir(parents=True)\n    _write_pyproject(pkg_dir / \"pyproject.toml\", \"pkg\", \"1.2.3\")\n    monkeypatch.chdir(tmp_path)\n    action = PypiAction(package=\"pkg\", version=\"1.2.3\", path=\"packages/pkg\")\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(action, dry_run=True)\n    mock_run.assert_not_called()\n\n\ndef test_execute_pypi_raises_when_plan_version_mismatches_on_disk(\n    tmp_path: Path, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    # Reproduces the publish_changesets.yml bug: the plan was computed\n    # from a tree where ``pnpm -w run version`` had bumped pyproject.toml\n    # to 0.8.10, but the publish job checks out the branch fresh and\n    # sees the still-committed 0.8.9. Without this guard, uv build would\n    # silently produce 0.8.9 artifacts that don't match the plan glob.\n    pkg_dir = tmp_path / \"packages\" / \"pkg\"\n    pkg_dir.mkdir(parents=True)\n    _write_pyproject(pkg_dir / \"pyproject.toml\", \"pkg\", \"0.8.9\")\n    monkeypatch.chdir(tmp_path)\n    action = PypiAction(package=\"pkg\", version=\"0.8.10\", path=\"packages/pkg\")\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        with pytest.raises(RuntimeError, match=\"Plan expects pkg@0.8.10\"):\n            execute_action(action, dry_run=False)\n    mock_run.assert_not_called()\n\n\n# -- plan_helm / execute_helm_action tests --\n\n\ndef _make_helm_packages() -> list[PackageJson]:\n    return [\n        PackageJson(\n            name=\"llama-agents\",\n            version=\"0.4.14\",\n            path=Path(\"/fake/charts/llama-agents\"),\n            private=False,\n            helm=HelmConfig(registry=\"oci://docker.io/llamaindex\"),\n        ),\n        PackageJson(\n            name=\"llama-agents-crds\",\n            version=\"0.4.14\",\n            path=Path(\"/fake/charts/llama-agents-crds\"),\n            private=False,\n            helm=HelmConfig(registry=\"oci://docker.io/llamaindex\"),\n        ),\n    ]\n\n\ndef test_plan_helm_skips_already_published() -> None:\n    with patch(\"dev_cli.changesets.is_helm_chart_published\", return_value=True):\n        actions = plan_helm(_make_helm_packages())\n    assert actions == []\n\n\ndef test_plan_helm_emits_one_action_per_chart() -> None:\n    with patch(\"dev_cli.changesets.is_helm_chart_published\", return_value=False):\n        actions = plan_helm(_make_helm_packages())\n    assert [a.package for a in actions] == [\"llama-agents\", \"llama-agents-crds\"]\n    assert all(a.version == \"0.4.14\" for a in actions)\n\n\ndef test_plan_helm_skips_packages_without_helm() -> None:\n    packages = [\n        PackageJson(\n            name=\"no-helm-pkg\",\n            version=\"1.0.0\",\n            path=Path(\"/fake\"),\n            private=True,\n            helm=None,\n        ),\n    ]\n    with patch(\"dev_cli.changesets.is_helm_chart_published\") as mock_check:\n        assert plan_helm(packages) == []\n    mock_check.assert_not_called()\n\n\ndef test_execute_helm_action_dry_run_skips_run_command() -> None:\n    action = HelmAction(\n        package=\"llama-agents\",\n        chart_path=\"/fake/charts/llama-agents\",\n        version=\"0.4.14\",\n        registry=\"oci://docker.io/llamaindex\",\n    )\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(action, dry_run=True)\n    mock_run.assert_not_called()\n\n\ndef test_execute_helm_action_packages_and_pushes() -> None:\n    action = HelmAction(\n        package=\"llama-agents\",\n        chart_path=\"/fake/charts/llama-agents\",\n        version=\"0.4.14\",\n        registry=\"oci://docker.io/llamaindex\",\n    )\n    with patch(\"dev_cli.changesets.run_command\") as mock_run:\n        execute_action(action, dry_run=False)\n    assert mock_run.call_args_list[0][0][0] == [\n        \"helm\",\n        \"package\",\n        \"/fake/charts/llama-agents\",\n    ]\n    assert mock_run.call_args_list[1][0][0] == [\n        \"helm\",\n        \"push\",\n        \"llama-agents-0.4.14.tgz\",\n        \"oci://docker.io/llamaindex\",\n    ]\n\n\ndef test_is_helm_chart_published_uses_chart_name() -> None:\n    with patch(\"urllib.request.urlopen\") as mock_urlopen:\n        result = is_helm_chart_published(\"llama-agents-crds\", \"0.7.1\")\n        assert result is True\n        called_url = mock_urlopen.call_args[0][0]\n        assert \"llamaindex/llama-agents-crds\" in called_url\n        assert \"0.7.1\" in called_url\n\n\n# -- template resolution tests --\n\n\ndef _make_workspace() -> tuple[PackageJson, dict[str, PackageJson]]:\n    \"\"\"Create a self_pkg and workspace for template tests.\"\"\"\n    self_pkg = PackageJson(\n        name=\"my-chart\",\n        version=\"1.2.3\",\n        path=Path(\"/fake/chart\"),\n        private=True,\n        helm=HelmConfig(registry=\"oci://docker.io/llamaindex\"),\n    )\n    workspace = {\n        \"my-chart\": self_pkg,\n        \"my-app\": PackageJson(\n            name=\"my-app\",\n            version=\"2.0.0\",\n            path=Path(\"/fake/app\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"docker/Dockerfile\",\n                imageName=\"llamaindex/test-app\",\n                platforms=[\"linux/amd64\"],\n            ),\n        ),\n        \"no-docker\": PackageJson(\n            name=\"no-docker\",\n            version=\"3.0.0\",\n            path=Path(\"/fake/nodock\"),\n            private=True,\n        ),\n    }\n    return self_pkg, workspace\n\n\ndef test_resolve_template_version() -> None:\n    self_pkg, workspace = _make_workspace()\n    assert _resolve_template(\"{my-app:version}\", self_pkg, workspace) == \"2.0.0\"\n\n\ndef test_resolve_template_self_version() -> None:\n    self_pkg, workspace = _make_workspace()\n    assert _resolve_template(\"{self:version}\", self_pkg, workspace) == \"1.2.3\"\n\n\ndef test_resolve_template_docker_tag() -> None:\n    self_pkg, workspace = _make_workspace()\n    assert _resolve_template(\"{my-app:dockerTag}\", self_pkg, workspace) == \"2.0.0\"\n\n\ndef test_resolve_template_pep440_version() -> None:\n    self_pkg, workspace = _make_workspace()\n    self_pkg.version = \"1.2.3-a.4\"\n    assert _resolve_template(\"{self:pep440Version}\", self_pkg, workspace) == \"1.2.3a4\"\n\n\ndef test_resolve_template_unknown_package() -> None:\n    self_pkg, workspace = _make_workspace()\n    with pytest.raises(ValueError, match=\"unknown package 'nonexistent'\"):\n        _resolve_template(\"{nonexistent:version}\", self_pkg, workspace)\n\n\ndef test_resolve_template_docker_tag_no_docker() -> None:\n    self_pkg, workspace = _make_workspace()\n    with pytest.raises(ValueError, match=\"has no docker config\"):\n        _resolve_template(\"{no-docker:dockerTag}\", self_pkg, workspace)\n\n\ndef test_resolve_template_unknown_property() -> None:\n    self_pkg, workspace = _make_workspace()\n    with pytest.raises(ValueError, match=\"unknown property 'badProp'\"):\n        _resolve_template(\"{my-app:badProp}\", self_pkg, workspace)\n\n\n# -- YAML writer tests --\n\n\ndef test_write_yaml_values_sets_dot_path(tmp_path: Path) -> None:\n    yaml_file = tmp_path / \"values.yaml\"\n    yaml_file.write_text(\n        \"\"\"# A comment\nimages:\n  controlPlane:\n    repository: llamaindex/llama-agents-control-plane\n    tag: \"0.4.13\"\n    pullPolicy: IfNotPresent\n\"\"\"\n    )\n    changed = _write_yaml_values(yaml_file, {\"images.controlPlane.tag\": \"0.4.14\"})\n    assert changed is True\n    content = yaml_file.read_text()\n    assert \"0.4.14\" in content\n    assert \"# A comment\" in content  # comments preserved\n\n\ndef test_write_yaml_values_preserves_comments(tmp_path: Path) -> None:\n    yaml_file = tmp_path / \"values.yaml\"\n    original = \"\"\"# Top comment\nimages:\n  controlPlane: # the control plane\n    repository: llamaindex/llama-agents-control-plane\n    tag: \"0.8.0\"\n    pullPolicy: IfNotPresent\n  operator: # the operator\n    repository: llamaindex/llama-agents-operator\n    tag: \"operator-0.8.0\"\n    pullPolicy: IfNotPresent\n\"\"\"\n    yaml_file.write_text(original)\n    changed = _write_yaml_values(yaml_file, {\"images.controlPlane.tag\": \"0.9.0\"})\n    assert changed is True\n    content = yaml_file.read_text()\n    assert \"0.9.0\" in content\n    assert \"# Top comment\" in content\n    assert \"# the control plane\" in content\n    assert \"# the operator\" in content\n\n\ndef test_write_yaml_values_no_change(tmp_path: Path) -> None:\n    yaml_file = tmp_path / \"values.yaml\"\n    yaml_file.write_text('version: \"0.8.0\"\\n')\n    changed = _write_yaml_values(yaml_file, {\"version\": \"0.8.0\"})\n    assert changed is False\n\n\ndef test_write_yaml_values_missing_file(tmp_path: Path) -> None:\n    yaml_file = tmp_path / \"nonexistent.yaml\"\n    changed = _write_yaml_values(yaml_file, {\"key\": \"val\"})\n    assert changed is False\n\n\ndef test_write_yaml_values_creates_intermediate_keys(tmp_path: Path) -> None:\n    yaml_file = tmp_path / \"test.yaml\"\n    yaml_file.write_text(\"existing: value\\n\")\n    changed = _write_yaml_values(yaml_file, {\"new.nested.key\": \"hello\"})\n    assert changed is True\n    content = yaml_file.read_text()\n    assert \"hello\" in content\n\n\n# -- TOML writer tests --\n\n\ndef test_write_toml_values_sets_dot_path(tmp_path: Path) -> None:\n    toml_file = tmp_path / \"pyproject.toml\"\n    toml_file.write_text(\n        \"\"\"[project]\nname = \"test\"\nversion = \"1.0.0\"\n\"\"\"\n    )\n    changed = _write_toml_values(toml_file, {\"project.version\": \"2.0.0\"})\n    assert changed is True\n    content = toml_file.read_text()\n    assert '\"2.0.0\"' in content\n\n\ndef test_write_toml_values_no_change(tmp_path: Path) -> None:\n    toml_file = tmp_path / \"pyproject.toml\"\n    toml_file.write_text(\n        \"\"\"[project]\nname = \"test\"\nversion = \"1.0.0\"\n\"\"\"\n    )\n    changed = _write_toml_values(toml_file, {\"project.version\": \"1.0.0\"})\n    assert changed is False\n\n\ndef test_write_toml_values_missing_file(tmp_path: Path) -> None:\n    toml_file = tmp_path / \"nonexistent.toml\"\n    changed = _write_toml_values(toml_file, {\"key\": \"val\"})\n    assert changed is False\n\n\n# -- apply_sync_values tests --\n\n\ndef test_apply_sync_values_explicit_yaml(tmp_path: Path) -> None:\n    values_yaml = tmp_path / \"values.yaml\"\n    values_yaml.write_text(\n        \"\"\"images:\n  app:\n    tag: \"old\"\n\"\"\"\n    )\n    pkg = PackageJson(\n        name=\"my-chart\",\n        version=\"1.0.0\",\n        path=tmp_path,\n        private=True,\n        syncValues={\n            \"values.yaml\": {\n                \"images.app.tag\": \"{dep-app:dockerTag}\",\n            }\n        },\n    )\n    workspace = {\n        \"my-chart\": pkg,\n        \"dep-app\": PackageJson(\n            name=\"dep-app\",\n            version=\"2.0.0\",\n            path=Path(\"/fake\"),\n            private=False,\n            docker=DockerConfig(\n                dockerfile=\"Dockerfile\",\n                imageName=\"llamaindex/test-dep-app\",\n                platforms=[\"linux/amd64\"],\n            ),\n        ),\n    }\n    changed = apply_sync_values(pkg, workspace)\n    assert changed is True\n    assert \"2.0.0\" in values_yaml.read_text()\n\n\ndef test_apply_sync_values_implicit_helm(tmp_path: Path) -> None:\n    chart_yaml = tmp_path / \"Chart.yaml\"\n    chart_yaml.write_text('apiVersion: v2\\nversion: \"0.1.0\"\\n')\n    pkg = PackageJson(\n        name=\"my-chart\",\n        version=\"0.2.0\",\n        path=tmp_path,\n        private=True,\n        helm=HelmConfig(registry=\"oci://docker.io/llamaindex\"),\n    )\n    changed = apply_sync_values(pkg, {\"my-chart\": pkg})\n    assert changed is True\n    assert \"0.2.0\" in chart_yaml.read_text()\n\n\ndef test_apply_sync_values_implicit_pyproject(tmp_path: Path) -> None:\n    pyproject = tmp_path / \"pyproject.toml\"\n    pyproject.write_text('[project]\\nname = \"test\"\\nversion = \"1.0.0\"\\n')\n    pkg = PackageJson(\n        name=\"test-pkg\",\n        version=\"2.0.0\",\n        path=tmp_path,\n        private=False,\n    )\n    changed = apply_sync_values(pkg, {\"test-pkg\": pkg})\n    assert changed is True\n    content = pyproject.read_text()\n    assert '\"2.0.0\"' in content\n"
  },
  {
    "path": "tests/dev_cli/test_cli_misc.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for miscellaneous CLI commands (tag-metadata, update-index-html).\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nimport pytest\nfrom click.testing import CliRunner\nfrom conftest import commit_and_tag\n\nfrom dev_cli.cli import cli\n\n\n@pytest.fixture\ndef runner() -> CliRunner:\n    return CliRunner()\n\n\n# --- compute-tag-metadata tests ---\n\n\n@pytest.mark.parametrize(\n    (\"from_version\", \"to_version\", \"expected_type\"),\n    [\n        (\"v1.0.0\", \"v1.0.1\", \"patch\"),\n        (\"v1.0.0\", \"v1.1.0\", \"minor\"),\n        (\"v1.0.0\", \"v2.0.0\", \"major\"),\n    ],\n)\ndef test_compute_tag_metadata_change_types(\n    runner: CliRunner,\n    git_repo: Path,\n    from_version: str,\n    to_version: str,\n    expected_type: str,\n) -> None:\n    commit_and_tag(git_repo, \"file.txt\", f\"pkg@{from_version}\", f\"pkg@{from_version}\")\n    commit_and_tag(git_repo, \"file.txt\", f\"pkg@{to_version}\", f\"pkg@{to_version}\")\n\n    result = runner.invoke(\n        cli,\n        [\"compute-tag-metadata\", \"--tag\", f\"pkg@{to_version}\"],\n        env={},\n    )\n    assert result.exit_code == 0\n    assert f\"Change type: {expected_type}\" in result.output\n\n\ndef test_compute_tag_metadata_writes_output_file(\n    runner: CliRunner, git_repo: Path\n) -> None:\n    commit_and_tag(git_repo, \"file.txt\", \"pkg@v1.0.0\", \"pkg@v1.0.0\")\n    commit_and_tag(git_repo, \"file.txt\", \"pkg@v1.0.1\", \"pkg@v1.0.1\")\n\n    output_file = git_repo / \"out.txt\"\n    result = runner.invoke(\n        cli,\n        [\"compute-tag-metadata\", \"--tag\", \"pkg@v1.0.1\", \"--output\", str(output_file)],\n        env={},\n    )\n    assert result.exit_code == 0\n    contents = output_file.read_text()\n    assert \"tag_suffix=v1.0.1\" in contents\n    assert \"semver=1.0.1\" in contents\n    assert \"change_type=patch\" in contents\n\n\ndef test_compute_tag_metadata_requires_tag(runner: CliRunner) -> None:\n    result = runner.invoke(cli, [\"compute-tag-metadata\"])\n    assert result.exit_code != 0\n    assert \"Missing option '--tag'\" in result.output\n\n\n# --- update-index-html tests ---\n\n\ndef test_update_index_html_success(runner: CliRunner, tmp_path: Path) -> None:\n    index_path = tmp_path / \"index.html\"\n    index_path.write_text(\n        \"\"\"\n<html>\n  <head>\n    <script type=\"module\" crossorigin src=\"old.js\"></script>\n    <link rel=\"stylesheet\" crossorigin href=\"old.css\">\n  </head>\n</html>\n\"\"\".strip()\n    )\n    result = runner.invoke(\n        cli,\n        [\n            \"update-index-html\",\n            \"--js-url\",\n            \"https://cdn/js\",\n            \"--css-url\",\n            \"https://cdn/css\",\n            \"--index-path\",\n            str(index_path),\n        ],\n    )\n    assert result.exit_code == 0\n    updated = index_path.read_text()\n    assert 'src=\"https://cdn/js\"' in updated\n    assert 'href=\"https://cdn/css\"' in updated\n\n\ndef test_update_index_html_missing_file(runner: CliRunner, tmp_path: Path) -> None:\n    result = runner.invoke(\n        cli,\n        [\n            \"update-index-html\",\n            \"--js-url\",\n            \"https://cdn/js\",\n            \"--css-url\",\n            \"https://cdn/css\",\n            \"--index-path\",\n            str(tmp_path / \"missing.html\"),\n        ],\n    )\n    assert result.exit_code != 0\n    assert \"not found\" in result.output\n"
  },
  {
    "path": "tests/dev_cli/test_pytest_cmd.py",
    "content": "# SPDX-License-Identifier: MIT\n# Copyright (c) 2026 LlamaIndex Inc.\n\"\"\"Tests for the pytest CLI command.\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\nimport threading\nimport time\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\nfrom unittest.mock import Mock, patch\n\nimport pytest\nfrom click.testing import CliRunner\nfrom conftest import is_sync_call, sync_success\n\nfrom dev_cli.cli import cli\nfrom dev_cli.commands.pytest_cmd import (\n    PackageInfo,\n    _render_progress_table,\n    discover_test_packages,\n    extract_failed_test_names,\n    extract_failures_section,\n    extract_test_counts,\n    run_tests_with_rich_progress,\n)\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n\n@pytest.fixture\ndef runner() -> CliRunner:\n    return CliRunner()\n\n\n@pytest.fixture\ndef packages_dir(tmp_path: Path) -> Path:\n    \"\"\"Create a packages directory.\"\"\"\n    packages = tmp_path / \"packages\"\n    packages.mkdir()\n    return packages\n\n\n@pytest.fixture\ndef create_pkg(packages_dir: Path) -> Callable[[str], PackageInfo]:\n    \"\"\"Factory to create a package with tests subdirectory and pyproject.toml.\"\"\"\n\n    def _create(name: str) -> PackageInfo:\n        pkg = packages_dir / name\n        pkg.mkdir()\n        (pkg / \"tests\").mkdir()\n        (pkg / \"pyproject.toml\").write_text(f'[project]\\nname = \"{name}\"\\n')\n        return PackageInfo(path=pkg, name=name)\n\n    return _create\n\n\n# --- discover_test_packages tests ---\n\n\ndef test_discover_finds_packages_with_tests(packages_dir: Path) -> None:\n    # Create package with tests and pyproject.toml\n    pkg = packages_dir / \"pkg-with-tests\"\n    pkg.mkdir()\n    (pkg / \"tests\").mkdir()\n    (pkg / \"pyproject.toml\").write_text('[project]\\nname = \"pkg-with-tests\"\\n')\n    # Package without tests (but with pyproject.toml)\n    no_tests = packages_dir / \"pkg-without-tests\"\n    no_tests.mkdir()\n    (no_tests / \"pyproject.toml\").write_text('[project]\\nname = \"pkg-without-tests\"\\n')\n    # Package with tests but no pyproject.toml (should be ignored)\n    orphan = packages_dir / \"orphan-pkg\"\n    orphan.mkdir()\n    (orphan / \"tests\").mkdir()\n\n    result = discover_test_packages(packages_dir.parent)\n    assert len(result) == 1\n    assert result[0].name == \"pkg-with-tests\"\n\n\ndef test_discover_returns_sorted_list(packages_dir: Path) -> None:\n    for name in [\"zebra-pkg\", \"alpha-pkg\", \"middle-pkg\"]:\n        pkg = packages_dir / name\n        pkg.mkdir()\n        (pkg / \"tests\").mkdir()\n        (pkg / \"pyproject.toml\").write_text(f'[project]\\nname = \"{name}\"\\n')\n\n    result = discover_test_packages(packages_dir.parent)\n    assert [p.name for p in result] == [\"alpha-pkg\", \"middle-pkg\", \"zebra-pkg\"]\n\n\ndef test_discover_empty_when_no_packages_dir(tmp_path: Path) -> None:\n    result = discover_test_packages(tmp_path)\n    assert result == []\n\n\n# --- Basic CLI tests ---\n\n\ndef test_pytest_help(runner: CliRunner) -> None:\n    result = runner.invoke(cli, [\"pytest\", \"--help\"])\n    assert result.exit_code == 0\n    assert \"--package\" in result.output\n    assert \"-p\" in result.output\n\n\ndef test_pytest_filters_by_package(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    pkg_a = create_pkg(\"pkg-a\")\n    pkg_b = create_pkg(\"pkg-b\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[pkg_a, pkg_b],\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(returncode=0, stdout=\"\", stderr=\"\")\n            runner.invoke(cli, [\"pytest\", \"-p\", \"pkg-a\"])\n\n            # 2 calls: sync + pytest for pkg-a only\n            assert mock_run.call_count == 2\n            call_args = mock_run.call_args_list[1][0][0]\n            assert \"pkg-a\" in str(call_args)\n\n\ndef test_pytest_errors_on_unknown_package(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    real_pkg = create_pkg(\"real-pkg\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[real_pkg],\n    ):\n        result = runner.invoke(cli, [\"pytest\", \"-p\", \"unknown-pkg\"])\n\n        assert result.exit_code == 1\n        assert \"No packages matched: unknown-pkg\" in result.output\n        assert \"Available: real-pkg\" in result.output\n\n\n@pytest.mark.parametrize(\n    \"pkg_names,filters,expected_matches\",\n    [\n        ([\"pkg-foo\", \"pkg-foo-client\", \"pkg-bar\"], [\"foo\"], 2),  # substring\n        ([\"foo\", \"foo-client\"], [\"foo\"], 1),  # exact takes precedence\n        ([\"foo\", \"foo-client\", \"bar\"], [\"foo\", \"bar\"], 2),  # mixed\n    ],\n    ids=[\"substring\", \"exact\", \"mixed\"],\n)\ndef test_pytest_package_filter_matching(\n    runner: CliRunner,\n    create_pkg: Callable[[str], PackageInfo],\n    pkg_names: list[str],\n    filters: list[str],\n    expected_matches: int,\n) -> None:\n    pkgs = [create_pkg(n) for n in pkg_names]\n    filter_args = [arg for f in filters for arg in [\"-p\", f]]\n\n    with patch(\"dev_cli.commands.pytest_cmd.discover_test_packages\", return_value=pkgs):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(returncode=0, stdout=\"\", stderr=\"\")\n            runner.invoke(cli, [\"pytest\", *filter_args])\n            assert mock_run.call_count == expected_matches * 2  # sync + pytest per pkg\n\n\ndef test_pytest_passes_args_through(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    my_pkg = create_pkg(\"my-pkg\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[my_pkg],\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(returncode=0)\n            runner.invoke(cli, [\"pytest\", \"--\", \"-v\", \"--tb=short\", \"-k\", \"test_foo\"])\n\n            call_args = mock_run.call_args[0][0]\n            assert \"-v\" in call_args\n            assert \"--tb=short\" in call_args\n            assert \"-k\" in call_args\n            assert \"test_foo\" in call_args\n\n\ndef test_pytest_continues_on_failure(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\", \"pkg-c\"]]\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.side_effect = [\n                sync_success(),\n                Mock(returncode=1, stdout=\"\", stderr=\"\"),\n                sync_success(),\n                Mock(returncode=0, stdout=\"\", stderr=\"\"),\n                sync_success(),\n                Mock(returncode=0, stdout=\"\", stderr=\"\"),\n            ]\n            result = runner.invoke(cli, [\"pytest\"])\n\n            assert mock_run.call_count == 6\n            assert result.exit_code == 1\n            assert \"FAILED\" in result.output\n            assert \"PASSED\" in result.output\n\n\ndef test_pytest_shows_summary(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\"]]\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(returncode=0)\n            result = runner.invoke(cli, [\"pytest\"])\n\n            assert \"pkg-a\" in result.output\n            assert \"pkg-b\" in result.output\n            assert \"2 packages passed\" in result.output\n\n\n# --- Quiet/verbose mode tests ---\n\n\ndef test_pytest_quiet_mode_hides_streaming_output(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Quiet mode (default) shows compact progress, not full pytest output.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\"]]\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(\n                returncode=0,\n                stdout=\"collected 5 items\\ntest_foo.py::test_one PASSED\\n\",\n                stderr=\"\",\n            )\n            result = runner.invoke(cli, [\"pytest\"])\n\n            assert \"[1/2]\" in result.output\n            assert \"[2/2]\" in result.output\n            # Pytest's \"collected X items\" should NOT appear\n            assert \"collected\" not in result.output\n\n\ndef test_pytest_verbose_shows_streaming_output(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Verbose mode shows full pytest output AND aggregate summary.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\"]]\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(\n                returncode=0,\n                stdout=\"collected 5 items\\ntest_foo.py::test_one PASSED\\n\",\n                stderr=\"\",\n            )\n            result = runner.invoke(cli, [\"pytest\", \"--verbose\"])\n\n            assert \"collected 5 items\" in result.output\n            assert \"test_foo.py::test_one PASSED\" in result.output\n            assert \"2 packages passed\" in result.output\n\n\ndef test_pytest_failure_recap_always_shown(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Failure recap is shown in both quiet and verbose modes.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\"]]\n\n    failure_output = (\n        \"collected 3 items\\n\"\n        \"test_foo.py::test_failing FAILED\\n\"\n        \"FAILURES\\n\"\n        \"test_failing - AssertionError: expected True\\n\"\n    )\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        for verbose_flag in [[], [\"--verbose\"]]:\n            with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n                mock_run.side_effect = [\n                    sync_success(),\n                    Mock(returncode=0, stdout=\"all tests passed\\n\", stderr=\"\"),\n                    sync_success(),\n                    Mock(returncode=1, stdout=failure_output, stderr=\"\"),\n                ]\n                result = runner.invoke(cli, [\"pytest\", *verbose_flag])\n\n                assert \"FAILURES\" in result.output or \"FAILED\" in result.output\n                assert \"pkg-b\" in result.output\n                assert \"AssertionError\" in result.output\n\n\n# --- Parallel execution tests ---\n\n\ndef test_pytest_runs_parallel_by_default(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Tests run in parallel by default - verified by concurrent execution.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\", \"pkg-c\"]]\n\n    started_count = 0\n    started_lock = threading.Lock()\n    all_started = threading.Event()\n    call_order: list[str] = []\n\n    def mock_subprocess_run(\n        cmd: list[str], env: object = None, **kwargs: object\n    ) -> subprocess.CompletedProcess[str]:\n        nonlocal started_count\n\n        if is_sync_call(cmd):\n            return subprocess.CompletedProcess(\n                args=cmd, returncode=0, stdout=\"\", stderr=\"\"\n            )\n\n        pkg_name = \"unknown\"\n        for i, arg in enumerate(cmd):\n            if arg == \"--directory\" and i + 1 < len(cmd):\n                pkg_name = Path(cmd[i + 1]).name\n                break\n\n        with started_lock:\n            started_count += 1\n            call_order.append(f\"start:{pkg_name}\")\n            if started_count >= 3:\n                all_started.set()\n\n        all_started.wait(timeout=2.0)\n\n        with started_lock:\n            call_order.append(f\"end:{pkg_name}\")\n\n        return subprocess.CompletedProcess(args=cmd, returncode=0, stdout=\"\", stderr=\"\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\n            \"dev_cli.commands.pytest_cmd._run_tracked\",\n            side_effect=mock_subprocess_run,\n        ):\n            result = runner.invoke(cli, [\"pytest\"])\n\n            # Verify parallel: at least 2 starts before any end\n            start_indices = [\n                i for i, x in enumerate(call_order) if x.startswith(\"start:\")\n            ]\n            end_indices = [i for i, x in enumerate(call_order) if x.startswith(\"end:\")]\n            assert len(start_indices) >= 2\n            assert max(start_indices[:2]) < min(end_indices)\n            assert result.exit_code == 0\n\n\ndef test_pytest_parallel_one_runs_sequentially(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"--parallel 1 runs packages one at a time.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\", \"pkg-c\"]]\n\n    call_order: list[str] = []\n    lock = threading.Lock()\n    active_count = 0\n    max_concurrent = 0\n\n    def mock_subprocess_run(\n        cmd: list[str], env: object = None, **kwargs: object\n    ) -> subprocess.CompletedProcess[str]:\n        nonlocal active_count, max_concurrent\n\n        if is_sync_call(cmd):\n            return subprocess.CompletedProcess(\n                args=cmd, returncode=0, stdout=\"\", stderr=\"\"\n            )\n\n        pkg_name = \"unknown\"\n        for i, arg in enumerate(cmd):\n            if arg == \"--directory\" and i + 1 < len(cmd):\n                pkg_name = Path(cmd[i + 1]).name\n                break\n\n        with lock:\n            active_count += 1\n            max_concurrent = max(max_concurrent, active_count)\n            call_order.append(f\"start:{pkg_name}\")\n\n        time.sleep(0.01)\n\n        with lock:\n            call_order.append(f\"end:{pkg_name}\")\n            active_count -= 1\n\n        return subprocess.CompletedProcess(args=cmd, returncode=0, stdout=\"\", stderr=\"\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\n            \"dev_cli.commands.pytest_cmd._run_tracked\",\n            side_effect=mock_subprocess_run,\n        ):\n            result = runner.invoke(cli, [\"pytest\", \"--parallel\", \"1\"])\n\n            assert max_concurrent == 1\n            # Verify strict ordering: start, end, start, end...\n            for i in range(0, len(call_order) - 1, 2):\n                assert call_order[i].startswith(\"start:\")\n                assert call_order[i + 1].startswith(\"end:\")\n            assert result.exit_code == 0\n\n\ndef test_pytest_parallel_handles_mixed_results(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Parallel execution handles mixed passed/failed results correctly.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-fail\", \"pkg-pass-1\", \"pkg-pass-2\"]]\n\n    completed: list[str] = []\n    lock = threading.Lock()\n\n    def mock_subprocess_run(\n        cmd: list[str], env: object = None, **kwargs: object\n    ) -> subprocess.CompletedProcess[str]:\n        if is_sync_call(cmd):\n            return subprocess.CompletedProcess(\n                args=cmd, returncode=0, stdout=\"\", stderr=\"\"\n            )\n\n        pkg_name = \"unknown\"\n        for i, arg in enumerate(cmd):\n            if arg == \"--directory\" and i + 1 < len(cmd):\n                pkg_name = Path(cmd[i + 1]).name\n                break\n\n        with lock:\n            completed.append(pkg_name)\n\n        if pkg_name == \"pkg-fail\":\n            return subprocess.CompletedProcess(\n                args=cmd,\n                returncode=1,\n                stdout=\"FAILED test_example.py::test_bad\\nAssertionError: bad\\n\",\n                stderr=\"\",\n            )\n        return subprocess.CompletedProcess(args=cmd, returncode=0, stdout=\"\", stderr=\"\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\n            \"dev_cli.commands.pytest_cmd._run_tracked\",\n            side_effect=mock_subprocess_run,\n        ):\n            result = runner.invoke(cli, [\"pytest\"])\n\n            assert set(completed) == {\"pkg-fail\", \"pkg-pass-1\", \"pkg-pass-2\"}\n            assert result.exit_code == 1\n            assert \"2 passed\" in result.output\n            assert \"1 failed\" in result.output\n            assert \"packages\" in result.output\n\n\n# --- extract_failures_section tests ---\n\n\ndef test_extract_failures_returns_failures_block() -> None:\n    pytest_output = \"\"\"\n============================= test session starts ==============================\ncollected 5 items\n\ntest_example.py::test_one PASSED\ntest_example.py::test_two FAILED\n\n=================================== FAILURES ===================================\n_________________________ test_two _________________________\n\n    def test_two():\n>       assert False\nE       AssertionError\n\ntest_example.py:10: AssertionError\n=========================== short test summary info ============================\nFAILED test_example.py::test_two - AssertionError\n============================== 1 failed, 1 passed ==============================\n\"\"\"\n    result = extract_failures_section(pytest_output)\n    assert result is not None\n    assert \"FAILURES\" in result\n    assert \"test_two\" in result\n    assert \"short test summary\" not in result\n\n\ndef test_extract_failures_returns_none_for_passing() -> None:\n    pytest_output = \"\"\"\n============================= test session starts ==============================\ncollected 2 items\n\ntest_example.py::test_one PASSED\n\n============================== 2 passed ==============================\n\"\"\"\n    result = extract_failures_section(pytest_output)\n    assert result is None\n\n\ndef test_extract_failures_excludes_warnings() -> None:\n    pytest_output = \"\"\"\n=================================== FAILURES ===================================\n_________________________ test_failing _________________________\n\nAssertionError\n=============================== warnings summary ===============================\ntest_example.py:3: DeprecationWarning: deprecated\n=========================== short test summary info ============================\n\"\"\"\n    result = extract_failures_section(pytest_output)\n    assert result is not None\n    assert \"FAILURES\" in result\n    assert \"DeprecationWarning\" not in result\n\n\ndef test_extract_failures_handles_multiple() -> None:\n    pytest_output = \"\"\"\n=================================== FAILURES ===================================\n_________________________ test_one _________________________\n\nAssertionError: first failure\n_________________________ test_two _________________________\n\nAssertionError: second failure\n=========================== short test summary info ============================\n\"\"\"\n    result = extract_failures_section(pytest_output)\n    assert result is not None\n    assert \"first failure\" in result\n    assert \"second failure\" in result\n\n\n# --- extract_failed_test_names tests ---\n\n\ndef test_extract_failed_names_from_short_summary() -> None:\n    pytest_output = \"\"\"\n=========================== short test summary info ============================\nFAILED tests/test_example.py::test_one - AssertionError\nFAILED tests/test_example.py::test_two - ValueError\n============================== 2 failed ==============================\n\"\"\"\n    result = extract_failed_test_names(pytest_output)\n    assert len(result) == 2\n    test_names = [name for name, _ in result]\n    reasons = [reason for _, reason in result]\n    assert \"tests/test_example.py::test_one\" in test_names\n    assert \"AssertionError\" in reasons\n    assert \"ValueError\" in reasons\n\n\ndef test_extract_failed_names_fallback_to_headers() -> None:\n    pytest_output = \"\"\"\n=================================== FAILURES ===================================\n_________________________ test_one _________________________\n\nAssertionError\n_________________________ test_two _________________________\n\nValueError\n============================== 2 failed ==============================\n\"\"\"\n    result = extract_failed_test_names(pytest_output)\n    assert len(result) == 2\n    test_names = [name for name, _ in result]\n    assert \"test_one\" in test_names\n    assert \"test_two\" in test_names\n\n\ndef test_extract_failed_names_empty_for_passing() -> None:\n    pytest_output = \"\"\"\n============================= test session starts ==============================\ntest_example.py::test_one PASSED\n============================== 2 passed ==============================\n\"\"\"\n    result = extract_failed_test_names(pytest_output)\n    assert result == []\n\n\ndef test_extract_failed_names_ignores_separator_lines() -> None:\n    \"\"\"Separator lines like '_ _ _ _ _' should not match as test names.\"\"\"\n    pytest_output = \"\"\"\n=================================== FAILURES ===================================\n_________________________ test_one _________________________\n\nAssertionError\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\n\nDuring handling of the above exception...\n_________________________ test_two _________________________\n\nAssertionError\n\"\"\"\n    result = extract_failed_test_names(pytest_output)\n    test_names = [name for name, _ in result]\n    assert len(result) == 2\n    assert all(\"_ _ _\" not in name for name in test_names)\n\n\ndef test_extract_failed_names_handles_ansi_codes() -> None:\n    pytest_output = \"\"\"\n=========================== short test summary info ============================\n\\x1b[31mFAILED\\x1b[0m tests/test_example.py::test_one - AssertionError\n============================== 1 failed ==============================\n\"\"\"\n    result = extract_failed_test_names(pytest_output)\n    assert len(result) == 1\n    test_name, reason = result[0]\n    assert test_name == \"tests/test_example.py::test_one\"\n    assert reason == \"AssertionError\"\n\n\n# --- Output ordering tests ---\n\n\ndef test_pytest_summary_appears_last(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\"]]\n\n    failure_output = (\n        \"test_foo.py::test_failing FAILED\\n\"\n        \"=================================== FAILURES ===================================\\n\"\n        \"_________________________ test_failing _________________________\\n\"\n        \"AssertionError\\n\"\n        \"=========================== short test summary info ============================\\n\"\n    )\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.side_effect = [\n                sync_success(),\n                Mock(returncode=0, stdout=\"all tests passed\\n\", stderr=\"\"),\n                sync_success(),\n                Mock(returncode=1, stdout=failure_output, stderr=\"\"),\n            ]\n            result = runner.invoke(cli, [\"pytest\"])\n\n            failures_pos = result.output.find(\"FAILURES\")\n            summary_pos = result.output.find(\"Test Summary\")\n            failed_tests_pos = result.output.find(\"Failed tests:\")\n            assert failures_pos < summary_pos < failed_tests_pos\n\n\ndef test_pytest_extracts_failures_not_full_output(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    # Use two packages so single-package auto-verbose doesn't kick in\n    pkg_a = create_pkg(\"pkg-a\")\n    pkg_b = create_pkg(\"pkg-b\")\n\n    failure_output = (\n        \"============================= test session starts ==============================\\n\"\n        \"platform linux -- Python 3.11.0\\n\"\n        \"plugins: asyncio-0.21.0\\n\"\n        \"collected 1 item\\n\"\n        \"\\n\"\n        \"=================================== FAILURES ===================================\\n\"\n        \"_________________________ test_failing _________________________\\n\"\n        \"AssertionError\\n\"\n        \"=============================== warnings summary ===============================\\n\"\n        \"DeprecationWarning: old API\\n\"\n        \"=========================== short test summary info ============================\\n\"\n    )\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[pkg_a, pkg_b],\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.side_effect = [\n                sync_success(),\n                Mock(returncode=1, stdout=failure_output, stderr=\"\"),\n                sync_success(),\n                Mock(returncode=0, stdout=\"\", stderr=\"\"),\n            ]\n            result = runner.invoke(cli, [\"pytest\"])\n\n            assert \"AssertionError\" in result.output\n            # Should NOT include warnings or session info\n            assert \"DeprecationWarning\" not in result.output\n            assert \"platform linux\" not in result.output\n\n\ndef test_pytest_shows_failed_test_names_at_end(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    pkg_a = create_pkg(\"pkg-a\")\n\n    failure_output = (\n        \"=================================== FAILURES ===================================\\n\"\n        \"_________________________ test_failing _________________________\\n\"\n        \"AssertionError\\n\"\n        \"=========================== short test summary info ============================\\n\"\n        \"FAILED tests/test_example.py::test_failing - AssertionError\\n\"\n    )\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[pkg_a],\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(returncode=1, stdout=failure_output, stderr=\"\")\n            result = runner.invoke(cli, [\"pytest\"])\n\n            summary_pos = result.output.find(\"Test Summary\")\n            failed_tests_pos = result.output.find(\"Failed tests:\")\n            assert failed_tests_pos > summary_pos\n            assert \"tests/test_example.py::test_failing\" in result.output\n            assert \"[pkg-a]\" in result.output\n\n\n# --- Rich progress display tests ---\n\n\ndef test_render_progress_table_pending(\n    packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    from rich.spinner import Spinner\n\n    pkg = create_pkg(\"pkg-a\")\n    results: dict[str, dict[str, bool | str | float | None]] = {}\n    start_times: dict[str, float] = {}\n    spinners: dict[str, Spinner] = {}\n\n    table = _render_progress_table([pkg], results, start_times, spinners)\n    assert table.row_count == 1  # just the name\n\n\ndef test_render_progress_table_running(\n    packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    from rich.spinner import Spinner\n\n    pkg = create_pkg(\"pkg-a\")\n    results: dict[str, dict[str, bool | str | float | None]] = {}\n    start_times = {\"pkg-a\": time.time() - 5.0}\n    spinners = {\"pkg-a\": Spinner(\"dots\", style=\"yellow\")}\n\n    table = _render_progress_table([pkg], results, start_times, spinners)\n    assert table.row_count == 2  # name + spinner\n\n\ndef test_render_progress_table_completed(\n    packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    from rich.spinner import Spinner\n\n    pkg_a = create_pkg(\"pkg-a\")\n    pkg_b = create_pkg(\"pkg-b\")\n    results = {\n        \"pkg-a\": {\"success\": True, \"duration\": 2.5, \"stdout\": \"\", \"stderr\": \"\"},\n        \"pkg-b\": {\"success\": False, \"duration\": 3.0, \"stdout\": \"\", \"stderr\": \"\"},\n    }\n    start_times: dict[str, float] = {}\n    spinners: dict[str, Spinner] = {}\n\n    table = _render_progress_table([pkg_a, pkg_b], results, start_times, spinners)\n    assert table.row_count == 4  # 2 packages x (name + detail)\n\n\ndef test_run_tests_with_rich_progress_returns_results(\n    packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    pkg = create_pkg(\"pkg-a\")\n\n    with patch(\"dev_cli.commands.pytest_cmd.run_package_tests\") as mock_run:\n        mock_run.return_value = {\n            \"success\": True,\n            \"stdout\": \"tests passed\",\n            \"stderr\": \"\",\n            \"duration\": 1.5,\n        }\n\n        results = run_tests_with_rich_progress([pkg], (), max_workers=1)\n\n        assert \"pkg-a\" in results\n        assert results[\"pkg-a\"][\"success\"] is True\n\n\ndef test_run_tests_with_rich_progress_multiple(\n    packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    packages = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\", \"pkg-c\"]]\n    call_count = 0\n\n    def mock_run(\n        pkg: PackageInfo, pytest_args: tuple[str, ...]\n    ) -> dict[str, bool | str | float | None]:\n        nonlocal call_count\n        call_count += 1\n        return {\n            \"success\": pkg.name != \"pkg-b\",\n            \"stdout\": \"output\",\n            \"stderr\": \"\",\n            \"duration\": 1.0,\n        }\n\n    with patch(\"dev_cli.commands.pytest_cmd.run_package_tests\", side_effect=mock_run):\n        results = run_tests_with_rich_progress(packages, (), max_workers=3)\n\n        assert call_count == 3\n        assert results[\"pkg-a\"][\"success\"] is True\n        assert results[\"pkg-b\"][\"success\"] is False\n        assert results[\"pkg-c\"][\"success\"] is True\n\n\n# --- TTY detection tests ---\n\n\ndef test_pytest_verbose_does_not_use_rich(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"--verbose uses simple output, not rich progress.\"\"\"\n    pkg_a = create_pkg(\"pkg-a\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[pkg_a],\n    ):\n        with patch(\n            \"dev_cli.commands.pytest_cmd.run_tests_with_rich_progress\"\n        ) as mock_rich:\n            with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_subprocess:\n                mock_subprocess.return_value = Mock(\n                    returncode=0, stdout=\"passed\\n\", stderr=\"\"\n                )\n                runner.invoke(cli, [\"pytest\", \"--verbose\"])\n\n                mock_rich.assert_not_called()\n                mock_subprocess.assert_called()\n\n\ndef test_pytest_skips_rich_when_not_tty(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Rich progress is NOT used when stdout is not a TTY.\"\"\"\n    pkg_a = create_pkg(\"pkg-a\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[pkg_a],\n    ):\n        with patch(\n            \"dev_cli.commands.pytest_cmd.run_tests_with_rich_progress\"\n        ) as mock_rich:\n            with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_subprocess:\n                mock_subprocess.return_value = Mock(\n                    returncode=0, stdout=\"passed\", stderr=\"\"\n                )\n                runner.invoke(cli, [\"pytest\"])\n\n                mock_rich.assert_not_called()\n                mock_subprocess.assert_called()\n\n\n# --- extract_test_counts tests ---\n\n\ndef test_extract_test_counts_passed_only() -> None:\n    output = \"============================== 42 passed in 3.2s ==============================\"\n    assert extract_test_counts(output) == \"42 passed\"\n\n\ndef test_extract_test_counts_mixed() -> None:\n    output = \"============================== 1 failed, 42 passed in 3.2s ==============================\"\n    assert extract_test_counts(output) == \"1 failed, 42 passed\"\n\n\ndef test_extract_test_counts_with_skipped() -> None:\n    output = \"============================== 10 passed, 2 skipped in 1.5s ==============================\"\n    assert extract_test_counts(output) == \"10 passed, 2 skipped\"\n\n\ndef test_extract_test_counts_no_summary() -> None:\n    assert extract_test_counts(\"no summary here\\n\") is None\n\n\ndef test_extract_test_counts_with_ansi() -> None:\n    output = \"\\x1b[32m============================== 5 passed in 0.5s ==============================\\x1b[0m\"\n    assert extract_test_counts(output) == \"5 passed\"\n\n\ndef test_extract_test_counts_without_timing() -> None:\n    output = \"============================== 42 passed ==============================\"\n    assert extract_test_counts(output) == \"42 passed\"\n\n\n# --- Single-package auto-verbose tests ---\n\n\ndef test_single_package_shows_full_output(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Single package runs show full pytest output without -v flag.\"\"\"\n    pkg_a = create_pkg(\"pkg-a\")\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=[pkg_a],\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.return_value = Mock(\n                returncode=0,\n                stdout=\"collected 5 items\\ntest_foo.py::test_one PASSED\\n============================== 5 passed in 1.0s ==============================\\n\",\n                stderr=\"\",\n            )\n            result = runner.invoke(cli, [\"pytest\", \"-p\", \"pkg-a\"])\n\n            # Should show full pytest output\n            assert \"collected 5 items\" in result.output\n            assert \"test_foo.py::test_one PASSED\" in result.output\n            # Should still show pass summary\n            assert \"1 package passed\" in result.output\n\n\n# --- Test counts in compact output ---\n\n\ndef test_compact_output_shows_test_counts(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Non-TTY compact progress lines include test counts.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\"]]\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.side_effect = [\n                sync_success(),\n                Mock(\n                    returncode=0,\n                    stdout=\"============================== 10 passed in 1.0s ==============================\\n\",\n                    stderr=\"\",\n                ),\n                sync_success(),\n                Mock(\n                    returncode=0,\n                    stdout=\"============================== 5 passed in 0.5s ==============================\\n\",\n                    stderr=\"\",\n                ),\n            ]\n            result = runner.invoke(cli, [\"pytest\"])\n\n            assert (\n                \"PASSED (10 passed)\" in result.output\n                or \"PASSED (5 passed)\" in result.output\n            )\n            assert \"15 tests\" in result.output\n\n\ndef test_summary_table_shows_test_counts(\n    runner: CliRunner, packages_dir: Path, create_pkg: Callable[[str], PackageInfo]\n) -> None:\n    \"\"\"Summary table includes test counts per package.\"\"\"\n    pkgs = [create_pkg(name) for name in [\"pkg-a\", \"pkg-b\"]]\n\n    with patch(\n        \"dev_cli.commands.pytest_cmd.discover_test_packages\",\n        return_value=pkgs,\n    ):\n        with patch(\"dev_cli.commands.pytest_cmd._run_tracked\") as mock_run:\n            mock_run.side_effect = [\n                sync_success(),\n                Mock(\n                    returncode=0,\n                    stdout=\"============================== 10 passed in 1.0s ==============================\\n\",\n                    stderr=\"\",\n                ),\n                sync_success(),\n                Mock(\n                    returncode=1,\n                    stdout=\"=================================== FAILURES ===================================\\n_________________________ test_bad _________________________\\nAssertionError\\n============================== 1 failed, 4 passed in 0.5s ==============================\\n\",\n                    stderr=\"\",\n                ),\n            ]\n            result = runner.invoke(cli, [\"pytest\"])\n\n            assert \"PASSED (10 passed)\" in result.output\n            assert \"FAILED (1 failed, 4 passed)\" in result.output\n            assert \"15 tests\" in result.output\n"
  }
]