Skip to content

feat(vended-tools): add swarm tool in Python [replica of harness-sdk PR 4922] - #33

Open
poshinchen wants to merge 10 commits into
base-pr-4922from
strandly-v3-pr-4922
Open

poshinchen wants to merge 10 commits into
base-pr-4922from
strandly-v3-pr-4922

Conversation

@poshinchen

Copy link
Copy Markdown
Owner

Replica of https://github.com/strands-agents/harness-sdk/pull/4922 (backticked: no cross-reference). Below: the v4-reason optimized review and a sanitized copy of live Strandly's actual comments.

@poshinchen poshinchen left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strandly v4-reason (optimized) review — run af1746, Opus 4.8

Assessment: comment

Assessment: Comment

Solid, well-tested addition. The swarm tool is a clean shim over the SDK Swarm with a thoughtful authority-mode schema, depth-limiting, and good test coverage (schema building, spec resolution, limits forwarding, depth exhaustion, and an end-to-end child-stop case).

Review Categories
  • Consistency: Preset ordering differs between the schema enum (sorted) and the description (insertion order); mcp_servers is the only axis not normalized from None to a default.
  • Side effects: _resolve_specs mutates the model-supplied input dicts in place.
  • Robustness: Non-completion errors embed the full (potentially large) SwarmResult in the exception message.
  • API process: Confirm api/needs-review sign-off for the new public make_swarm surface.

Nice work on the depth-propagation design and the comprehensive, shape-level test assertions.

def _build_description(base: str, presets: Mapping[str, Preset]) -> str:
"""Append preset information to the tool description so the model knows about them."""
if not presets:
return base

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important
Issue: _build_agent_item_schema lists agent_type enum values via sorted(presets), but _build_description lists the available roles via presets.items() (insertion order). When these two orderings differ, the enum shown in the schema and the human-readable role list in the description are presented in a different order, which is confusing for the model and for anyone debugging the tool spec.
Suggestion: Use a single, consistent ordering for both. For example, sort in _build_description as well:

Suggested change
return base
roles = "\n".join(f"- {name}: {presets[name].description or name}" for name in sorted(presets))

default_preset = next(iter(presets))

if isinstance(mcp_servers, Choice):
if not mcp_servers.options:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important
Issue: mcp_servers is the only axis not normalized to a default when None is passed. instructions defaults to Open() and model defaults to Inherit(), but mcp_servers is left as None and passed through to _resolve_specs/_build_agent_item_schema as-is. This asymmetry means the default swarm instance has mcp_servers=None while the others have concrete axis objects, which is surprising and forces downstream None handling. Was leaving it None intentional (i.e. "no MCP axis at all" differs from Inherit())?
Suggestion: If None and Inherit() are meant to behave identically, normalize mcp_servers here (mcp_servers = mcp_servers if mcp_servers is not None else Inherit()) for consistency; otherwise document the distinction in the docstring.

raise ValueError(f"Agent at index {index} must be a dict, got {type(entry).__name__}")
name = entry.get("name")
if not isinstance(name, str) or not name.strip():
raise ValueError(f"Agent at index {index} must have a non-empty 'name' string")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important
Issue: _resolve_specs mutates the caller's input in place via entry["name"] = name. The agents list comes directly from the model's tool input (tool_context's parsed arguments). Mutating caller-supplied dicts is a side effect that can surprise callers and complicate debugging/logging of the original request.
Suggestion: Build a resolved copy instead of mutating the input, e.g. pass {**entry, "name": name} into _resolve_spec rather than reassigning entry["name"].

f"Swarm stopped before completing (status={result.status.value}, "
f"{result.execution_count} iterations). Partial output:\n{result}"
)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion
Issue: On non-completion the error embeds the full result object via f-string (Partial output:\n{result}). Depending on SwarmResult.__str__, this could be very large (every node's full output) and is raised as a RuntimeError message, which may be logged/surfaced verbatim to the model and users.
Suggestion: Consider bounding or summarizing the partial output included in the exception message, similar to how task[:120] is truncated for the start log.

@poshinchen

Copy link
Copy Markdown
Owner Author

Live Strandly's actual comments on the original PR 4922 (sanitized copy: mentions escaped, links backticked)

Review (CHANGES_REQUESTED):

Changes requested — the model can't actually use this tool as shipped: the agents schema is an untyped array of object (presets/Choice/Fixed never reach the model — tool_spec is byte-identical with or without them), and a swarm that hits max_handoffs/max_iterations/node_timeout comes back as a success tool result (often ""). Both have straightforward fixes and the sibling make_subagent (strands-agents#4841) already shows the pattern. New public API with no needs-api-review label — this probably wants the API meeting rather than a solo approve.

Verified at 061d8a4
  • hatch test tests/strands/vended_tools/test_swarm.py → 23 passed; ruff check clean.
  • swarm.tool_spec == make_swarm(presets=…, instructions=Choice([…]), model=Fixed(…)).tool_spec → True.
  • Fake Swarm sleeping 1 s inside the tool: a 100 ms heartbeat task on the same loop ticked 0 times; Status.FAILED with no node text → tool returned ''.
  • Docs example: child built from the researcher preset → tool_names == [] (parent has no http_request/web_fetch); {"name": "c", "instructions": "You are a writer."} under instructions=Choice → agent_type=None, child gets all parent tools.
  • End-to-end Agent(model=fake, tools=[swarm]) with agent.cancel() mid-swarm: swarm ran every remaining node; parent saw the cancel only after the tool returned.
  • Passes: API/DevX, correctness/tests, adversarial (the first reviewer + adversarial spawns timed out and were re-run time-boxed). Repro scripts/outputs uploaded as artifacts.
Questions (non-blocking)
  1. Is v1 a model-authored team (model writes roles) or developer-curated (presets)? Today only the former is possible, and blind.
  2. The superseded strands_tools swarm gave children no tools unless named; this inverts to "inherit everything". Deliberate?
  3. Repetition detection off by default (0/0) vs 8/3 in the old tool — is max_handoffs=20 the only loop guard you want for a team the parent can't steer?
  4. Should execution_timeout's docstring say it's only checked between nodes? A single node can run to node_timeout regardless.
Reading order
  1. swarm.py:97-138 (model-facing signature, sync call, result mapping) 2. spec.py:224-258 (what the tool relies on) 3. test_swarm.py:217-307 4. docs section.
Appendix — non-blocking (9)
  • 🟡 swarm.py:94-95 mcp_servers hard-wired to Inherit(), no parameter — make_swarm(tools=Fixed(["calculator"])) still hands children every MCP server's tools. Sibling exposes mcp_servers= with the same default.
  • 🟡 swarm.py:84-95 no Choice validation: make_swarm(tools=Choice(["calculator"])) (not multiple=True) is accepted, and when the model omits tools, spec.py:186-190 returns None → inherit all. A restriction silently inverts. Sibling raises here.
  • 🟡 swarm.py:179-183 .strip() is used

Review (COMMENTED):

No blockers from me — all six findings from the first round are fixed and hold up end-to-end at 9fa2f6d. Not approving solo: new public API (make_swarm, swarm, DEFAULT_MAX_DEPTH) still has no needs-api-review / completed-api-review label, so this should go through API review once it leaves draft.

Status of previous findings:

  • 🔴 untyped agents schema → fixed (_build_agent_item_schema renders agent_type enum, Open/Choice axes, additionalProperties: false, minItems/maxItems; presets appended to description).
  • 🔴 FAILED status hidden → fixed (result.status != COMPLETED raises; model now gets status: error with partial output).
  • 🟡 sync call blocking the loop → fixed (await sdk_swarm.invoke_async); cancel still un-wired, noted in a code comment.
  • 🟡 no depth guard / inherited coordination tools → fixed (max_depth + strands.swarm_depth state key; handoff_to_agent popped from children).
  • 🟡 docs example toolless child → fixed (parent now has http_request, web_fetch).
  • 🟡 tests → fixed (schema, FAILED, depth, exclusion, builder/nodes all covered; 28 pass).
Verified at 9fa2f6d (fix commit 2d4b1aa)
  • hatch test tests/strands/vended_tools/test_swarm.py → 28 passed; ruff clean.
  • End-to-end with mocked models (followup_verify.py artifact): success → "a: child answer"; max_iterations=1 + handoff → status: error, "Swarm stopped before completing (status=failed, 1 iterations; …) Partial output: …"; max_depth=1 → child's nested call rejected, child completes; Swarm(nodes=[Agent(tools=[swarm])]) → completed (was a hard error before); 0.5 s swarm → 25 heartbeat ticks on the parent loop (was 0).
  • Re-checked the six resolved threads against the new diff; each fix does what it says. No fan-out this round — delta is fixes to my own findings.
Appendix — non-blocking (5)
  • 🟡 swarm.py:274 required = ["name"] — with the default instructions=Open() and no presets, a model that omits instructions gets a child with system_prompt="" and no error. Consider adding instructions to required when the axis is Open and there's no default_preset.
  • 🟡 still open from round 1: mcp_servers hard-wired Inherit() (swarm.py:106); no Choice validation (non-multiple tools=Choice with tools omitted → inherit all); "a"/"a " both accepted as distinct names (swarm.py:239-243, verified again) — strip before dedupe.
  • ⚪ docs: instructions=Choice([...]) alongside presets — picking a Choice instruction skips the preset (spec.py:238-245), so the child gets all parent tools rather than the preset's. Now visible to the model, but a reader of the example won't expect it; one sentence would do.
  • ⚪ swarm.py:192 cancel caveat is a code comment; worth a line in the make_swarm docstring so callers of agent.cancel() know a running swarm finishes anyway.
  • ⚪ swarm.py:195 Status.INTERRUPTED also

Review (COMMENTED):

One should-fix in the new delta, otherwise no blockers. The new mcp_servers axis is resolved but never rendered into the agent schema, so mcp_servers=Choice([...]) is a knob the model can't turn — and with multiple=False it silently becomes "inherit all". api/needs-review is now on, good.

Delta since my last pass (9fa2f6d → eaa402a): mcp_servers axis, Swarm defaults as source of truth (None → not forwarded), instructions required under Open() with no presets, name stripped before dedupe, depth message no longer quotes a possibly-wrong max_depth, cancel caveat in the docstring, "Available roles" wording. All verified; the name strip does not leak into the stored toolUse input (pydantic copies).

Verified at eaa402a
  • hatch test tests/strands/vended_tools/test_swarm.py → 28 passed; ruff clean.
  • make_swarm(mcp_servers=Choice(["docs","github"])) → "mcp_servers" in items.properties == False, additionalProperties: false; resolved spec.mcp_servers = None (non-multiple) / ['docs','github'] (multiple) / ['docs'] (Fixed).
  • End-to-end with {"name": " spaced "} → child node spaced, history still ' spaced ', result success.
  • Reviewed solo — delta is small and mostly fixes to my own appendix items.
Appendix — non-blocking (3)
  • ⚪ test_swarm.py — the new mcp_servers parameter has no test (only the fixture's Inherit()); one Choice(multiple=True) + one Fixed case would pin it.
  • ⚪ Round-1 leftover: tools=Choice([...]) without multiple=True has the same omitted-→-inherit-all shape as the inline finding; the sibling rejects non-multiple Choice for list axes at construction, which would close both at once.
  • ⚪ Docs don't mention mcp_servers; fine for the example, but the parameter table/docstring is the only place it's discoverable.

Review (APPROVED):

Approve — round-3 finding fixed at cc7bfbe: mcp_servers=Choice(...) is now rendered into the agent schema and resolves from model input, and non-multiple / empty Choice on tools/mcp_servers is rejected at make_swarm time (closes the inherit-all inversion for both axes). Nothing else changed. api/needs-review is on, so the API pass still happens on its own track.

One housekeeping item before merge: CI "Python / Lint" is red — ruff check reports 2× E501 (121 > 120): swarm.py:115 and test_swarm.py:125. Linter-owned, so no inline comment; approval stands on the substance, but it can't merge until those two lines wrap.

Verified at cc7bfbe
  • hatch test tests/strands/vended_tools/test_swarm.py → 32 passed.
  • hatch fmt --check src/strands/vended_tools/swarm tests/strands/vended_tools/test_swarm.py → formatter clean, ruff check fails: 2 × E501 (swarm.py:115, test_swarm.py:125). My earlier "ruff clean" in this review was wrong — I'd read the formatter's pass line and missed the linter output; corrected here.
  • make_swarm(mcp_servers=Choice(["docs","github"], multiple=True)) → items.properties.mcp_servers = {"type":"array","items":{"enum":["docs","github"]}}; model sending ["docs"] → spec.mcp_servers == ['docs'].
  • Choice(["a"]) / Choice([], multiple=True) on either list axis → ValueError with a message that names the alternative (Fixed([]) / Inherit()).
  • mcp_servers=None passes through to _resolve_spec, which defaults it to Inherit() (spec.py:226) — same behaviour as before.
  • New tests cover schema rendering, rejection cases, and _build_agent_item_schema with the new axis.

Review (CHANGES_REQUESTED):

Changes requested — one 🔴 introduced by the streaming change. sdk_swarm.stream_async(task, invocation_state=tool_context.invocation_state) hands the children the parent's live invocation_state, so they share the parent's request_state. A child that calls the inherited stop tool halts the parent's loop: the parent returns '' with stop_reason=tool_use and never gets to use the swarm result. Repro and a verified one-line fix inline — note a shallow copy (what make_subagent does) is not enough; request_state has to be dropped.

Everything else in the delta since cc7bfbe checks out: swarm events now stream to the parent (multiagent_node_start/stream/stop forwarded; final result held back as the tool result), entry-point/handoff info in the description and agents schema, constants folded into swarm.py, lint clean.

Verified at cdd43ea
  • hatch test tests/strands/vended_tools/test_swarm.py → 32 passed; hatch fmt --linter --check on the swarm files → All checks passed.
  • Streaming: Agent(tools=[swarm]) with mocked models → 14 tool_stream_events on the parent stream during the swarm (multiagent_node_start, multiagent_node_stream, multiagent_node_stop); tool result success: "child_a: child done".
  • Leak repro (round5-v5b.py): parent Agent(tools=[swarm, stop]), child model calls stop then answers → parent stop_reason=tool_use, final text '', parent model consumed 1 of 2 responses, caller's invocation_state["request_state"] == {'stop_event_loop': True}.
  • Fix variants (round5-v5c.py): as-is → broken; {**invocation_state} → still broken (same request_state dict object); copy minus request_state → stop_reason=end_turn, parent summarises.
  • Reviewed solo — delta is small; mkmeral's threads are design-level and already answered by the author.
Appendix — non-blocking (3)
  • 🟡 Cross-PR: make_subagent in feat: add vended subagent tool strands-agents/harness-sdk#4841 uses child_state = {**invocation_state} (subagent.py:346) — the shallow copy shares request_state the same way, so a subagent calling stop should halt its parent too. Not verified there; flagging for whoever lands second.
  • ⚪ swarm.py:39-40 model-facing text: "…tools (drawn from your own) Your handoff_to_agent tool is never passed on…" — missing period, and "your handoff_to_agent tool" presumes the caller has one (only true when the caller is itself a swarm node). Suggest: "…drawn from your own. Agents get their own handoff tool; your tools are otherwise passed through."
  • ⚪ Beyond request_state, the child cycles also overwrite agent, messages, model, tool_config, event_loop_cycle_id in the shared dict mid-tool. The parent re-sets them on its next cycle so I couldn't show a user-visible effect, but it's the same root cause — the copy fixes it too.

Review (APPROVED):

From my side, yes — approving to clear my changes-requested. The request_state leak is fixed at e5bebed and the round-5 repro now passes (child calls stop → parent stop_reason=end_turn, parent's own summary returned); the description wording nit is fixed too. No open findings from me.

Not yet mergeable on process, which is yours/the maintainers' call, not mine:

  • check-api-review-label is failing because api/needs-review is still on — the API review needs to happen and the label flipped before that check goes green.
  • reviewDecision is REVIEW_REQUIRED — needs a maintainer approval (mkmeral has only commented so far).
  • The rest of CI was still in progress when I looked; worth a glance once it settles.
Verified at e5bebed
  • hatch test tests/strands/vended_tools/test_swarm.py → 33 passed (new test covers the child-stop case); hatch fmt --linter --check on the swarm files → All checks passed.
  • Round-5 repro re-run: parent stop_reason: end_turn, final text: 'PARENT SUMMARY of swarm result', parent model consumed 2 of 2 responses, caller's request_state == {}.
  • Delta since cdd43ea is exactly the two things I asked for (copy minus request_state, description text) plus the test.

Inline at strands-py/src/strands/vended_tools/swarm/swarm.py:173:

🔴 The model is never told what goes in these dicts — no keys, no presets, no Choice options. tool_spec is identical for swarm and for make_swarm(presets=…, instructions=Choice([…]), model=Fixed(…)); the agents description the model sees is literally Agent specification dicts (see :func:make_swarm). with items: {"type": "object", "additionalProperties": true}.

Consequences I verified: a model that sends system_prompt (the key the superseded strands_tools swarm used) gets a child with system_prompt=""; under instructions=Choice([...]) any free-form instruction is dropped and the default preset is skipped (spec.py:238-245) → blank agent holding every parent tool. Reached by Agent(tools=[swarm]), the headline usage.

The sibling make_subagent (strands-agents#4841) builds the schema per axis via Choice.to_schema_property and appends Available subagents (agent_type): … to the description.

Suggestion: build items as {"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":…, <one property per Open/Choice axis>}} plus minItems: 1, maxItems: max_agents, and list the presets in the description. Factoring the per-axis property builder into spec.py so both tools (and the TS port) share it would make the authority-mode system a real two-consumer abstraction.

Inline at strands-py/src/strands/vended_tools/swarm/swarm.py:138:

🔴 Swarm failure is invisible to the model. Swarm sets Status.FAILED without raising on max_handoffs/max_iterations/repetitive-handoff (multiagent/swarm.py:799-802) and on node timeout/exception (:876-880); str(result) only emits "{node}: {text}" lines (base.py:154-176). Verified ToolResults the parent gets:

  • max_iterations hit → {"status":"success","content":[{"text":"A: I handed off to B."}]}
  • node_timeout hit → {"status":"success","content":[{"text":""}]}
  • child model raises → {"status":"success","content":[{"text":"writer: Bedrock ThrottlingException: …"}]} — the exception text reads as the agent's answer.

Hitting a limit is the normal failure mode of a model-authored team, so this is the default path, not an edge.

Suggestion:

        result = await sdk_swarm.invoke_async(task)
        if result.status != Status.COMPLETED:
            raise RuntimeError(
                f"Swarm stopped before completing (status={result.status.value}, "
                f"{result.execution_count} iterations; max_handoffs={max_handoffs}, "
                f"max_iterations={max_iterations}). Partial output:\n{result}"
            )
        return str(result)

(Status import from ..multiagent.base.) The should_continue reason string is only logged today (swarm.py:801), so the tool can't quote it — worth surfacing on the result in a follow-up.

Inline at strands-py/src/strands/vended_tools/swarm/swarm.py:136:

🟡 Blocks the parent's event loop for the whole swarm run, and ignores cancel_signal. Async decorated tools are awaited directly on the loop (tools/decorator.py:649); Swarm.__call__ → run_async → future.result() (_async.py:42-45). With a fake 1 s swarm, a 100 ms heartbeat on the same loop ticked 0 times — so stream_async consumers, hooks and concurrent tools freeze for up to execution_timeout=900 s. tool_context.cancel_signal is never read; end-to-end, agent.cancel() fired during the first child call still ran every remaining node.

Suggestion: result = await sdk_swarm.invoke_async(task) (fixes the blocking; folded into the suggestion on L138). Swarm.invoke_async has no cancel parameter today, so either poll tool_context.cancel_signal around it or document that a running swarm can't be cancelled.

Inline at strands-py/src/strands/vended_tools/swarm/swarm.py:70:

🟡 Default tools=None → Inherit() gives every child the parent's full registry, including swarm itself — no depth guard. Verified: parent Agent(tools=[swarm, calculator]) → child tool_names == ['calculator', 'swarm']. Each nesting level is bounded only by 900 s × 20 iterations. The sibling guards exactly this with max_depth + _DEPTH_STATE_KEY (strands-agents#4841 subagent.py:44,272-286). Same root cause also makes the tool unusable from inside an SDK Swarm node: the child inherits the outer handoff_to_agent and the inner swarm rejects it (see appendix).

Suggestion: exclude the swarm tool's own name (and handoff_to_agent) when inheriting, and/or add max_depth with the sibling's state-key pattern.

Inline at site/src/content/docs/user-guide/sdk/tools/vended-tools.mdx:858:

🟡 This example doesn't do what the prose says. With Agent(tools=[swarm]) the parent has no http_request/web_fetch, and _default_builder silently drops unknown names (spec.py:285) → the researcher is built with tool_names == []. And instructions=Choice([...]) alongside presets: a model-picked instruction bypasses the preset (spec.py:238-245) and the child inherits all parent tools instead of the preset's narrowed set — the opposite of what the reader expects. (Neither is visible to the model today anyway — see the schema comment.)

Suggestion: give the parent the tools the presets name, drop the instructions=Choice(...) line or document the override, and show the agents shape the model is expected to send.

Inline at strands-py/tests/strands/vended_tools/test_swarm.py:243:

🟡 The feature under test is the authority-mode wiring, and none of it is covered. grep "Preset\|Choice\|Fixed\|agent_type\|builder=" over this file → one hit (:115). Nothing covers presets, default_preset auto-pick (swarm.py:91-92), agent_type → preset resolution, Unknown agent_type surfacing through the tool, a custom builder=, or that nodes= passed to Swarm are the built children. Here the mock result carries "writer: Done!" but the assertion is only isinstance(result, str) — a regression to "" passes (and does, see the L138 comment).

Suggestion: assert result == "writer: Done!"; add one test per axis (Fixed/Choice/preset/agent_type error) that asserts the AgentSpec handed to the builder; add a Status.FAILED case.

Inline at strands-py/src/strands/vended_tools/swarm/swarm.py:340:

🟡 mcp_servers=Choice(...) is accepted but unreachable by the model. The axis is passed to _resolve_specs (L174) but not to _build_agent_item_schema (L119), so no mcp_servers property is emitted and additionalProperties: false blocks the model from sending one. Verified: with Choice(["docs","github"]) the model can never pick, and _resolve_list(_UNSET, Choice(multiple=False)) → None → inherit every MCP server (spec.py:186-190) — the restriction inverts. multiple=True degrades to "all options", Fixed works.

Reached by any make_swarm(mcp_servers=Choice(...)) caller.

Suggestion:

    if isinstance(tools, Choice):
        properties["tools"] = tools.to_schema_property("Tools this agent may use.")

    if isinstance(mcp_servers, Choice):
        properties["mcp_servers"] = mcp_servers.to_schema_property("MCP servers this agent may use.")

plus mcp_servers=mcp_servers in the _build_agent_item_schema signature/call at L119. Rejecting non-multiple Choice for tools/mcp_servers in make_swarm (as make_subagent does) would also close the inherit-all inversion for both axes.

Inline at strands-py/src/strands/vended_tools/swarm/swarm.py:228:

🔴 Children share the parent's live invocation_state, so a child calling stop halts the parent. Agent.stream_async uses the dict as-is (agent.py:1413 merged_state = invocation_state) and event_loop_cycle reuses an existing request_state (event_loop.py:242); the vended stop tool sets request_state["stop_event_loop"] = True (stop.py:111-112), which the parent's loop honours at event_loop.py:990 right after this tool returns.

Repro: Agent(tools=[swarm, stop]), child model calls stop → parent stop_reason=tool_use, final text '', parent model never called again. Reached by the default tools=Inherit() — children get stop whenever the parent has it, and "signal completion" is exactly what a child model will do with it.

A shallow copy is not enough (request_state is the same dict object — tested, still breaks). Dropping it works:

        child_state = {k: v for k, v in tool_context.invocation_state.items() if k != "request_state"}
        async for event in sdk_swarm.stream_async(task, invocation_state=child_state):

Worth a test: child calls stop, assert the parent still gets a final turn.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Documentation Preview Failed

The documentation deployment encountered an error. Please check the deployment logs for more details.

This branch had an error being deployed

1 failed deployment
auto-approve — e5bebed2 Deployed Oct 9, 2026 by poshinchen via Build and Deploy Preview #11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants