Expose IInteractionService to polyglot app hosts#17959
Conversation
Add an ATS-first polyglot surface for the interaction service so app hosts in TypeScript, Python, Go, Rust, and Java can prompt users from command callbacks. - Expose ExecuteCommandContext.ServiceProvider (serviceProvider()) so command callbacks can resolve services, including the new getInteractionService(). - Register IInteractionService as an ATS handle and add InteractionExports with prompt methods, input factories (as IInteractionService extensions so the scanner keeps the input name and emits them as service methods), an InteractionInputBuilder handle, and a curated InteractionInputLoadContext for dynamic loading that never exposes the raw IServiceProvider. - Model inputs via a builder/handle instead of a DTO because input options can carry non-serializable callbacks (dynamic loading). - Add a 'pick-zone' withCommand test bench (dynamically loaded choice input) to the polyglot apphosts and regenerate the codegen snapshots for all languages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… surface Expands the polyglot test bench so every newly added IInteractionService member is exercised by the per-language typecheck: all prompt overloads (confirmation/messageBox/notification/input/inputs), every input factory (text/secret/boolean/number/choice), the builder methods (withValue/ withChoiceOptions/withDynamicLoading), the dynamic-loading context accessors/setters (getInputName/getInputValue/setChoiceOptions/setValue), and all option/result DTO fields plus the MessageIntent enum. Added to the TypeScript, Python, Go and Java apphosts. Also removes the redundant <remarks> on ExecuteCommandContext.ServiceProvider and regenerates the TypeScript snapshot to drop the propagated doc comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tsc (strict) widened the const zones ternary into a union of object literals with mutually-exclusive optional undefined properties, which is not assignable to setChoiceOptions(Record<string, string>). Annotate the variable as Record<string, string> so each branch is contextually typed. Verified by generating the SDK modules with 'aspire restore' and running 'tsc --noEmit' on the TypeScript apphost (now passes clean). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PromptInput/PromptInputs echoed the engine's InteractionInput instances back to the polyglot caller. When an input was created via withDynamicLoading, the input still carried its LoadCallback Func on DynamicLoading, which System.Text.Json cannot serialize across the ATS/JSON-RPC boundary, surfacing as a failed 'promptInputs' invocation. Project result inputs onto callback-free copies so only data fields (name, value, options, etc.) cross the boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native InputsDialogInteractionOptions.ValidationCallback was not reachable from the polyglot surface: it is a Func and InteractionInputsDialogOptions is a serializable DTO that cannot carry delegates. Add an optional validationCallback parameter to the ATS PromptInput/PromptInputs exports (the callback travels as a method argument, not a DTO field) and bridge it onto the native options via a fresh instance so the shared Default singleton is never mutated. The callback receives the already-curated, exported InputsDialogValidationContext handle (its IServiceProvider is [AspireExportIgnore]'d), so polyglot callers can read submitted inputs and call addValidationError. Regenerate all 5 codegen snapshots + ats.txt and exercise the new callback in the interaction-showcase command of every polyglot test bench app (TS/Python/Go/Java). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…surface InteractionInput.DynamicLoading holds an InputLoadOptions with a non-serializable LoadCallback delegate, and the dynamic-loading payload is always stripped from interaction results at runtime (InteractionExports.ToResultInput). Polyglot app hosts configure dynamic loading exclusively through InteractionInputBuilder.WithDynamicLoading and never read this property back, so advertising it on the result DTO described a value that is always null on the wire (and pulled the otherwise-unused InputLoadOptions type into the surface). Annotate the property with [AspireExportIgnore] so it is excluded from the ATS surface, regenerate ats.txt and the Go/Java/Python/Rust snapshots (which drop the generated field), and remove the matching hand-authored line from the TypeScript base.mts so all five languages stay consistent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17959Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17959" |
There was a problem hiding this comment.
Pull request overview
This PR exposes Aspire’s IInteractionService to polyglot app hosts by extending the ATS/JSON-RPC surface with interaction-specific exports and handle-based input builders so delegate-bearing callbacks (dynamic loading, validation) don’t need to serialize across the wire.
Changes:
- Exposes
ExecuteCommandContext.ServiceProviderto polyglot command callbacks and adds an ATS export to resolveIInteractionServicefrom that provider. - Introduces an ATS-first interaction surface (
InteractionExports) including prompt APIs, input factories, dynamic-loading context, and validation callback plumbing. - Regenerates ATS shape + polyglot SDK snapshots and updates polyglot bench app hosts to exercise the new interaction APIs (including dynamic loading + validation).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts | Adds TypeScript bench commands to exercise the interaction service surface (dynamic loading + validation). |
| tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py | Adds Python bench commands to exercise interaction prompts, inputs, dynamic loading, and validation. |
| tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java | Adds Java bench commands to exercise interaction prompts, inputs, dynamic loading, and validation. |
| tests/PolyglotAppHosts/Aspire.Hosting/Go/apphost.go | Adds Go bench commands to exercise interaction prompts, inputs, dynamic loading, and validation. |
| tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts | Updates generated TypeScript SDK snapshot to include the interaction service + input builder handles and related DTOs. |
| tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs | Updates generated Rust SDK snapshot for interaction service, DTOs, and handle wrappers. |
| tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py | Updates generated Python SDK snapshot for interaction service, DTOs, and handle wrappers. |
| tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go | Updates generated Go SDK snapshot for interaction service, DTOs, and handle wrappers. |
| src/Aspire.Hosting/IInteractionService.cs | Excludes InteractionInput.DynamicLoading from the ATS surface to avoid delegate serialization. |
| src/Aspire.Hosting/Ats/InteractionExports.cs | Adds ATS exports and supporting DTO/handle types for prompts, input factories/builders, dynamic loading, and validation callbacks. |
| src/Aspire.Hosting/Ats/AtsTypeMappings.cs | Adds ATS export mapping for IInteractionService. |
| src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs | Removes ATS ignore on ExecuteCommandContext.ServiceProvider to enable service resolution from polyglot command callbacks. |
| src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | Regenerates ATS shape to include the interaction-related handles, DTOs, and capabilities. |
| src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.mts | Removes InteractionInput.dynamicLoading from the TypeScript base model to match ATS-ignore behavior. |
The polyglot choice input factories (createChoiceInput, withChoiceOptions, setChoiceOptions) accepted an IReadOnlyDictionary<string,string>, which does not guarantee option order across the ATS boundary. Dropdown option order is user-visible, so model choices as an ordered InteractionChoiceOption[] (value + label) instead, matching the native IReadOnlyList ordering contract. Regenerate ats.txt and all five language snapshots, and update the TypeScript, Go, Java, and Python polyglot test bench apphosts to the new ordered array form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Java generator named the options-bag parameter "options" and then flattened its properties into locals of the same camelCase name. When a capability has an optional parameter literally named "options" (the interaction prompt methods, createChoiceInput, etc.), the generated "var options = options.getOptions()" shadowed the parameter, producing "variable options is already defined" plus cascading "cannot infer type" javac errors. This broke the Java SDK Validation CI job for every apphost because IInteractionService is part of core Aspire.Hosting. Rename the bag parameter to "optionsBag" (matching the TypeScript generator) so flattened locals never collide. Regenerated the Java snapshots accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The interaction option DTOs declared their nullable string properties with plain setters. The ATS DTO scanner can only detect optionality for nullable value types (Nullable.GetUnderlyingType) or init-only properties; it cannot see nullable reference-type annotations through reflection. As a result PrimaryButtonText, SecondaryButtonText, LinkText, LinkUrl, Label, Description, Placeholder, Value and DependsOnInputs were emitted as required. Downstream generators then treated them as required: the Go and Rust SDKs serialized empty strings unconditionally, blanking server-side defaults when a caller only wanted to set a single option such as Intent. (TypeScript, Python and Java already rendered them optional.) Switch these option-bag properties to init-only, matching the existing HttpsCertificateInfo.Thumbprint convention, so the scanner marks them optional. Regenerated ats.txt and the Go/Rust snapshots (now Option/pointer with conditional serialization) and updated the Go validation apphost to use aspire.StringPtr for the now-optional fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…InputValue Address PR review feedback on the interaction-service exports: - PromptInputs now validates each array element and throws a clear ArgumentException naming the parameter and index instead of letting a null element (possible over JSON-RPC) surface as a NullReferenceException. - GetInputValue returns an empty string instead of null when an input has no value or no input with that name exists. The ATS code-generation system has no support for nullable reference return types, so every generated SDK already types this as a non-nullable string; returning null caused Go/Rust clients to fail decoding and made the TS contract unsound. Updated the TypeScript snapshot for the revised returns doc text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
…-interaction-service # Conflicts: # src/Aspire.Hosting/api/Aspire.Hosting.ats.txt # tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go # tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs # tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java # tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts
|
Re-running the failed jobs in the CI workflow for this pull request because 4 jobs were identified as retry-safe transient failures in the CI run attempt.
Matched test failure patterns (1 test)
|
|
Let me know if you think we should try the DTO again to be closer to the C# API since we support callbacks in them with #17950 |
|
@sebastienros where? |
|
pr-testing report? |
Can you give an example of what using interaction service as DTOs would look like? It seems like we should stick to the .NET API unless there is a good reason not to. In the .NET API the inputs passed in are mutated with the set value (at least, I remember from memory). For example, const result = await interactionService.promptInputs(
"Pick a zone",
"Choose a region, then pick a zone from the dynamically loaded options.",
[region, zone]);Does the TypeScript API work the same way? A test to check would be good. |
|
I see this kind of code in some of the samples: const selectedColor = multi.inputs?.find(input => input.name === "color")?.value;const name = inputs.find(input => input.name === "name");
if (name?.value === "bad") {
await validationContext.addValidationError("name", "Name cannot be 'bad'.");
}There should be an easy way to get an input by name. See examples of using .NET API: |
…-interaction-service
… strong DTO callbacks for Go/Java/Python Relocate the inputs-dialog validation callback from a method parameter of promptInput/promptInputs onto the InteractionInputsDialogOptions DTO property, matching the C# IInteractionService usage and leveraging DTO-callback support. DTO-property callbacks previously rendered weakly typed (Object / func(...any) any) in Go/Java/Python while TypeScript already rendered them strongly typed. Teach the Go, Java, and Python generators to render DTO callback properties with full strong typing using the same metadata TS already consumes (no runtime-template changes: each language's recursive transport marshaller already registers funcs/closures embedded in serialized DTO maps). - Go: DTO callback fields use RenderCallbackType; ToMap embeds a typed shim via a shared EmitGoCallbackShimBody factored out of EmitCallbackRegistration. - Java: strong field/getter/setter; toMap wraps AspireActionN/AspireFuncN in a java.util.function.Function with typed arg conversion (guarded to <=1 param to match the single-arg runtime wrapper); fromMap skips client->host callbacks. Lambda parameter renamed to transportArg to avoid shadowing the converted local. - Python: DTO callback properties use typing.Callable[[T], R]. - Rust: DTO callbacks remain Option<Value> (serde derive cannot serialize Box<dyn Fn>); expanded comment documents the known follow-up, consistent with existing PrepareRequest/CreateProcessSpec handling. Regenerated ats.txt and the 5 codegen snapshots, and updated the Go/Java/Python/TS polyglot apphosts to set validationCallback inside InteractionInputsDialogOptions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lback Local `aspire restore` + compile (go build / javac) validation surfaced that the withCommand UpdateState DTO callback now renders strongly typed in Go and Java (a side effect of emitting strong DTO callbacks), so the benches' weak forms no longer compiled. - Go: updateCommandState is now func(ctx UpdateCommandStateContext) ResourceCommandState (drops the manual args[0] type assertion since ctx is strongly typed). - Java: setUpdateState takes a bare lambda matching AspireFunc1 instead of a java.util.function.Function cast. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…p test Address JamesNK's PR feedback on clunky by-name input lookups. The TypeScript InteractionInputCollection already ships client-side by-name accessors (get/value/required/requiredValue), so the showcase bench now uses value(name) in validation callbacks and the echo command instead of toArray().find(). Add a round-trip test proving PromptInputs returns submitted values keyed by input name, documenting that polyglot callers receive values via the result rather than by mutating the server-side input handles they pass in. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bring the Go, Java, and Python polyglot code generators to parity with
TypeScript by injecting hand-authored by-name accessor methods
(get/required/value/requiredValue) onto the auto-generated
InteractionInputCollection wrapper. These delegate to the generated
toArray capability and match names case-insensitively, mirroring the
.NET InteractionInputCollection indexer and the TS get/value helpers.
Update the Go/Java/Python test-bench apphosts to use value("name")
instead of toArray() loops, and regenerate the affected snapshots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR Testing ReportPR Information
CLI Version Verification
Changes Analyzed (23 files)
Test Scenarios ExecutedScenario 1: CLI install + version gateObjective: Install the PR CLI via the dogfood script and confirm it matches the PR head.
Scenario 2: SDK regeneration from the PR build (aspire restore)Objective: Regenerate the TypeScript SDK from the PR's Aspire.Hosting and confirm the new polyglot interaction API surface is present.
Scenario 3: AppHost typecheck against regenerated SDK (tsc --noEmit)Objective: Confirm the public API is consumable and sound in real TypeScript caller code.
Scenario 4 (soundness): GetInputValue nullability contractObjective: Validate an earlier review concern that the generated The generated surface is Unhappy-Path Coverage
Notes / Limitations
Summary
Overall ResultPR VERIFIED for the polyglot IInteractionService API surface. Recommendations
|
Change InputsInteractionResult from a by-value DTO to an
[AspireExport(ExposeProperties = true)] handle so its inputs are surfaced as
the InteractionInputCollection handle. Polyglot callers can now read submitted
values by name (for example result.inputs().value("color")) using the same
accessors the validation and command-argument collections expose, instead of
scanning a serialized array by hand. canceled and inputs become cheap
RPC-backed accessors.
From() wraps callback-free input copies (ToResultInput) in a fresh collection
so the handle can be enumerated/serialized safely after the prompt completes.
Regenerated all 5 language snapshots and the ats.txt baseline, updated the
Go/Java/Python/TypeScript polyglot benches to use the by-name accessor, and
extended the round-trip test to assert by-name access.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Re-running the failed jobs in the CI workflow for this pull request because 3 jobs were identified as retry-safe transient failures in the CI run attempt.
Matched test failure patterns (1 test)
|
|
@JamesNK added by-name accessors to all the input collections so you don't have to scan arrays. It works the same on validation inputs, command arguments, and the multi-input prompt result: // validation
const color = await ctx.inputs().value("color");
// multi-input result
const result = await interactions.promptInputs(...);
if (!await result.canceled()) {
const color = await result.inputs().value("color");
}The result is now a handle (like the validation context), so |
Collection-returning getters (`result.inputs()`, `validationContext.inputs()`,
and a command's `arguments()`) returned `Promise<InteractionInputCollection>`,
which forced a double await to reach the by-name accessors:
`await (await result.inputs()).value("color")`. The C#, Go, Java, and Python
surfaces all chain in a single step, so this was a TypeScript-only wart.
Introduce a hand-written `InteractionInputCollectionPromise` thenable in
base.mts that is awaitable to the collection but also forwards the by-name
accessors (value/get/required/requiredValue/toArray), and register
`InteractionInputCollection` as a promise-wrapper type so the generator emits
the fluent wrapper for every collection-returning getter. Callers can now write
`await result.inputs().value("color")`; the previous double-await form still
compiles because the wrapper resolves to the plain collection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The prompt-result-as-handle change made `PromptInputs` return a single `InputsInteractionResult` handle (captured-error pattern) instead of `(result, error)`, but the Go bench still used two-variable assignment, breaking the Go SDK Validation build (`assignment mismatch: 2 variables but PromptInputs returns 1 value`). Update both call sites to single-value assignment; the error now surfaces on the terminal `Canceled()`/`Inputs()` calls, consistent with the other handle wrappers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… present
The TypeScript generator only collapsed a lone `options` DTO parameter
directly when it was the *single* optional parameter. Methods that also
accept a trailing `cancellationToken` (e.g. the IInteractionService prompt
methods) therefore bundled both optionals into a generated
`{ options?, cancellationToken? }` object, forcing callers to write the
awkward double-nested `promptNotification(t, m, { options: { ... } })`.
Thread a trailing cancellation token as its own parameter so the options
DTO can be used directly alongside it, collapsing the public API to
`promptNotification(t, m, { intent, ... }, cancellationToken?)` while
keeping the named C# options object. This mirrors the C# IInteractionService
shape and removes the generated PromptXxxOptions wrapper interfaces.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
❓ CLI E2E Tests unknown — 113 passed, 0 failed, 2 unknown (commit View all recordings
📹 Recordings uploaded automatically from CI run #27174145002 |
Description
Aspire's
IInteractionService(prompts, message boxes, notifications, and dynamic inputs) was only reachable from C# app hosts. Polyglot app hosts (TypeScript, Python, Go, Java, Rust) had no way to drive user interactions even though other services are already exposed through the ATS callback-service-provider surface. This PR brings the interaction service to polyglot app hosts.The core challenge is that the natural way to model interaction inputs as DTOs breaks down for inputs that carry callbacks (dynamic option loading, dialog validation): delegates are not serializable across the ATS/JSON-RPC boundary. Modeling them as DTO fields produces a runtime serialization crash.
Approach
The work was done in the order the design required:
ServiceProviderfromExecuteCommandContextso polyglot commands have an entry point to resolve services.InteractionExportssurface (a tailored internal API rather than a 1:1 DTO mapping of the native types). Inputs are created server-side through factory methods returning an opaqueInteractionInputBuilderhandle, so callbacks never have to cross the wire as data. Callbacks (dynamic loading, dialog validation) are passed as method parameters and registered as callback handles; the engine marshals a curated context back to the client.promptConfirmation,promptMessageBox,promptNotification,promptInput,promptInputs, input factories (createTextInput/createSecretInput/createBooleanInput/createNumberInput/createChoiceInput),withChoiceOptions/withValue/withDynamicLoading, and the inputs-dialogvalidationCallback.Notable details for reviewers
InteractionInput.DynamicLoadingholds aFuncand is sanitized out of interaction results at runtime (ToResultInput), and the property is[AspireExportIgnore]'d so it no longer appears on the polyglot read-back surface (it was always null on the wire). This is the fix for the original "Serialization of System.Func is not supported" crash.value/getfor optional lookup,requiredValue/requiredto throw when missing), so callers readctx.inputs().value("color")instead of scanning an array. To make this work on the prompt result,InputsInteractionResultis modeled as a handle ([AspireExport(ExposeProperties = true)]) so it surfaces the sameInteractionInputCollection(result.inputs()) and aresult.canceled()accessor, reusing the proven validation/argument-collection pattern.validationCallbacklives on the options object (alongsideprimaryButtonText, etc.), mirroring the C#InputsDialogInteractionOptionsshape, now that callbacks can be carried on DTOs.{ value, label }entries so option order is preserved across the wire.InputsDialogValidationContext(its rawIServiceProvideris[AspireExportIgnore]'d). The sharedInputsDialogInteractionOptions.Defaultsingleton is never mutated.withCommandtest bench. The polyglot bench apps intests/PolyglotAppHostsexercise the full surface (including a dynamically-loaded options command and dialog validation) in all four languages, providing per-member coverage.ats.txtand the five language snapshots are regenerated. Per repo convention, generated-shape behavior is validated through the polyglot bench apps rather than unit tests asserting code shape.Manual end-to-end validation was done by building custom app hosts and driving the dashboard in a browser: input validation (required/reserved errors keep the dialog open, valid input closes it) and dynamic options (
alwaysLoadOnStartpopulates choices,dependsOnInputsreloads them on dependency change) both work with no serialization or RPC errors.Fixes # (issue)
TypeScript examples
The interaction service is resolved from the command context's service provider. All examples come from the polyglot test bench (
tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.mts).Resolve the service from a command callback:
Message-style prompts (confirmation, message box, notification):
Create inputs through factory methods (each returns an opaque server-side builder handle). Choices are an ordered array of
{ value, label }entries:Dynamically loaded options. The callback runs server-side and reads other inputs through the load context, so no delegate is ever serialized:
Single and multi-input prompts with a dialog validation callback. The callback lives on the options argument (next to
primaryButtonText) and runs server-side, reading submitted values by name:Checklist
<remarks />and<code />elements on your triple slash comments?