Skip to content

Feature/orion display proposals d#64

Open
sonra44 wants to merge 3 commits intomasterfrom
feature/orion-display-proposals-d
Open

Feature/orion display proposals d#64
sonra44 wants to merge 3 commits intomasterfrom
feature/orion-display-proposals-d

Conversation

@sonra44
Copy link
Copy Markdown
Owner

@sonra44 sonra44 commented Jan 8, 2026

This PR introduces display proposals for the Orion operator console as part of Stage 0. It sets up the basic UI layout and ensures that display-related features are aligned with the Stage 0 roadmap.

Summary by Sourcery

Add support for receiving, validating, and displaying QIKI proposals in the Orion operator console and upgrade QIKI intent handling to a structured v1 protocol.

New Features:

  • Introduce a Proposals screen in the Orion console with a sortable table and inspector integration for QIKI proposals.
  • Add structured QIKI intent v1 and proposals v1 models, including environment, selection, and snapshot context, plus proposal batch metadata.

Enhancements:

  • Update QIKI intent publishing to send enriched IntentV1 payloads asynchronously with minimal system snapshots and environment context.
  • Extend NATS client and subjects to handle versioned QIKI intent and proposals streams while keeping backward compatibility aliases.
  • Improve command placeholder help text to document both q: and // QIKI intent prefixes.

Tests:

  • Add unit tests for the QIKI v1 protocol models, including strict validation and JSON round-trips.
  • Add tests ensuring QIKI console commands publish IntentV1 messages and that shell commands do not.
  • Add tests covering ingestion and validation behavior for proposal batches in the Orion app.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jan 8, 2026

Reviewer's Guide

Adds Stage 0 support for displaying QIKI proposals in the Orion operator console and introduces a versioned, strongly-typed QIKI intent/proposals protocol, updating NATS subjects, console wiring, and tests accordingly.

Sequence diagram for ingesting and displaying QIKI proposals

sequenceDiagram
    participant QIKICore as QIKI_core
    participant Nats as NatsClient
    participant Orion as OrionApp
    participant UI as ProposalsScreen

    QIKICore->>Nats: publish QIKI_PROPOSALS_V1(ProposalsBatchV1)
    Nats->>Nats: message_handler decode msg.data
    Nats->>Orion: callback handle_proposals_data({ data: batch_dict })

    Orion->>Orion: handle_proposals_data
    Orion->>Orion: ProposalsBatchV1.model_validate(payload)
    Orion->>Orion: _ingest_proposals_batch(batch)
    Orion->>Orion: update _proposals_batches
    Orion->>Orion: update _proposals_by_key from batch.proposals
    Orion->>Orion: enforce _max_proposals_rows
    Orion->>Orion: _render_proposals_table()
    Orion->>UI: update proposals-table rows
    Orion->>Orion: _calm_log("QIKI: <title>") per proposal
    Orion->>Orion: if active_screen == proposals
    Orion->>Orion: _refresh_inspector()

    actor Operator
    Operator->>UI: Move selection in proposals-table
    UI->>Orion: on_data_table_row_highlighted(event)
    Orion->>Orion: lookup payload in _proposals_by_key
    Orion->>Orion: _set_selection(SelectionContext(app_id=proposals,...))
    Orion->>Orion: inspector uses selection to render details
Loading

Sequence diagram for publishing QIKI IntentV1 from Orion

sequenceDiagram
    actor Operator
    participant Orion as OrionApp
    participant Nats as NatsClient
    participant QIKICore as QIKI_core

    Operator->>Orion: Enter command "q: text" or "// text"
    Orion->>Orion: _run_command(raw)
    Orion->>Orion: extract qiki_text
    Orion->>Orion: asyncio.create_task(_publish_qiki_intent(qiki_text))

    Orion->>Orion: _publish_qiki_intent(text)
    Orion->>Orion: TelemetrySnapshotModel.normalize_payload(latest_telemetry)
    Orion->>Orion: build vitals from normalized telemetry
    Orion->>Orion: determine selection kind/id from _selection_by_app
    Orion->>Orion: collect incidents_top from _incident_store
    Orion->>Orion: resolve environment_mode from OPERATOR_CONSOLE_ENVIRONMENT_MODE
    Orion->>Orion: find app_spec for active_screen
    Orion->>Orion: create SelectionV1
    Orion->>Orion: create IntentV1(text, lang_hint, screen, selection,...)
    Orion->>Orion: intent_payload = intent.model_dump()
    Orion->>Orion: canonical = json.dumps(intent_payload)
    Orion->>Orion: digest = sha256(canonical)

    Orion->>Nats: publish_command(QIKI_INTENT_V1, intent_payload)
    Nats-->>QIKICore: deliver IntentV1 message
    alt publish ok
        Orion->>Orion: _console_log("Intent sent: hash=... ts=...")
    else publish failed
        Orion->>Orion: _console_log("Failed to send intent: <error>")
    end
Loading

Updated class diagram for Orion-QIKI protocol models

classDiagram
    class _StrictModel {
      <<BaseModel>>
      +model_config
    }

    class LangHint {
      <<enum>>
      +AUTO
      +RU
      +EN
    }

    class EnvironmentMode {
      <<enum>>
      +FACTORY
      +MISSION
    }

    class SelectionV1 {
      +kind: "event"|"incident"|"track"|"snapshot"|"none" = "none"
      +id: str
    }

    class IntentV1 {
      +version: 1
      +text: str
      +lang_hint: LangHint = LangHint.AUTO
      +screen: str
      +selection: SelectionV1
      +ts: int
      +environment_mode: EnvironmentMode = EnvironmentMode.FACTORY
      +snapshot_min: dict~str,Any~
    }

    class ProposalV1 {
      +proposal_id: str
      +title: str
      +justification: str
      +priority: int [0..100]
      +confidence: float [0.0..1.0]
      +proposed_actions: list~Any~
      +_must_be_empty_in_stage_a(v: list~Any~) list~Any~
    }

    class ProposalsBatchV1 {
      +version: 1
      +ts: int
      +proposals: list~ProposalV1~
      +metadata: dict~str,Any~
    }

    _StrictModel <|-- SelectionV1
    _StrictModel <|-- IntentV1
    _StrictModel <|-- ProposalV1
    _StrictModel <|-- ProposalsBatchV1

    IntentV1 *-- SelectionV1
    ProposalsBatchV1 *-- ProposalV1
Loading

File-Level Changes

Change Details Files
Add a new Proposals screen to the Orion operator console UI and wire it into navigation and inspector/help flows.
  • Register a new Orion app spec for the proposals screen with title, hotkey, and aliases.
  • Add proposals label/shortcut into app title bar hints and keyboard help, including an inspector hint when proposals is active.
  • Introduce a #screen-proposals container with a DataTable for priority, confidence, and title, plus corresponding CSS layout and screen switching logic.
  • Route the workspace focus and selection context to the proposals table and feed inspector/summary panels with proposal-specific fields.
src/qiki/services/operator_console/main_orion.py
Implement proposals data ingestion, storage, rendering, and NATS subscription plumbing.
  • Maintain in-memory stores for proposals and recent batches with a configurable max row limit.
  • Add _seed_proposals_table and _render_proposals_table helpers to initialize and sort/render proposals by priority and confidence, including NA fallbacks.
  • Subscribe to QIKI proposals via NATS, validate payloads as ProposalsBatchV1, and update the store/table while logging per-proposal messages and refreshing the inspector when active.
  • Extend the NATS client with subscribe_qiki_proposals, using a configurable subject and wrapping incoming messages into a normalized callback payload.
src/qiki/services/operator_console/main_orion.py
src/qiki/services/operator_console/clients/nats_client.py
Introduce a strict, versioned QIKI protocol model for intents and proposals and update operator intents publishing to use it.
  • Define LangHint, EnvironmentMode, SelectionV1, IntentV1, ProposalV1, and ProposalsBatchV1 Pydantic models with strict validation, version locking, and Stage A constraint that proposed_actions must be empty.
  • Update NATS subjects to use QIKI_INTENT_V1 and QIKI_PROPOSALS_V1, keeping QIKI_INTENTS as a backward-compatible alias pointing to the v1 intent subject.
  • Refactor _publish_qiki_intent to build an IntentV1 that includes language hint, environment mode (from env var), active screen label, selection, and a minimal snapshot (vitals, selection, top incidents), then publish to QIKI_INTENT_V1 with a canonicalized payload and hash logging.
  • Change QIKI intent command handling to send intents fire-and-forget via asyncio.create_task, ensuring the operator loop is not blocked and improving error/log messages when NATS is unavailable or publishing fails.
src/qiki/shared/models/orion_qiki_protocol.py
src/qiki/shared/nats_subjects.py
src/qiki/services/operator_console/main_orion.py
Add tests around QIKI routing, protocol models, and proposals ingestion/display wiring.
  • Extend existing QIKI routing tests with a fake NATS client to assert that q:-prefixed commands publish IntentV1 to QIKI_INTENT_V1 and that non-QIKI shell commands do not publish intents.
  • Add unit tests for IntentV1, ProposalV1, and ProposalsBatchV1 to verify round-trip serialization, required fields, Stage A constraints on proposed_actions, strict versioning, and rejection of extra fields.
  • Add tests for proposals ingestion to ensure _ingest_proposals_batch populates the internal store and that handle_proposals_data safely ignores invalid payloads without crashing.
src/qiki/services/operator_console/tests/test_qiki_routing.py
tests/unit/test_orion_qiki_protocol_v1.py
src/qiki/services/operator_console/tests/test_qiki_proposals_display.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 8, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The fire-and-forget asyncio.create_task(self._publish_qiki_intent(...)) callback currently calls t.exception() without handling it, which will re-raise in the callback context; consider logging the exception instead of calling t.exception() directly so background failures don’t crash the app.
  • In _publish_qiki_intent, OPERATOR_CONSOLE_INTENT_TOP_INCIDENTS is parsed from the environment on every call; consider caching this as an attribute (similar to _max_proposals_rows) to avoid repeated environment lookups and parsing in a potentially hot path.
  • The eviction logic in _ingest_proposals_batch trims _proposals_by_key based on dict insertion order, which may not align with recency or importance when existing proposal IDs are updated; consider basing eviction on an explicit timestamp or priority to avoid dropping still-relevant proposals.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The fire-and-forget `asyncio.create_task(self._publish_qiki_intent(...))` callback currently calls `t.exception()` without handling it, which will re-raise in the callback context; consider logging the exception instead of calling `t.exception()` directly so background failures don’t crash the app.
- In `_publish_qiki_intent`, `OPERATOR_CONSOLE_INTENT_TOP_INCIDENTS` is parsed from the environment on every call; consider caching this as an attribute (similar to `_max_proposals_rows`) to avoid repeated environment lookups and parsing in a potentially hot path.
- The eviction logic in `_ingest_proposals_batch` trims `_proposals_by_key` based on dict insertion order, which may not align with recency or importance when existing proposal IDs are updated; consider basing eviction on an explicit timestamp or priority to avoid dropping still-relevant proposals.

## Individual Comments

### Comment 1
<location> `src/qiki/services/operator_console/tests/test_qiki_routing.py:44-53` </location>
<code_context>
+    assert app._proposals_by_key["P1"]["title"] == "Title 1"
+
+
+@pytest.mark.asyncio
+async def test_handle_proposals_data_invalid_does_not_crash() -> None:
+    app = OrionApp()
</code_context>

<issue_to_address>
**suggestion (testing):** Add a routing test for the `//` shorthand QIKI intent prefix

You already validate the `//` shorthand in `_parse_qiki_intent`, but `test_qiki_prefix_publishes_intent_v1` currently only uses the `q:` prefix. Please add a sibling test that calls `await app._run_command("// scan 360")` and asserts that exactly one `QIKI_INTENT_V1` message is published with a valid `IntentV1` payload, so both prefixes are covered by routing tests and regressions in either branch are less likely.
</issue_to_address>

### Comment 2
<location> `src/qiki/services/operator_console/tests/test_qiki_proposals_display.py:9-18` </location>
<code_context>
+from qiki.shared.models.orion_qiki_protocol import ProposalsBatchV1, ProposalV1
+
+
+def test_ingest_proposals_batch_updates_store() -> None:
+    app = OrionApp()
+    assert app._proposals_by_key == {}
+
+    batch = ProposalsBatchV1(
+        ts=1700000000000,
+        proposals=[
+            ProposalV1(
+                proposal_id="P1",
+                title="Title 1",
+                justification="Just 1",
+                priority=80,
+                confidence=0.7,
+            ),
+            ProposalV1(
+                proposal_id="P2",
+                title="Title 2",
+                justification="Just 2",
+                priority=20,
+                confidence=0.5,
+            ),
+        ],
+        metadata={"source": "test"},
+    )
+
+    app._ingest_proposals_batch(batch)
+    assert "P1" in app._proposals_by_key
+    assert app._proposals_by_key["P1"]["title"] == "Title 1"
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Extend proposals ingestion tests to cover edge cases (missing IDs, row limit, sorting)

To better cover edge cases, please add:

* A test where a `ProposalV1` has an empty/whitespace `proposal_id` and assert `_ingest_proposals_batch` ignores it.
* A test that sets `app._max_proposals_rows` to a small value (e.g. 2), ingests more proposals than that, and asserts `_proposals_by_key` is truncated accordingly.
* Optionally, a test for `_render_proposals_table()` that verifies the rendered order matches the `priority`/`confidence` sorting rules.

These will exercise the non-happy paths in `_ingest_proposals_batch`/`_render_proposals_table` and strengthen coverage for real-world data conditions.

Suggested implementation:

```python
import pytest

from qiki.services.operator_console.main_orion import OrionApp
from qiki.shared.models.orion_qiki_protocol import ProposalsBatchV1, ProposalV1

```

```python
def test_ingest_proposals_batch_updates_store() -> None:
    app = OrionApp()
    assert app._proposals_by_key == {}

    batch = ProposalsBatchV1(
        ts=1700000000000,
        proposals=[
            ProposalV1(
                proposal_id="P1",
                title="Title 1",
                justification="Just 1",
                priority=80,
                confidence=0.7,
            ),
            ProposalV1(
                proposal_id="P2",
                title="Title 2",
                justification="Just 2",
                priority=20,
                confidence=0.5,
            ),
        ],
        metadata={"source": "test"},
    )

    app._ingest_proposals_batch(batch)

    assert "P1" in app._proposals_by_key
    assert app._proposals_by_key["P1"]["title"] == "Title 1"
    assert "P2" in app._proposals_by_key
    assert app._proposals_by_key["P2"]["title"] == "Title 2"


def test_ingest_proposals_batch_ignores_empty_proposal_id() -> None:
    app = OrionApp()
    assert app._proposals_by_key == {}

    batch = ProposalsBatchV1(
        ts=1700000000001,
        proposals=[
            ProposalV1(
                proposal_id="  ",  # empty/whitespace ID should be ignored
                title="Ignored Title",
                justification="Ignored Justification",
                priority=50,
                confidence=0.5,
            ),
            ProposalV1(
                proposal_id="VALID",
                title="Valid Title",
                justification="Valid Justification",
                priority=90,
                confidence=0.9,
            ),
        ],
        metadata={"source": "test-empty-id"},
    )

    app._ingest_proposals_batch(batch)

    # Only the valid proposal should be stored
    assert "VALID" in app._proposals_by_key
    assert app._proposals_by_key["VALID"]["title"] == "Valid Title"
    # No key for the empty/whitespace ID should be present
    assert "" not in app._proposals_by_key
    assert "  " not in app._proposals_by_key
    assert len(app._proposals_by_key) == 1


def test_ingest_proposals_batch_respects_row_limit() -> None:
    app = OrionApp()
    app._max_proposals_rows = 2
    assert app._proposals_by_key == {}

    batch = ProposalsBatchV1(
        ts=1700000000002,
        proposals=[
            ProposalV1(
                proposal_id="P1",
                title="Title 1",
                justification="Just 1",
                priority=10,
                confidence=0.4,
            ),
            ProposalV1(
                proposal_id="P2",
                title="Title 2",
                justification="Just 2",
                priority=50,
                confidence=0.6,
            ),
            ProposalV1(
                proposal_id="P3",
                title="Title 3",
                justification="Just 3",
                priority=90,
                confidence=0.9,
            ),
        ],
        metadata={"source": "test-row-limit"},
    )

    app._ingest_proposals_batch(batch)

    # Store should not grow beyond configured max rows
    assert len(app._proposals_by_key) <= app._max_proposals_rows
    # All stored proposals must have non-empty IDs
    assert all(k.strip() for k in app._proposals_by_key.keys())


def test_render_proposals_table_sorts_by_priority_and_confidence() -> None:
    app = OrionApp()

    batch = ProposalsBatchV1(
        ts=1700000000003,
        proposals=[
            ProposalV1(
                proposal_id="LOW",
                title="Low priority",
                justification="",
                priority=10,
                confidence=0.9,
            ),
            ProposalV1(
                proposal_id="MID",
                title="Mid priority",
                justification="",
                priority=50,
                confidence=0.5,
            ),
            ProposalV1(
                proposal_id="HIGH_LOW_CONF",
                title="High priority, low confidence",
                justification="",
                priority=90,
                confidence=0.3,
            ),
            ProposalV1(
                proposal_id="HIGH_HIGH_CONF",
                title="High priority, high confidence",
                justification="",
                priority=90,
                confidence=0.9,
            ),
        ],
        metadata={"source": "test-sorting"},
    )

    app._ingest_proposals_batch(batch)

    table = app._render_proposals_table()

    # We expect proposals ordered first by priority (desc), then by confidence (desc)
    # so HIGH_HIGH_CONF should appear before HIGH_LOW_CONF, and both before MID/LOW.
    # The concrete structure of `table` may vary; try to support both Rich Table
    # and plain-text string renderings.

    # If it's a Rich Table, inspect rows directly
    try:
        # Rich Table-like: rows with .cells or ._cells attributes
        rows = getattr(table, "rows", None)
        if rows is not None:
            # Extract the first column (assumed to be proposal_id) from each row
            ids_in_order = []
            for row in rows:
                cells = getattr(row, "cells", None) or getattr(row, "_cells", None) or row
                # First cell is assumed to contain the proposal ID
                first_cell = cells[0]
                ids_in_order.append(str(first_cell))

            assert ids_in_order.index("HIGH_HIGH_CONF") < ids_in_order.index(
                "HIGH_LOW_CONF"
            )
            assert ids_in_order.index("HIGH_LOW_CONF") < ids_in_order.index("MID")
            assert ids_in_order.index("MID") < ids_in_order.index("LOW")
        else:
            raise AttributeError
    except AttributeError:
        # Fallback: assume it's a string or renderable that can be cast to string
        rendered = str(table)
        high_high_idx = rendered.index("HIGH_HIGH_CONF")
        high_low_idx = rendered.index("HIGH_LOW_CONF")
        mid_idx = rendered.index("MID")
        low_idx = rendered.index("LOW")

        assert high_high_idx < high_low_idx < mid_idx < low_idx

```

Because the implementation of `OrionApp._ingest_proposals_batch` and `OrionApp._render_proposals_table` is not visible in the provided snippet, you may need to adjust the assumptions in `test_render_proposals_table_sorts_by_priority_and_confidence`:

1. If `_render_proposals_table()` returns a different structure than a Rich `Table` or string, adapt the test's inspection logic to match how proposal IDs are represented.
2. If the column order differs (e.g., proposal ID is not the first column), update the extraction of `proposal_id` from the table rows accordingly.
3. If the row-limit logic for `_max_proposals_rows` intentionally behaves differently (e.g., always keeping the newest N entries based on timestamp), you can tighten `test_ingest_proposals_batch_respects_row_limit` to assert the exact IDs that should remain.
</issue_to_address>

### Comment 3
<location> `tests/unit/test_orion_qiki_protocol_v1.py:41-15` </location>
<code_context>
+        IntentV1.model_validate({"version": 1, "text": "x"})
+
+
+def test_proposal_v1_actions_must_be_empty_in_stage_a() -> None:
+    ok = ProposalV1(
+        proposal_id="P1",
+        title="Title",
+        justification="Justification",
+        priority=50,
+        confidence=0.6,
+        proposed_actions=[],
+    )
+    assert ok.proposed_actions == []
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add boundary and enum coverage for the protocol models

The current tests cover round-trips, required fields, strict versioning, and `proposed_actions` validation. To further lock down the schema, consider adding:

* Boundary checks for `ProposalV1.priority` and `confidence` (e.g. `0`, `100`, `0.0`, `1.0`) plus assertions that out-of-range values fail validation.
* A test that `SelectionV1(kind="event", id="X")` validates while an invalid `kind` (e.g. `"foo"`) is rejected.
* Optionally, tests that `EnvironmentMode` and `LangHint` accept their string values (e.g. `"FACTORY"`, `"MISSION"`, `"ru"`, `"en"`).

This would better assert the on-wire contract and guard against accidental schema changes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd34190040

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3954 to +3956
intent_payload = intent.model_dump()
canonical = json.dumps(intent_payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
digest = sha256(canonical).hexdigest()[:8]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serialize intent payload before hashing

In Pydantic v2, IntentV1.model_dump() returns Enum objects (e.g., LangHint, EnvironmentMode) in Python mode. The subsequent json.dumps(...) has no default encoder and will raise TypeError: Object of type LangHint is not JSON serializable before the try/publish_command block, so q:/// intents never reach NATS. Use intent.model_dump(mode="json") or pass default=str to json.dumps when building the digest to avoid the crash.

Useful? React with 👍 / 👎.

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.

1 participant