Skip to content

feat: add Astra choice and pick list ask blocks - #731

Merged
thomasluizon merged 4 commits into
redesign/mainfrom
fix/ticket-1480-astra-ask-blocks
Oct 17, 2026
Merged

thomasluizon merged 4 commits into
redesign/mainfrom
fix/ticket-1480-astra-ask-blocks

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Closes thomasluizon/orbit-tickets#1480.

Astra can issue choice and pickList parts to chat clients that declare those kinds. Answers are validated and claimed once before any model call. Choice answers consume one daily message. Pick list answers bypass the model and daily allowance, and return one bulk approval preview containing the selected stored arguments.

The implementation lands in:

  • AskTool, the chat handler partials, validators, and block contracts: both forms, per-item bulk validation, capability negotiation, prompt guidance, the kill switch, stopping after ask, and both PostHog events.
  • IssuedAstraBlock, its store, OrbitDbContext, the generated migration and designer, and account reset: ownership, 24-hour expiry, invalidation by later turns, atomic claims, and cascade deletion.
  • AstraBlockAnswerService and the existing operation executor/policy evaluator: stored selections use the same bulk preview builder. Preview-only requests also require a preview in shadow mode, so the selection cannot execute a write.
  • ChatController, rate limiting, service registration, the agent catalog, and generated openapi.json: optional multipart blockAnswer on both routes, answer throttling, HTTP rejection statuses before streaming, and exclusion of ask from API key and MCP operations.

Deploy the API and its migration before the clients for thomasluizon/orbit-tickets#1483 and thomasluizon/orbit-tickets#1484.

Assumptions

  • The fixed pick list reply reuses the existing "Nothing has been saved." / "Nada foi salvo." wording; introducing new copy was rejected. Answer errors reuse the existing localized question and selection copy under distinct codes.
  • Log and skip items use the bulk tools' existing {habit_id, date} items in revised_items; dynamic filters were rejected because each selected id must identify one stored item.
  • Missing create dates resolve at issuance through IUserDateService; resolving them on the answer turn was rejected so the stored proposal remains stable.
  • Block-answer stream turns are processed before SSE starts; opening the stream first was rejected so answer errors retain their HTTP status.
  • ask is explicitly chat-only and omitted from the public agent catalog; adding an externally executable agent operation was rejected.

Extended the existing ask test suites instead of creating a separate test project.

Test evidence

  • dotnet build Orbit.slnx: exit 0, zero errors; regenerated OpenAPI is committed.
  • dotnet test tests/Orbit.Domain.Tests --no-build --no-restore: exit 0, 672 passed.
  • dotnet test tests/Orbit.Application.Tests --no-build --no-restore: exit 0, 5,600 passed.
  • dotnet test tests/Orbit.Infrastructure.Tests --no-build --no-restore: exit 0, 3,349 passed.
  • dotnet test --no-build --no-restore: exit 0 across all four test projects, including 32 analyzer tests (9,653 total).
  • Focused tests cover form limits, bulk argument rejection, all answer rejection codes, client negotiation, the kill switch, concurrent answers, quota behavior, reset/deletion, and exact preview content. The composed SQLite tests use the real executor, policy evaluator, pending store and preview builder for all three actions in ordinary and shadow modes; habit and log counts stay unchanged before approval.

The stopped-round token counter needed a stronger test:

  1. Unchanged Ask_EmitsNegotiatedPartAndStopsBeforeLaterTools passed with the defect present: dotnet test tests/Orbit.Application.Tests --no-build --no-restore --filter FullyQualifiedName~Ask_EmitsNegotiatedPartAndStopsBeforeLaterTools returned exit 0, one passed.
  2. The strengthened test adds a block-only response reporting 60,001 tokens. Before the fix, dotnet test tests/Orbit.Application.Tests --no-restore --filter FullyQualifiedName~Ask_EmitsNegotiatedPartAndStopsBeforeLaterTools returned exit 1, one failed and one passed: the handler incorrectly rejected the valid reply after counting the same model response twice.
  3. After the fix, dotnet test tests/Orbit.Application.Tests --no-restore --filter 'FullyQualifiedName~ValidationCopyTests|FullyQualifiedName~ErrorCopyTests|FullyQualifiedName~Ask_EmitsNegotiatedPartAndStopsBeforeLaterTools' returned exit 0, 1,235 passed, including both strengthened cases. The full application suite above also passes.
  • Unchanged baseline on b414370d: dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~Handle_CrisisDisclosureBeyondQuota_ReturnsStaticResourcesWithoutCallingAi passed, exit 0.
  • Regression command: dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~ChoiceAnswer_CrisisDisclosure_ReturnsStaticResourcesWithoutQuotaOrModel
    • Before implementation changes: all four cases failed, exit 1. English and Portuguese answers at quota returned failure; cases with allowance reached quota handling.
    • On this commit: all four passed, exit 0, asserting static resources, no model call, and no quota check or consumption.
  • Focused handler/service/guard tests: 231 passed.
  • dotnet build Orbit.slnx: exit 0, zero errors. OpenAPI regenerated unchanged.
  • dotnet test tests/Orbit.Domain.Tests: 672 passed, exit 0.
  • dotnet test tests/Orbit.Application.Tests: 5,609 passed, exit 0.
  • dotnet test tests/Orbit.Infrastructure.Tests: 3,349 passed, exit 0.
  • All commit hooks passed.

External interface evidence

IssuedAstraBlockStoreTests executes the installed EF Core SQLite provider to verify affected-row claim results, owner scoping, expiry, invalidation, reset, and cascade deletion. AstraPickListPreviewTests also verifies transaction rollback after a quota race against real SQL.

The migration was generated from OrbitDbContextFactory with dotnet ef migrations add AddIssuedAstraBlocks --project ../Orbit.Infrastructure --startup-project . from src/Orbit.Api; the tool reported a successful build and generation.

Eval scenarios for #26

  • An open question with a known answer set produces a choice part, such as a short walk with Morning, Lunch, and Evening options.
  • "Suggest some morning habits" produces a pickList with action create_habits.
  • A direct command such as "log my walk" produces no ask block.

Manual steps

After integration, deploy through GitHub Actions → Release API (release.yml). Verify a validated crisis choice answer at exhausted quota returns static resources without increasing usage. No new configuration, migrations, or backfills.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

Choice-answer preprocessing can suppress the quota-independent crisis support response. This safety regression should be fixed before merging.

Reviewed changes across all 45 files, including the answer lifecycle, bulk approval path, and compatibility with the sibling client contracts.

  • Interactive questions: Adds negotiated, chat-only choice and pickList blocks, argument validation, tool-round termination, and feature-flag controls.
  • One-shot persistence: Adds owner-scoped storage, expiry, invalidation, atomic claims, migration, and account cleanup.
  • Answer processing: Adds multipart answers and pre-stream HTTP errors; choices consume one daily message while pick lists create approval previews without model calls or writes, including in shadow mode.
  • Verification: The solution builds with zero errors and all 9,653 existing tests pass. A temporary regression test reproduced the crisis-answer failure described inline; it was removed afterward, leaving both worktrees clean.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes in 0265c574 since the prior Pullfrog review at b414370d, with the full PR diff reread for context. The resolved finding is addressed, and no new blocking issues remain.

  • Restored quota-independent crisis replies: Routed validated crisis choice answers to the existing static support response before quota checks, consumption, or model calls, while preserving ownership validation and one-shot claims.
  • Preserved feature-flag behavior: Shared the crisis enablement predicate with ordinary chat and retained normal quota/model handling when crisis support is disabled.
  • Verified regressions: Confirmed English and Portuguese crisis-answer coverage, invalid-answer rejection, concurrent claims, and ordinary choice behavior. The solution build completed with zero errors, and all 9,662 tests passed.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit d80442d into redesign/main Oct 17, 2026
26 checks passed
@thomasluizon
thomasluizon deleted the fix/ticket-1480-astra-ask-blocks branch October 17, 2026 21:44
thomasluizon added a commit that referenced this pull request Oct 17, 2026
The squash of orbit-api#731 replaced the commits this branch was stacked on.
Five add/add conflicts take the merged #731 version (this branch did not
change them beyond its older #731 base); the model snapshot keeps this
branch's Notion entities on top of it. The result equals the tree of
combined check a2 (base c8877ee with #731, #732 and #730 merged), which
passed build, the openapi freshness check and all four test projects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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