schema, @NonNull String templateUri) {
+ return observed("generateTaskPromptFromDataWithSchema", () -> {
+ Objects.requireNonNull(data, "data");
+ Objects.requireNonNull(schema, "schema");
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return promptGenerationOrchestrator.generateTaskPromptFromDataWithSchema(data, schema, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates an authorization prompt with metadata from natural-language input using the template identified by the
+ * template URI, bypassing scenario recognition.
+ *
+ * Authorization-T slot schemas are bundled with a2a-t-resources, so the entry point works out of the box with
+ * classpath resource source.
+ *
+ * @param text natural-language authorization input
+ * @param templateUri template URI string identifying the target template, such as
+ * {@code Authorization-T/authorization-policy-management/v1}
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the text or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed
+ * @throws net.openan.a2at.sdk.core.exception.PromptGenerationException with the code {@code template.not_found},
+ * {@code template.load_failed}, {@code slot.schema_not_found}, {@code llm.invocation_failed},
+ * {@code template.render_failed} or {@code slot.not_provided} when generating the prompt fails, or the code
+ * {@code input.text_too_long} when the text exceeds the configured maximum length
+ */
+ public MetadataContent generateAuthPromptFromText(@NonNull String text, @NonNull String templateUri) {
+ return observed("generateAuthPromptFromText", () -> {
+ Objects.requireNonNull(text, "text");
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return promptGenerationOrchestrator.generateAuthPromptFromText(text, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates an authorization prompt with metadata from structured input and a data schema using the template
+ * identified by the template URI, bypassing scenario recognition.
+ *
+ *
Authorization-T slot schemas are bundled with a2a-t-resources, so the entry point works out of the box with
+ * classpath resource source.
+ *
+ * @param data structured authorization input as a string-to-object map
+ * @param schema data schema map describing the meaning of each input field; must not be null or empty
+ * @param templateUri template URI string identifying the target template, such as
+ * {@code Authorization-T/authorization-policy-management/v1}
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the template URI, data or schema is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or the schema is empty
+ * @throws net.openan.a2at.sdk.core.exception.PromptGenerationException with the code {@code template.not_found},
+ * {@code template.load_failed}, {@code slot.schema_not_found}, {@code llm.invocation_failed},
+ * {@code template.render_failed} or {@code slot.not_provided} when generating the prompt fails
+ */
+ public MetadataContent generateAuthPromptFromDataWithSchema(
+ @NonNull Map data, @NonNull Map schema, @NonNull String templateUri) {
+ return observed("generateAuthPromptFromDataWithSchema", () -> {
+ Objects.requireNonNull(data, "data");
+ Objects.requireNonNull(schema, "schema");
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return promptGenerationOrchestrator.generateAuthPromptFromDataWithSchema(data, schema, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates a notification prompt with metadata from natural-language input using the template identified by the
+ * template URI, bypassing scenario recognition.
+ *
+ * @param text natural-language notification input
+ * @param templateUri template URI string identifying the target template, such as
+ * {@code Notification-T/network-layer/subscribe-incident/v1}
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the text or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed
+ * @throws net.openan.a2at.sdk.core.exception.PromptGenerationException with the code {@code template.not_found},
+ * {@code template.load_failed}, {@code slot.schema_not_found}, {@code llm.invocation_failed},
+ * {@code template.render_failed} or {@code slot.not_provided} when generating the prompt fails, or the code
+ * {@code input.text_too_long} when the text exceeds the configured maximum length
+ */
+ public MetadataContent generateNotificationPromptFromText(@NonNull String text, @NonNull String templateUri) {
+ return observed("generateNotificationPromptFromText", () -> {
+ Objects.requireNonNull(text, "text");
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return promptGenerationOrchestrator.generateNotificationPromptFromText(text, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates a notification prompt with metadata from structured input and a data schema using the template
+ * identified by the template URI, bypassing scenario recognition.
+ *
+ * @param data structured notification input as a string-to-object map
+ * @param schema data schema map describing the meaning of each input field; must not be null or empty
+ * @param templateUri template URI string identifying the target template, such as
+ * {@code Notification-T/network-layer/subscribe-incident/v1}
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the template URI, data or schema is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or the schema is empty
+ * @throws net.openan.a2at.sdk.core.exception.PromptGenerationException with the code {@code template.not_found},
+ * {@code template.load_failed}, {@code slot.schema_not_found}, {@code llm.invocation_failed},
+ * {@code template.render_failed} or {@code slot.not_provided} when generating the prompt fails
+ */
+ public MetadataContent generateNotificationPromptFromDataWithSchema(
+ @NonNull Map data, @NonNull Map schema, @NonNull String templateUri) {
+ return observed("generateNotificationPromptFromDataWithSchema", () -> {
+ Objects.requireNonNull(data, "data");
+ Objects.requireNonNull(schema, "schema");
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return promptGenerationOrchestrator.generateNotificationPromptFromDataWithSchema(
+ data, schema, parsedTemplateUri);
+ });
}
/**
@@ -57,7 +299,7 @@ public PromptGenerationResult generateTaskPrompt(Object userInput) {
* @return transport payload representing the initial negotiation turn
*/
public Map startNegotiation(NegotiationType type, String contentText, Map facts) {
- return negotiationOrchestrator.startNegotiation(type, contentText, facts);
+ return observed("startNegotiation", () -> negotiationOrchestrator.startNegotiation(type, contentText, facts));
}
/**
@@ -68,7 +310,7 @@ public Map startNegotiation(NegotiationType type, String content
* @return normalized payload describing the receive result
*/
public Map receiveNegotiation(String message, Map context) {
- return negotiationOrchestrator.receiveNegotiation(message, context);
+ return observed("receiveNegotiation", () -> negotiationOrchestrator.receiveNegotiation(message, context));
}
/**
@@ -81,20 +323,452 @@ public Map receiveNegotiation(String message, Map continueNegotiation(
NegotiationContext context, NegotiationStatus status, String contentText) {
- return negotiationOrchestrator.continueNegotiation(context, status, contentText);
- }
-
- private A2ATConfig resolvePromptResourceLocalRootDir(A2ATConfig config, Path envPath) {
- String localRootDir = config.prompt().localRootDir();
- Path localRootPath = Path.of(localRootDir);
- Path resolvedLocalRootPath = localRootPath.isAbsolute()
- ? localRootPath.normalize()
- : envPath.getParent().resolve(localRootPath).toAbsolutePath().normalize();
- return new A2ATConfig(
- new PromptRuntimeConfig(
- config.prompt().language(), config.prompt().sourceType(), resolvedLocalRootPath.toString()),
- config.llm(),
- config.negotiation(),
- config.promptCompliance());
+ return observed("continueNegotiation", () -> {
+ Map result = negotiationOrchestrator.continueNegotiation(context, status, contentText);
+ reportNegotiationRounds(context, status);
+ return result;
+ });
+ }
+
+ /**
+ * Generates a propose-phase negotiation message from typed data.
+ *
+ * This variant is deterministic and never calls an LLM: the typed content is validated, dispatched to the
+ * generator of the negotiation type addressed by the template URI and rendered from that template.
+ *
+ * @param data typed propose input carrying the negotiation context and the typed content
+ * @param templateUri template URI string such as {@code Negotiation-T/information-negotiation/propose/v1}; its
+ * performative segment must be {@code propose}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the data, its context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or its phase or type contradicts the
+ * method or the content type
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template exists for the URI in any resource root, the code
+ * {@code negotiation.content_invalid} when a required content field is blank or empty, the code
+ * {@code negotiation.invalid_input} when the content combines a non-blank confirm request with conditional
+ * sections, or the code {@code template.render_failed} when rendering the template fails
+ */
+ public MetadataContent generateNegotiationProposePromptFromData(
+ @NonNull NegotiationProposeData data, @NonNull String templateUri) {
+ return observed("generateNegotiationProposePromptFromData", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateProposeFromData(data, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates an accept-phase negotiation message from typed data.
+ *
+ *
This variant is deterministic and never calls an LLM. The content conclusion must be {@code Accept}; a
+ * mismatched conclusion is a content error.
+ *
+ * @param data typed terminal input carrying the negotiation context and the typed ending content
+ * @param templateUri template URI string such as {@code Negotiation-T/information-negotiation/accept-reject/v1};
+ * its performative segment must be {@code accept-reject}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the data, its context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or its phase or type contradicts the
+ * method or the content
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template exists for the URI in any resource root, the code
+ * {@code negotiation.conclusion_mismatch} when the content conclusion is not {@code Accept}, the code
+ * {@code negotiation.content_invalid} when a required content field is blank or empty, or the code
+ * {@code template.render_failed} when rendering the template fails
+ */
+ public MetadataContent generateNegotiationAcceptPromptFromData(
+ @NonNull NegotiationEndingData data, @NonNull String templateUri) {
+ return observed("generateNegotiationAcceptPromptFromData", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateAcceptFromData(data, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates a reject-phase negotiation message from typed data.
+ *
+ *
This variant is deterministic and never calls an LLM. The content conclusion must be {@code Reject}; a
+ * mismatched conclusion is a content error.
+ *
+ * @param data typed terminal input carrying the negotiation context and the typed ending content
+ * @param templateUri template URI string such as {@code Negotiation-T/feasibility-negotiation/accept-reject/v1};
+ * its performative segment must be {@code accept-reject}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the data, its context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or its phase or type contradicts the
+ * method or the content
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template exists for the URI in any resource root, the code
+ * {@code negotiation.conclusion_mismatch} when the content conclusion is not {@code Reject}, the code
+ * {@code negotiation.content_invalid} when a required content field is blank or empty, or the code
+ * {@code template.render_failed} when rendering the template fails
+ */
+ public MetadataContent generateNegotiationRejectPromptFromData(
+ @NonNull NegotiationEndingData data, @NonNull String templateUri) {
+ return observed("generateNegotiationRejectPromptFromData", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateRejectFromData(data, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates an abort negotiation message from typed data.
+ *
+ *
This variant is deterministic and never calls an LLM. Abort messages are type-independent: the addressed
+ * template must be the common abort template and the content carries only the termination reason.
+ *
+ * @param data typed abort input carrying the negotiation context and the termination reason
+ * @param templateUri template URI string of the common abort template {@code Negotiation-T/common/abort/v1}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the data, its context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or does not address the common abort
+ * template
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template exists for the URI in any resource root, the code
+ * {@code negotiation.content_invalid} when the termination reason is blank, or the code
+ * {@code template.render_failed} when rendering the template fails
+ */
+ public MetadataContent generateNegotiationAbortPromptFromData(
+ @NonNull NegotiationAbortData data, @NonNull String templateUri) {
+ return observed("generateNegotiationAbortPromptFromData", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateAbortFromData(data, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates a propose-phase negotiation message from free text.
+ *
+ *
This variant runs one LLM content-extraction step constrained by the template URI and then renders
+ * deterministically like the from-data variant. The template is loaded before the LLM call and the extraction step
+ * is retried up to the configured attempt limit on the retryable failure codes
+ * {@code negotiation.content_extract_failed}, {@code llm.invocation_failed} and {@code llm.response_invalid}.
+ *
+ * @param text free-text input describing the message content
+ * @param context negotiation context carried in the {@code negotiationContext} metadata entry of the generated
+ * message without any LLM involvement
+ * @param templateUri template URI string such as {@code Negotiation-T/target-negotiation/propose/v1}; its performative
+ * segment must be {@code propose}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or its phase contradicts the method
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template or prompt resource exists for the URI and language,
+ * {@code negotiation.content_extract_failed}, {@code llm.invocation_failed} or {@code llm.response_invalid} when the extraction step fails
+ * after exhausting its retries, {@code negotiation.field_missing} when the extracted content misses a required
+ * field, or {@code negotiation.invalid_input} when the text is blank or the extracted content contradicts the
+ * phase, or the code {@code input.text_too_long} when the text exceeds the configured maximum length
+ */
+ public MetadataContent generateNegotiationProposePromptFromText(
+ @NonNull String text,
+ net.openan.a2at.sdk.core.model.@NonNull NegotiationContext context,
+ @NonNull String templateUri) {
+ return observed("generateNegotiationProposePromptFromText", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateProposeFromText(text, context, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates an accept-phase negotiation message from free text.
+ *
+ *
This variant runs one LLM content-extraction step constrained by the template URI and then renders
+ * deterministically like the from-data variant. The template is loaded before the LLM call and the extracted
+ * conclusion must be {@code Accept}.
+ *
+ * @param text free-text input describing the message content
+ * @param context negotiation context carried in the {@code negotiationContext} metadata entry of the generated
+ * message without any LLM involvement
+ * @param templateUri template URI string such as {@code Negotiation-T/information-negotiation/accept-reject/v1};
+ * its performative segment must be {@code accept-reject}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or its phase contradicts the method
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template or prompt resource exists for the URI and language,
+ * {@code negotiation.content_extract_failed}, {@code llm.invocation_failed} or {@code llm.response_invalid} when the extraction step fails
+ * after exhausting its retries, {@code negotiation.field_missing} when the extracted content misses a required
+ * field, or {@code negotiation.invalid_input} when the text is blank, {@code negotiation.conclusion_mismatch}
+ * when the extracted conclusion is not {@code Accept}, or the code {@code input.text_too_long} when the text
+ * exceeds the configured maximum length
+ */
+ public MetadataContent generateNegotiationAcceptPromptFromText(
+ @NonNull String text,
+ net.openan.a2at.sdk.core.model.@NonNull NegotiationContext context,
+ @NonNull String templateUri) {
+ return observed("generateNegotiationAcceptPromptFromText", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateAcceptFromText(text, context, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates a reject-phase negotiation message from free text.
+ *
+ *
This variant runs one LLM content-extraction step constrained by the template URI and then renders
+ * deterministically like the from-data variant. The template is loaded before the LLM call and the extracted
+ * conclusion must be {@code Reject}.
+ *
+ * @param text free-text input describing the message content
+ * @param context negotiation context carried in the {@code negotiationContext} metadata entry of the generated
+ * message without any LLM involvement
+ * @param templateUri template URI string such as {@code Negotiation-T/feasibility-negotiation/accept-reject/v1};
+ * its performative segment must be {@code accept-reject}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or its phase contradicts the method
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template or prompt resource exists for the URI and language,
+ * {@code negotiation.content_extract_failed}, {@code llm.invocation_failed} or {@code llm.response_invalid} when the extraction step fails
+ * after exhausting its retries, {@code negotiation.field_missing} when the extracted content misses a required
+ * field, or {@code negotiation.invalid_input} when the text is blank, {@code negotiation.conclusion_mismatch}
+ * when the extracted conclusion is not {@code Reject}, or the code {@code input.text_too_long} when the text
+ * exceeds the configured maximum length
+ */
+ public MetadataContent generateNegotiationRejectPromptFromText(
+ @NonNull String text,
+ net.openan.a2at.sdk.core.model.@NonNull NegotiationContext context,
+ @NonNull String templateUri) {
+ return observed("generateNegotiationRejectPromptFromText", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateRejectFromText(text, context, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Generates an abort negotiation message from free text.
+ *
+ *
This variant runs one LLM content-extraction step constrained by the common abort template and then renders
+ * deterministically like the from-data variant. The template is loaded before the LLM call and the extraction step
+ * is retried up to the configured attempt limit on the retryable failure codes
+ * {@code negotiation.content_extract_failed}, {@code llm.invocation_failed} and {@code llm.response_invalid}.
+ *
+ * @param text free-text input stating the termination reason
+ * @param context negotiation context carried in the {@code negotiationContext} metadata entry of the generated
+ * message without any LLM involvement
+ * @param templateUri template URI string of the common abort template {@code Negotiation-T/common/abort/v1}
+ * @return generated message carrying the template URI, the rendered message text and the negotiation extension URI
+ * @throws NullPointerException if the context or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or does not address the common abort
+ * template
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationGenerationException with the code
+ * {@code template.not_found} when no template or prompt resource exists for the URI and language,
+ * {@code negotiation.content_extract_failed}, {@code llm.invocation_failed} or {@code llm.response_invalid} when the extraction step fails
+ * after exhausting its retries, {@code negotiation.field_missing} when the extracted content misses the
+ * termination reason, or {@code negotiation.invalid_input} when the text is blank, or the code
+ * {@code input.text_too_long} when the text exceeds the configured maximum length
+ */
+ public MetadataContent generateNegotiationAbortPromptFromText(
+ @NonNull String text,
+ net.openan.a2at.sdk.core.model.@NonNull NegotiationContext context,
+ @NonNull String templateUri) {
+ return observed("generateNegotiationAbortPromptFromText", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.generateAbortFromText(text, context, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Lists every template available for the configured language across all A2A-T extensions.
+ *
+ *
This query never throws: the extension directories are discovered from the bundled resource tree itself, so
+ * templates of extensions added later are included automatically. Templates that exist nowhere for the language are
+ * skipped and an empty list is returned when no template can be loaded at all.
+ *
+ * @return loadable templates of the configured language across all extensions, sorted by template URI; empty when
+ * none can be loaded
+ */
+ public List getPrompts() {
+ return observed("getPrompts", () -> templateQueryService.getPrompts());
+ }
+
+ /**
+ * Loads one template by its URI, regardless of the extension.
+ *
+ * This query never throws for a well-formed template URI: a missing template yields an empty result together
+ * with a warning log instead of a failure.
+ *
+ * @param templateUri template URI string such as {@code Negotiation-T/target-negotiation/propose/v1} or
+ * {@code Task-T/network-layer/ran-energy-saving/v1}
+ * @return the addressed template, or an empty optional when no template exists for it in the configured language
+ * @throws NullPointerException if the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed
+ */
+ public Optional getPrompt(@NonNull String templateUri) {
+ return observed("getPrompt", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return templateQueryService.getPrompt(parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Validates a propose-phase negotiation message and extracts its parameters.
+ *
+ * The pipeline checks the template URI before any LLM call, runs the deterministic rule gate on the negotiation
+ * context carried in the {@code negotiationContext} metadata entry, then performs one LLM semantic validation call
+ * that also extracts the parameters, and finally merges the parameters with the context parameters taking
+ * precedence. The semantic step is retried up to the configured attempt limit on the retryable LLM infrastructure
+ * failure code.
+ *
+ * @param prompt rendered negotiation message text to validate
+ * @param context negotiation context carried alongside the message in the A2A-T metadata; {@code null} is reported
+ * as not being a negotiation message
+ * @param schema caller-provided parameter JSON schema describing the parameters to extract
+ * @param templateUri template URI string declaring the expected negotiation type and phase; its performative segment must
+ * be {@code propose}
+ * @return filled parameter data carrying the context parameters and the extracted parameters
+ * @throws NullPointerException if the schema or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or the template URI phase contradicts
+ * the method
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationParamExtractionException with the code
+ * {@code negotiation.invalid_input} when the prompt is null or blank or is not a negotiation message,
+ * {@code negotiation.rule_violation} when the negotiation context violates a rule (nested slot error codes
+ * such as {@code negotiation.invalid_context_id} or {@code negotiation.round_exceeded}), one of the closed
+ * {@code negotiation.*} code set (for example
+ * {@code negotiation.conclusion_content_mismatch}) when a rule or semantic check rejects the message,
+ * {@code negotiation.semantic_rejected} when the semantic validation rejects the message,
+ * {@code llm.invocation_failed} or {@code llm.response_invalid} when the semantic step fails after exhausting
+ * its retries, or {@code template.not_found} when the semantic validation prompt resources are missing, or
+ * {@code input.text_too_long} when the prompt exceeds the configured maximum length (A2AT_INPUT_TEXT_MAX_CHARS,
+ * default 16384)
+ */
+ public FilledParamData validateProposePromptAndDataFilling(
+ @NonNull String prompt,
+ net.openan.a2at.sdk.core.model.NegotiationContext context,
+ @NonNull Map schema,
+ @NonNull String templateUri) {
+ return observed("validateProposePromptAndDataFilling", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.validateProposePromptAndDataFilling(
+ prompt, context, schema, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Validates an accept-phase negotiation message and extracts its parameters.
+ *
+ * The pipeline is the one of {@link #validateProposePromptAndDataFilling} with the expected phase fixed to
+ * accept: the template URI must declare the {@code accept-reject} segment and the message must satisfy the
+ * accept-phase semantic constraints.
+ *
+ * @param prompt rendered negotiation message text to validate
+ * @param context negotiation context carried alongside the message in the A2A-T metadata; {@code null} is reported
+ * as not being a negotiation message
+ * @param schema caller-provided parameter JSON schema describing the parameters to extract
+ * @param templateUri template URI string declaring the expected negotiation type and phase; its performative segment must
+ * be {@code accept-reject}
+ * @return filled parameter data carrying the context parameters and the extracted parameters
+ * @throws NullPointerException if the schema or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or the template URI phase contradicts
+ * the method
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationParamExtractionException with the code
+ * {@code negotiation.invalid_input} when the prompt is null or blank or is not a negotiation message,
+ * {@code negotiation.rule_violation} when the negotiation context violates a rule (nested slot error codes
+ * such as {@code negotiation.invalid_context_id} or {@code negotiation.round_exceeded}), one of the closed
+ * {@code negotiation.*} code set (for example
+ * {@code negotiation.conclusion_content_mismatch}) when a rule or semantic check rejects the message,
+ * {@code negotiation.semantic_rejected} when the semantic validation rejects the message,
+ * {@code llm.invocation_failed} or {@code llm.response_invalid} when the semantic step fails after exhausting
+ * its retries, or {@code template.not_found} when the semantic validation prompt resources are missing, or
+ * {@code input.text_too_long} when the prompt exceeds the configured maximum length (A2AT_INPUT_TEXT_MAX_CHARS,
+ * default 16384)
+ */
+ public FilledParamData validateAcceptPromptAndDataFilling(
+ @NonNull String prompt,
+ net.openan.a2at.sdk.core.model.NegotiationContext context,
+ @NonNull Map schema,
+ @NonNull String templateUri) {
+ return observed("validateAcceptPromptAndDataFilling", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.validateAcceptPromptAndDataFilling(
+ prompt, context, schema, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Validates a reject-phase negotiation message and extracts its parameters.
+ *
+ * The pipeline is the one of {@link #validateProposePromptAndDataFilling} with the expected phase fixed to
+ * reject: the template URI must declare the {@code accept-reject} segment and the message must satisfy the
+ * reject-phase semantic constraints.
+ *
+ * @param prompt rendered negotiation message text to validate
+ * @param context negotiation context carried alongside the message in the A2A-T metadata; {@code null} is reported
+ * as not being a negotiation message
+ * @param schema caller-provided parameter JSON schema describing the parameters to extract
+ * @param templateUri template URI string declaring the expected negotiation type and phase; its performative segment must
+ * be {@code accept-reject}
+ * @return filled parameter data carrying the context parameters and the extracted parameters
+ * @throws NullPointerException if the schema or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or the template URI phase contradicts
+ * the method
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationParamExtractionException with the code
+ * {@code negotiation.invalid_input} when the prompt is null or blank or is not a negotiation message,
+ * {@code negotiation.rule_violation} when the negotiation context violates a rule (nested slot error codes
+ * such as {@code negotiation.invalid_context_id} or {@code negotiation.round_exceeded}), one of the closed
+ * {@code negotiation.*} code set (for example
+ * {@code negotiation.conclusion_content_mismatch}) when a rule or semantic check rejects the message,
+ * {@code negotiation.semantic_rejected} when the semantic validation rejects the message,
+ * {@code llm.invocation_failed} or {@code llm.response_invalid} when the semantic step fails after exhausting
+ * its retries, or {@code template.not_found} when the semantic validation prompt resources are missing, or
+ * {@code input.text_too_long} when the prompt exceeds the configured maximum length (A2AT_INPUT_TEXT_MAX_CHARS,
+ * default 16384)
+ */
+ public FilledParamData validateRejectPromptAndDataFilling(
+ @NonNull String prompt,
+ net.openan.a2at.sdk.core.model.NegotiationContext context,
+ @NonNull Map schema,
+ @NonNull String templateUri) {
+ return observed("validateRejectPromptAndDataFilling", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.validateRejectPromptAndDataFilling(
+ prompt, context, schema, parsedTemplateUri);
+ });
+ }
+
+ /**
+ * Validates an abort negotiation message and extracts its parameters.
+ *
+ * The pipeline is the one of {@link #validateProposePromptAndDataFilling} with the expected phase fixed to
+ * abort: the template URI must address the common abort template and the message must satisfy the abort-phase
+ * semantic constraints.
+ *
+ * @param prompt rendered negotiation message text to validate
+ * @param context negotiation context carried alongside the message in the A2A-T metadata; {@code null} is reported
+ * as not being a negotiation message
+ * @param schema caller-provided parameter JSON schema describing the parameters to extract
+ * @param templateUri template URI string of the common abort template {@code Negotiation-T/common/abort/v1}
+ * @return filled parameter data carrying the context parameters and the extracted parameters
+ * @throws NullPointerException if the schema or the template URI is null
+ * @throws IllegalArgumentException if the template URI is blank or malformed, or does not address the common abort
+ * template
+ * @throws net.openan.a2at.sdk.negotiation.content.NegotiationParamExtractionException with the code
+ * {@code negotiation.invalid_input} when the prompt is null or blank or is not a negotiation message,
+ * {@code negotiation.rule_violation} when the negotiation context violates a rule (nested slot error codes
+ * such as {@code negotiation.invalid_context_id} or {@code negotiation.round_exceeded}), one of the closed
+ * {@code negotiation.*} code set (for example
+ * {@code negotiation.conclusion_content_mismatch}) when a rule or semantic check rejects the message,
+ * {@code negotiation.semantic_rejected} when the semantic validation rejects the message,
+ * {@code llm.invocation_failed} or {@code llm.response_invalid} when the semantic step fails after exhausting
+ * its retries, or {@code template.not_found} when the semantic validation prompt resources are missing, or
+ * {@code input.text_too_long} when the prompt exceeds the configured maximum length (A2AT_INPUT_TEXT_MAX_CHARS,
+ * default 16384)
+ */
+ public FilledParamData validateAbortPromptAndDataFilling(
+ @NonNull String prompt,
+ net.openan.a2at.sdk.core.model.NegotiationContext context,
+ @NonNull Map schema,
+ @NonNull String templateUri) {
+ return observed("validateAbortPromptAndDataFilling", () -> {
+ TemplateUri parsedTemplateUri = parseTemplateUri(templateUri);
+ return negotiationContentService.validateAbortPromptAndDataFilling(
+ prompt, context, schema, parsedTemplateUri);
+ });
+ }
+
+ private static TemplateUri parseTemplateUri(String templateUri) {
+ Objects.requireNonNull(templateUri, "templateUri");
+ return TemplateUri.parse(templateUri)
+ .orElseThrow(() -> new IllegalArgumentException("Unparseable template URI: " + templateUri));
}
}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/package-info.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/package-info.java
index 12b0d90e..5843fe21 100644
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/package-info.java
+++ b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/package-info.java
@@ -1,2 +1,5 @@
/** Client-facing APIs for the a2a-t Java SDK. */
+@NullMarked
package net.openan.a2at.sdk.client;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/ClientPromptGenerationOrchestratorBuilder.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/ClientPromptGenerationOrchestratorBuilder.java
index 2abf1a86..c499a722 100644
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/ClientPromptGenerationOrchestratorBuilder.java
+++ b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/ClientPromptGenerationOrchestratorBuilder.java
@@ -1,20 +1,19 @@
package net.openan.a2at.sdk.client.prompt.assembly;
import java.util.List;
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
-import net.openan.a2at.sdk.client.prompt.extractor.ClientSlotValueExtractor;
-import net.openan.a2at.sdk.client.prompt.extractor.DefaultStructuredClientSlotValueExtractor;
-import net.openan.a2at.sdk.client.prompt.loader.ClientSlotSchemaLoader;
-import net.openan.a2at.sdk.client.prompt.loader.ClientTemplateLoader;
-import net.openan.a2at.sdk.client.prompt.loader.DefaultClasspathClientSlotSchemaLoader;
-import net.openan.a2at.sdk.client.prompt.loader.DefaultClasspathClientTemplateLoader;
import net.openan.a2at.sdk.client.prompt.orchestration.DefaultClientPromptGenerationOrchestrator;
-import net.openan.a2at.sdk.client.prompt.recognition.ClientScenarioRecognizer;
+import net.openan.a2at.sdk.core.model.InputLimitConfig;
import net.openan.a2at.sdk.llm.LLMClient;
+import net.openan.a2at.sdk.prompt.analysis.impl.DefaultStructuredPromptSlotValueExtractor;
+import net.openan.a2at.sdk.prompt.analysis.impl.PromptSlotValueExtractor;
+import net.openan.a2at.sdk.prompt.analysis.impl.LlmScenarioRecognizer;
import net.openan.a2at.sdk.prompt.analysis.impl.ScenarioRecognizer;
+import net.openan.a2at.sdk.prompt.resources.loader.ClasspathPromptSlotSchemaLoader;
+import net.openan.a2at.sdk.prompt.resources.loader.ClasspathPromptTemplateLoader;
+import net.openan.a2at.sdk.prompt.resources.loader.PromptSlotSchemaLoader;
+import net.openan.a2at.sdk.prompt.resources.loader.PromptTemplateTextLoader;
import net.openan.a2at.sdk.prompt.resources.model.ScenarioDefinition;
-import net.openan.a2at.sdk.prompt.taskrendering.api.TaskPromptRenderer;
+import net.openan.a2at.sdk.prompt.taskrendering.TaskPromptRenderer;
import net.openan.a2at.sdk.resources.ClasspathPromptResourceLoader;
/**
@@ -22,9 +21,10 @@
*
* @since 2026-06
*/
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ClientPromptGenerationOrchestratorBuilder {
+ private ClientPromptGenerationOrchestratorBuilder() {}
+
private LLMClient llmClient;
private List scenarios;
@@ -39,16 +39,18 @@ public final class ClientPromptGenerationOrchestratorBuilder {
private String slotUserPrompt;
- private ClientScenarioRecognizer scenarioRecognizer;
+ private ScenarioRecognizer scenarioRecognizer;
- private ClientTemplateLoader templateLoader;
+ private PromptTemplateTextLoader templateLoader;
- private ClientSlotSchemaLoader slotSchemaLoader;
+ private PromptSlotSchemaLoader slotSchemaLoader;
- private ClientSlotValueExtractor slotValueExtractor;
+ private PromptSlotValueExtractor slotValueExtractor;
private TaskPromptRenderer renderer;
+ private Integer maxTextChars;
+
private ClasspathPromptResourceLoader resourceLoader;
/**
@@ -143,7 +145,8 @@ public ClientPromptGenerationOrchestratorBuilder slotUserPrompt(String slotUserP
* @param scenarioRecognizer scenario recognizer implementation
* @return current builder
*/
- public ClientPromptGenerationOrchestratorBuilder scenarioRecognizer(ClientScenarioRecognizer scenarioRecognizer) {
+ public ClientPromptGenerationOrchestratorBuilder scenarioRecognizer(
+ ScenarioRecognizer scenarioRecognizer) {
this.scenarioRecognizer = scenarioRecognizer;
return this;
}
@@ -154,7 +157,7 @@ public ClientPromptGenerationOrchestratorBuilder scenarioRecognizer(ClientScenar
* @param templateLoader template loader implementation
* @return current builder
*/
- public ClientPromptGenerationOrchestratorBuilder templateLoader(ClientTemplateLoader templateLoader) {
+ public ClientPromptGenerationOrchestratorBuilder templateLoader(PromptTemplateTextLoader templateLoader) {
this.templateLoader = templateLoader;
return this;
}
@@ -165,7 +168,7 @@ public ClientPromptGenerationOrchestratorBuilder templateLoader(ClientTemplateLo
* @param slotSchemaLoader slot schema loader implementation
* @return current builder
*/
- public ClientPromptGenerationOrchestratorBuilder slotSchemaLoader(ClientSlotSchemaLoader slotSchemaLoader) {
+ public ClientPromptGenerationOrchestratorBuilder slotSchemaLoader(PromptSlotSchemaLoader slotSchemaLoader) {
this.slotSchemaLoader = slotSchemaLoader;
return this;
}
@@ -176,7 +179,7 @@ public ClientPromptGenerationOrchestratorBuilder slotSchemaLoader(ClientSlotSche
* @param slotValueExtractor slot extractor implementation
* @return current builder
*/
- public ClientPromptGenerationOrchestratorBuilder slotValueExtractor(ClientSlotValueExtractor slotValueExtractor) {
+ public ClientPromptGenerationOrchestratorBuilder slotValueExtractor(PromptSlotValueExtractor slotValueExtractor) {
this.slotValueExtractor = slotValueExtractor;
return this;
}
@@ -192,6 +195,18 @@ public ClientPromptGenerationOrchestratorBuilder renderer(TaskPromptRenderer ren
return this;
}
+ /**
+ * Configures the maximum accepted length in characters for free-text inputs.
+ *
+ * @param maxTextChars maximum accepted length in characters; unset falls back to
+ * {@link InputLimitConfig#DEFAULT_MAX_TEXT_CHARS}
+ * @return current builder
+ */
+ public ClientPromptGenerationOrchestratorBuilder maxTextChars(int maxTextChars) {
+ this.maxTextChars = maxTextChars;
+ return this;
+ }
+
/**
* Overrides the classpath resource loader shared by template and schema resolution.
*
@@ -221,18 +236,17 @@ public DefaultClientPromptGenerationOrchestrator build() {
// defaults.
ClasspathPromptResourceLoader effectiveResourceLoader =
resourceLoader == null ? new ClasspathPromptResourceLoader() : resourceLoader;
- ClientTemplateLoader effectiveTemplateLoader = templateLoader == null
- ? new DefaultClasspathClientTemplateLoader(effectiveResourceLoader)
- : templateLoader;
- ClientSlotSchemaLoader effectiveSlotSchemaLoader = slotSchemaLoader == null
- ? new DefaultClasspathClientSlotSchemaLoader(effectiveResourceLoader)
+ PromptTemplateTextLoader effectiveTemplateLoader =
+ templateLoader == null ? new ClasspathPromptTemplateLoader(effectiveResourceLoader) : templateLoader;
+ PromptSlotSchemaLoader effectiveSlotSchemaLoader = slotSchemaLoader == null
+ ? new ClasspathPromptSlotSchemaLoader(effectiveResourceLoader)
: slotSchemaLoader;
- ClientSlotValueExtractor effectiveSlotValueExtractor = slotValueExtractor == null
- ? new DefaultStructuredClientSlotValueExtractor(
+ PromptSlotValueExtractor effectiveSlotValueExtractor = slotValueExtractor == null
+ ? new DefaultStructuredPromptSlotValueExtractor(
llmClient, effectiveSlotSchemaLoader, slotSystemPrompt, slotUserPrompt)
: slotValueExtractor;
- ClientScenarioRecognizer effectiveScenarioRecognizer =
- scenarioRecognizer == null ? new ScenarioRecognizer(llmClient)::recognize : scenarioRecognizer;
+ ScenarioRecognizer effectiveScenarioRecognizer =
+ scenarioRecognizer == null ? new LlmScenarioRecognizer(llmClient)::recognize : scenarioRecognizer;
TaskPromptRenderer effectiveRenderer = renderer == null ? new TaskPromptRenderer() : renderer;
return new DefaultClientPromptGenerationOrchestrator(
@@ -243,7 +257,9 @@ public DefaultClientPromptGenerationOrchestrator build() {
scenarioUserPrompt,
effectiveTemplateLoader,
effectiveSlotValueExtractor,
- effectiveRenderer);
+ effectiveRenderer,
+ effectiveSlotSchemaLoader,
+ maxTextChars == null ? InputLimitConfig.DEFAULT_MAX_TEXT_CHARS : maxTextChars);
}
private static void require(Object value, String message) {
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/DefaultA2ATClientBuilder.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/DefaultA2ATClientBuilder.java
index e9f0a6f4..0206bbfb 100644
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/DefaultA2ATClientBuilder.java
+++ b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/DefaultA2ATClientBuilder.java
@@ -2,33 +2,29 @@
import java.nio.file.Path;
import java.util.List;
-import java.util.Map;
-import net.openan.a2at.sdk.client.prompt.extractor.ClientSlotValueExtractor;
-import net.openan.a2at.sdk.client.prompt.extractor.DefaultStructuredClientSlotValueExtractor;
-import net.openan.a2at.sdk.client.prompt.extractor.DefaultTemplateDrivenSlotValueExtractor;
-import net.openan.a2at.sdk.client.prompt.loader.ClientSlotSchemaLoader;
-import net.openan.a2at.sdk.client.prompt.loader.ClientTemplateLoader;
-import net.openan.a2at.sdk.client.prompt.loader.DefaultClasspathClientSlotSchemaLoader;
-import net.openan.a2at.sdk.client.prompt.loader.DefaultClasspathClientTemplateLoader;
-import net.openan.a2at.sdk.client.prompt.loader.LocalFileClientSlotSchemaLoader;
-import net.openan.a2at.sdk.client.prompt.loader.LocalFileClientTemplateLoader;
import net.openan.a2at.sdk.client.prompt.orchestration.ClientPromptGenerationOrchestrator;
-import net.openan.a2at.sdk.client.prompt.orchestration.DefaultClientPromptGenerationOrchestrator;
-import net.openan.a2at.sdk.client.prompt.recognition.ClientScenarioRecognizer;
import net.openan.a2at.sdk.core.model.A2ATConfig;
import net.openan.a2at.sdk.llm.LLMClient;
import net.openan.a2at.sdk.llm.LLMClientConfig;
import net.openan.a2at.sdk.llm.LLMClientFactory;
-import net.openan.a2at.sdk.llm.LLMConfigLoader;
+import net.openan.a2at.sdk.negotiation.generation.NegotiationContentService;
+import net.openan.a2at.sdk.negotiation.generation.NegotiationGenerationOrchestrator;
import net.openan.a2at.sdk.negotiation.runtime.NegotiationHandler;
import net.openan.a2at.sdk.negotiation.runtime.RoleBoundNegotiationOrchestrator;
import net.openan.a2at.sdk.negotiation.store.impl.InMemoryNegotiationStore;
import net.openan.a2at.sdk.negotiation.types.model.NegotiationRole;
+import net.openan.a2at.sdk.prompt.analysis.impl.DefaultStructuredPromptSlotValueExtractor;
+import net.openan.a2at.sdk.prompt.analysis.impl.LlmScenarioRecognizer;
+import net.openan.a2at.sdk.prompt.analysis.impl.PromptSlotValueExtractor;
import net.openan.a2at.sdk.prompt.analysis.impl.ScenarioRecognizer;
import net.openan.a2at.sdk.prompt.analysis.model.ScenarioRecognitionResult;
+import net.openan.a2at.sdk.prompt.resources.catalog.TemplateQueryService;
import net.openan.a2at.sdk.prompt.resources.loader.PromptResourceAccess;
+import net.openan.a2at.sdk.prompt.resources.loader.PromptSlotSchemaLoader;
+import net.openan.a2at.sdk.prompt.resources.loader.PromptTemplateTextLoader;
import net.openan.a2at.sdk.prompt.resources.model.ScenarioDefinition;
-import net.openan.a2at.sdk.prompt.taskrendering.api.TaskPromptRenderer;
+import net.openan.a2at.sdk.prompt.taskrendering.TaskPromptRenderer;
+import org.jspecify.annotations.Nullable;
/**
* Default builder that assembles one high-level A2AT client runtime from unified config.
@@ -37,10 +33,6 @@
*/
public final class DefaultA2ATClientBuilder {
- private static final String LOCAL_RULE_PROVIDER = "local_rule";
-
- private static final String OPENAI_COMPATIBLE_PROVIDER = "openai_compatible";
-
private static final String SCENARIO_RECOGNITION_PROMPT = "scenario_recognition";
private static final String SLOT_EXTRACTION_PROMPT = "slot_extraction";
@@ -49,6 +41,8 @@ public final class DefaultA2ATClientBuilder {
private Path envPath;
+ private LLMClient llmClient;
+
/**
* Creates one new builder instance.
*
@@ -80,6 +74,22 @@ public DefaultA2ATClientBuilder envPath(Path envPath) {
return this;
}
+ /**
+ * Injects an explicit LLM client that fully replaces the factory default.
+ *
+ * The injection point exists for testability and custom LLM assemblies: when set, every orchestrator built by
+ * this builder reuses the given client and no client is created from the `.env` LLM config. When unset, the builder
+ * keeps creating its default client and the behavior is unchanged.
+ *
+ * @param llmClient LLM client to inject; {@code null} keeps the factory default built from the `.env` LLM config
+ * @return current builder
+ * @since 2026-08
+ */
+ public DefaultA2ATClientBuilder llmClient(@Nullable LLMClient llmClient) {
+ this.llmClient = llmClient;
+ return this;
+ }
+
/**
* Builds the default prompt-generation orchestrator from the configured unified SDK config.
*
@@ -93,59 +103,37 @@ public ClientPromptGenerationOrchestrator buildPromptGenerationOrchestrator() {
PromptResourceAccess resources = PromptResourceAccess.create(config.prompt());
List scenarios =
resources.loadScenarios(config.prompt().language());
- if (LOCAL_RULE_PROVIDER.equals(config.llm().provider())) {
- return buildLocalRulePromptGenerationOrchestrator(resources, scenarios);
- }
- return buildLlmBackedPromptGenerationOrchestrator(resources, scenarios);
- }
+ PromptSlotSchemaLoader slotSchemaLoader = resources.slotSchemaLoader();
+ PromptTemplateTextLoader templateLoader = resources.templateLoader();
+ String language = config.prompt().language();
- private ClientPromptGenerationOrchestrator buildLocalRulePromptGenerationOrchestrator(
- PromptResourceAccess resources, List scenarios) {
- ClientSlotSchemaLoader slotSchemaLoader =
- newClientSlotSchemaLoader(resources, config.prompt().sourceType());
- return new DefaultClientPromptGenerationOrchestrator(
- new FirstScenarioRecognizer(slotSchemaLoader, config.prompt().language()),
- scenarios,
- config.prompt().language(),
- "",
- "",
- newClientTemplateLoader(resources, config.prompt().sourceType()),
- new DefaultTemplateDrivenSlotValueExtractor(slotSchemaLoader),
- new TaskPromptRenderer());
- }
+ LLMClient client = llmClient();
- private ClientPromptGenerationOrchestrator buildLlmBackedPromptGenerationOrchestrator(
- PromptResourceAccess resources, List scenarios) {
- ClientSlotSchemaLoader slotSchemaLoader =
- newClientSlotSchemaLoader(resources, config.prompt().sourceType());
- ClientTemplateLoader templateLoader =
- newClientTemplateLoader(resources, config.prompt().sourceType());
- LLMClient llmClient = createLlmClient();
- String language = config.prompt().language();
String scenarioSystemPrompt = resources.loadPrompt(SCENARIO_RECOGNITION_PROMPT, language, "system.md");
String scenarioUserPrompt = resources.loadPrompt(SCENARIO_RECOGNITION_PROMPT, language, "user.md");
String slotSystemPrompt = resources.loadPrompt(SLOT_EXTRACTION_PROMPT, language, "system.md");
String slotUserPrompt = resources.loadPrompt(SLOT_EXTRACTION_PROMPT, language, "user.md");
- ClientScenarioRecognizer llmScenarioRecognizer =
- new SingleScenarioAwareRecognizer(scenarios, new ScenarioRecognizer(llmClient)::recognize);
- ClientSlotValueExtractor slotValueExtractor = new StructuredInputAwareSlotValueExtractor(
- new DefaultTemplateDrivenSlotValueExtractor(slotSchemaLoader),
- new DefaultStructuredClientSlotValueExtractor(
- llmClient, slotSchemaLoader, slotSystemPrompt, slotUserPrompt));
+
+ ScenarioRecognizer scenarioRecognizer =
+ new SingleScenarioAwareRecognizer(scenarios, new LlmScenarioRecognizer(client)::recognize);
+ PromptSlotValueExtractor slotValueExtractor = new DefaultStructuredPromptSlotValueExtractor(
+ client, slotSchemaLoader, slotSystemPrompt, slotUserPrompt);
+
return ClientPromptGenerationOrchestratorBuilder.builder()
- .llmClient(llmClient)
+ .llmClient(client)
.scenarios(scenarios)
.language(language)
.scenarioSystemPrompt(scenarioSystemPrompt)
.scenarioUserPrompt(scenarioUserPrompt)
.slotSystemPrompt(slotSystemPrompt)
.slotUserPrompt(slotUserPrompt)
- .scenarioRecognizer(llmScenarioRecognizer)
+ .scenarioRecognizer(scenarioRecognizer)
.templateLoader(templateLoader)
.slotSchemaLoader(slotSchemaLoader)
.slotValueExtractor(slotValueExtractor)
.renderer(new TaskPromptRenderer())
+ .maxTextChars(config.inputLimits().maxTextChars())
.build();
}
@@ -164,6 +152,39 @@ public RoleBoundNegotiationOrchestrator buildNegotiationOrchestrator() {
NegotiationRole.CLIENT);
}
+ /**
+ * Builds the default negotiation content-layer orchestrator from the configured unified SDK config.
+ *
+ * The wiring is shared with the server side through
+ * {@link NegotiationContentService#buildOrchestrator(A2ATConfig, LLMClient)}: the message language and the local
+ * template root come from the prompt runtime config, the retry attempt limit comes from the LLM config.
+ *
+ * @return assembled negotiation generation orchestrator
+ */
+ public NegotiationGenerationOrchestrator buildNegotiationGenerationOrchestrator() {
+ require(config, "Unified SDK config must be configured.");
+ requireSupportedConfig();
+ require(envPath, "Unified SDK env path must be configured.");
+ return NegotiationContentService.buildOrchestrator(config, llmClient());
+ }
+
+ /**
+ * Builds the generic template query service from the configured unified SDK config.
+ *
+ *
The service answers the extension-agnostic template queries: the message language and the local template root
+ * come from the prompt runtime config, exactly like the negotiation generation orchestrator wiring.
+ *
+ * @return assembled template query service
+ */
+ public TemplateQueryService buildTemplateQueryService() {
+ require(config, "Unified SDK config must be configured.");
+ requireSupportedConfig();
+ return new TemplateQueryService(
+ config.prompt().language(),
+ config.prompt().sourceType(),
+ config.prompt().localRootDir());
+ }
+
private void requireSupportedConfig() {
if (!PromptResourceAccess.CLASSPATH_SOURCE_TYPE.equals(config.prompt().sourceType())
&& !PromptResourceAccess.LOCAL_FILE_SOURCE_TYPE.equals(
@@ -171,8 +192,7 @@ private void requireSupportedConfig() {
throw new UnsupportedOperationException(
"Unsupported prompt source type: " + config.prompt().sourceType());
}
- if (!LOCAL_RULE_PROVIDER.equals(config.llm().provider())
- && !OPENAI_COMPATIBLE_PROVIDER.equals(config.llm().provider())) {
+ if (!LLMClientFactory.availableProviders().contains(config.llm().provider())) {
throw new UnsupportedOperationException(
"Unsupported LLM provider: " + config.llm().provider());
}
@@ -182,9 +202,11 @@ private void requireSupportedConfig() {
}
}
- private LLMClient createLlmClient() {
- LLMClientConfig loadedConfig = LLMConfigLoader.load(envPath);
- return LLMClientFactory.create(loadedConfig.provider(), loadedConfig);
+ private synchronized LLMClient llmClient() {
+ if (llmClient == null) {
+ llmClient = LLMClientFactory.create(config.llm().provider(), LLMClientConfig.from(config.llm()));
+ }
+ return llmClient;
}
private static void require(Object value, String message) {
@@ -193,102 +215,24 @@ private static void require(Object value, String message) {
}
}
- private static ClientTemplateLoader newClientTemplateLoader(PromptResourceAccess resources, String sourceType) {
- if (PromptResourceAccess.CLASSPATH_SOURCE_TYPE.equals(sourceType)) {
- return new DefaultClasspathClientTemplateLoader(resources.classpathResourceLoader());
- }
- if (PromptResourceAccess.LOCAL_FILE_SOURCE_TYPE.equals(sourceType)) {
- return new LocalFileClientTemplateLoader(resources.localRootDir());
- }
- throw new UnsupportedOperationException("Unsupported prompt source type: " + sourceType);
- }
+ private static final class SingleScenarioAwareRecognizer implements ScenarioRecognizer {
- private static ClientSlotSchemaLoader newClientSlotSchemaLoader(PromptResourceAccess resources, String sourceType) {
- if (PromptResourceAccess.CLASSPATH_SOURCE_TYPE.equals(sourceType)) {
- return new DefaultClasspathClientSlotSchemaLoader(resources.classpathResourceLoader());
- }
- if (PromptResourceAccess.LOCAL_FILE_SOURCE_TYPE.equals(sourceType)) {
- return new LocalFileClientSlotSchemaLoader(resources.localRootDir());
- }
- throw new UnsupportedOperationException("Unsupported prompt source type: " + sourceType);
- }
-
- private static final class FirstScenarioRecognizer
- implements net.openan.a2at.sdk.client.prompt.recognition.ClientScenarioRecognizer {
-
- private final ClientSlotSchemaLoader slotSchemaLoader;
+ private final List scenarios;
- private final String language;
+ private final ScenarioRecognizer delegate;
- private FirstScenarioRecognizer(ClientSlotSchemaLoader slotSchemaLoader, String language) {
- this.slotSchemaLoader = slotSchemaLoader;
- this.language = language;
+ private SingleScenarioAwareRecognizer(List scenarios, ScenarioRecognizer delegate) {
+ this.scenarios = scenarios;
+ this.delegate = delegate;
}
@Override
public ScenarioRecognitionResult recognize(
String normalizedInput, List scenarios, String systemPrompt, String userPrompt) {
- if (scenarios.isEmpty()) {
- return new ScenarioRecognitionResult(false, null, "No scenarios configured.");
- }
-
- Map normalizedFacts = normalizeInput(normalizedInput);
- ScenarioDefinition bestScenario = scenarios.get(0);
- int bestScore = Integer.MIN_VALUE;
- for (ScenarioDefinition scenario : scenarios) {
- int score = scoreScenario(scenario, normalizedFacts);
- if (score > bestScore) {
- bestScenario = scenario;
- bestScore = score;
- }
- }
- return new ScenarioRecognitionResult(true, bestScenario.scenarioCode(), null);
- }
-
- private int scoreScenario(ScenarioDefinition scenario, Map normalizedFacts) {
- int score = 0;
- for (var slotDefinition : slotSchemaLoader
- .loadSlotSchema(scenario.scenarioCode(), language)
- .slotDefinitions()) {
- if (normalizedFacts.containsKey(slotDefinition.name())) {
- score += slotDefinition.required() ? 10 : 3;
- }
- }
- return score;
- }
-
- private static Map normalizeInput(String normalizedInput) {
- return Map.of("input", normalizedInput);
- }
- }
-
- private record SingleScenarioAwareRecognizer(List scenarios, ClientScenarioRecognizer delegate)
- implements ClientScenarioRecognizer {
-
- @Override
- public ScenarioRecognitionResult recognize(
- String normalizedInput,
- List ignoredScenarios,
- String systemPrompt,
- String userPrompt) {
- if (scenarios.size() == 1) {
- return new ScenarioRecognitionResult(true, scenarios.get(0).scenarioCode(), null);
- }
- return delegate.recognize(normalizedInput, scenarios, systemPrompt, userPrompt);
- }
- }
-
- private record StructuredInputAwareSlotValueExtractor(
- ClientSlotValueExtractor structuredExtractor, ClientSlotValueExtractor llmExtractor)
- implements ClientSlotValueExtractor {
-
- @Override
- public Map extractSlots(
- Object userInput, String scenarioCode, String language, String templateText) {
- if (userInput instanceof Map, ?>) {
- return structuredExtractor.extractSlots(userInput, scenarioCode, language, templateText);
+ if (this.scenarios.size() == 1) {
+ return new ScenarioRecognitionResult(true, this.scenarios.get(0).scenarioCode(), null);
}
- return llmExtractor.extractSlots(userInput, scenarioCode, language, templateText);
+ return delegate.recognize(normalizedInput, this.scenarios, systemPrompt, userPrompt);
}
}
}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/DefaultClientPromptGenerationOrchestratorFactory.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/DefaultClientPromptGenerationOrchestratorFactory.java
deleted file mode 100644
index 3e4a1fd9..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/assembly/DefaultClientPromptGenerationOrchestratorFactory.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.assembly;
-
-import java.util.List;
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
-import net.openan.a2at.sdk.client.prompt.orchestration.DefaultClientPromptGenerationOrchestrator;
-import net.openan.a2at.sdk.core.model.PromptGenerationConfig;
-import net.openan.a2at.sdk.llm.LLMClient;
-import net.openan.a2at.sdk.prompt.resources.model.ScenarioDefinition;
-
-/**
- * Default assembly entry point for client prompt generation orchestration.
- *
- * @since 2026-06
- */
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
-public final class DefaultClientPromptGenerationOrchestratorFactory {
-
- /**
- * Creates the default client prompt-generation orchestrator from the minimum required inputs.
- *
- * @param llmClient LLM client used for scenario recognition and slot extraction
- * @param scenarios supported scenario definitions
- * @param language locale identifier to load resources from
- * @param scenarioSystemPrompt system prompt for scenario recognition
- * @param scenarioUserPrompt user prompt for scenario recognition
- * @param slotSystemPrompt system prompt for slot extraction
- * @param slotUserPrompt user prompt for slot extraction
- * @return assembled default client prompt-generation orchestrator
- */
- public static DefaultClientPromptGenerationOrchestrator create(
- LLMClient llmClient,
- List scenarios,
- String language,
- String scenarioSystemPrompt,
- String scenarioUserPrompt,
- String slotSystemPrompt,
- String slotUserPrompt) {
- return create(
- llmClient,
- scenarios,
- new PromptGenerationConfig(
- language, scenarioSystemPrompt, scenarioUserPrompt, slotSystemPrompt, slotUserPrompt));
- }
-
- /**
- * Creates the default client prompt-generation orchestrator from one shared prompt-generation config bundle.
- *
- * @param llmClient LLM client used for scenario recognition and slot extraction
- * @param scenarios supported scenario definitions
- * @param config shared prompt-generation config bundle
- * @return assembled default client prompt-generation orchestrator
- */
- public static DefaultClientPromptGenerationOrchestrator create(
- LLMClient llmClient, List scenarios, PromptGenerationConfig config) {
- return ClientPromptGenerationOrchestratorBuilder.builder()
- .llmClient(llmClient)
- .scenarios(scenarios)
- .language(config.language())
- .scenarioSystemPrompt(config.scenarioSystemPrompt())
- .scenarioUserPrompt(config.scenarioUserPrompt())
- .slotSystemPrompt(config.slotSystemPrompt())
- .slotUserPrompt(config.slotUserPrompt())
- .build();
- }
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/ClientSlotValueExtractor.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/ClientSlotValueExtractor.java
deleted file mode 100644
index 93dd4d20..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/ClientSlotValueExtractor.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.extractor;
-
-import java.util.Map;
-
-/**
- * Extracts slot values for one scenario and language.
- *
- * @since 2026-06
- */
-@FunctionalInterface
-public interface ClientSlotValueExtractor {
-
- /**
- * Extracts normalized slot values for the supplied template context.
- *
- * @param userInput user-provided task description or structured input object
- * @param scenarioCode scenario code currently being rendered
- * @param language locale identifier of the backing resources
- * @param templateText resolved task template text
- * @return normalized slot values keyed by slot name
- */
- Map extractSlots(Object userInput, String scenarioCode, String language, String templateText);
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/DefaultStructuredClientSlotValueExtractor.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/DefaultStructuredClientSlotValueExtractor.java
deleted file mode 100644
index ed910153..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/DefaultStructuredClientSlotValueExtractor.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.extractor;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-import net.openan.a2at.sdk.client.prompt.loader.ClientSlotSchemaLoader;
-import net.openan.a2at.sdk.llm.LLMClient;
-import net.openan.a2at.sdk.prompt.analysis.impl.DefaultStructuredPromptSlotValueExtractor;
-import net.openan.a2at.sdk.prompt.analysis.model.StructuredSlotExtractionResult;
-
-/**
- * LLM-backed default slot extractor that requests structured slot payloads.
- *
- * @since 2026-06
- */
-public final class DefaultStructuredClientSlotValueExtractor implements ClientSlotValueExtractor {
-
- private final DefaultStructuredPromptSlotValueExtractor delegate;
-
- /**
- * Creates an LLM-backed slot extractor.
- *
- * @param llmClient LLM client used for slot extraction
- * @param slotSchemaLoader slot schema loader
- * @param systemPrompt system prompt for extraction
- * @param userPrompt user prompt for extraction
- */
- public DefaultStructuredClientSlotValueExtractor(
- LLMClient llmClient, ClientSlotSchemaLoader slotSchemaLoader, String systemPrompt, String userPrompt) {
- this.delegate = new DefaultStructuredPromptSlotValueExtractor(
- llmClient,
- (scenarioCode, language) -> slotSchemaLoader.loadSlotSchema(scenarioCode, language),
- systemPrompt,
- userPrompt);
- }
-
- @Override
- public Map extractSlots(
- Object userInput, String scenarioCode, String language, String templateText) {
- StructuredSlotExtractionResult result = delegate.extractSlots(userInput, scenarioCode, language);
- return new LinkedHashMap<>(result.slots());
- }
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/DefaultTemplateDrivenSlotValueExtractor.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/DefaultTemplateDrivenSlotValueExtractor.java
deleted file mode 100644
index 4f1bc4f7..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/extractor/DefaultTemplateDrivenSlotValueExtractor.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.extractor;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import net.openan.a2at.sdk.client.prompt.loader.ClientSlotSchemaLoader;
-import net.openan.a2at.sdk.client.prompt.loader.DefaultClasspathClientSlotSchemaLoader;
-import net.openan.a2at.sdk.prompt.resources.model.PromptSlotDefinition;
-import net.openan.a2at.sdk.prompt.resources.model.PromptSlotSchema;
-import net.openan.a2at.sdk.resources.ClasspathPromptResourceLoader;
-
-/**
- * Minimal slot extractor that derives output keys from slot schema and applies lightweight constraints.
- *
- * @since 2026-06
- */
-public final class DefaultTemplateDrivenSlotValueExtractor implements ClientSlotValueExtractor {
-
- private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{?\\s*([^{}]+?)\\s*\\}\\}?");
-
- private final ClientSlotSchemaLoader slotSchemaLoader;
-
- /**
- * Creates a default schema-driven extractor backed by packaged slot resources.
- */
- public DefaultTemplateDrivenSlotValueExtractor() {
- this(new DefaultClasspathClientSlotSchemaLoader(new ClasspathPromptResourceLoader()));
- }
-
- /**
- * Creates a schema-driven extractor backed by the provided slot schema loader.
- *
- * @param slotSchemaLoader slot schema loader
- */
- public DefaultTemplateDrivenSlotValueExtractor(ClientSlotSchemaLoader slotSchemaLoader) {
- this.slotSchemaLoader = slotSchemaLoader;
- }
-
- @Override
- public Map extractSlots(
- Object userInput, String scenarioCode, String language, String templateText) {
- Map slots = new LinkedHashMap<>();
- Map templateSlotNames = referencedTemplateSlots(templateText);
- PromptSlotSchema schema = slotSchemaLoader.loadSlotSchema(scenarioCode, language);
- for (PromptSlotDefinition definition : schema.slotDefinitions()) {
- if (!definition.required() && !templateSlotNames.containsKey(definition.name())) {
- continue;
- }
- String value = normalizeAndValidate(resolveSlotValue(userInput, definition.name()), definition);
- slots.put(definition.name(), value);
- }
- return slots;
- }
-
- private static Map referencedTemplateSlots(String templateText) {
- Map slotNames = new LinkedHashMap<>();
- Matcher matcher = PLACEHOLDER_PATTERN.matcher(templateText);
- while (matcher.find()) {
- slotNames.putIfAbsent(matcher.group(1).trim(), Boolean.TRUE);
- }
- return slotNames;
- }
-
- private static String resolveSlotValue(Object userInput, String slotName) {
- if (userInput instanceof Map, ?> mapValue) {
- Object rawValue = mapValue.get(slotName);
- return rawValue == null ? "" : String.valueOf(rawValue);
- }
- if ("input".equals(slotName)) {
- return String.valueOf(userInput);
- }
- return "";
- }
-
- private static String normalizeAndValidate(String value, PromptSlotDefinition definition) {
- String normalized = value == null ? "" : value.trim();
- if (normalized.isBlank()) {
- return "";
- }
- if (definition.allowedValues() != null
- && !definition.allowedValues().isEmpty()
- && !definition.allowedValues().contains(normalized)) {
- return "";
- }
- if (definition.pattern() != null
- && !definition.pattern().isBlank()
- && !normalized.matches(definition.pattern())) {
- return "";
- }
- if ("integer".equalsIgnoreCase(definition.jsonType()) || "number".equalsIgnoreCase(definition.jsonType())) {
- try {
- double numericValue = Double.parseDouble(normalized);
- if (definition.minimum() != null && numericValue < definition.minimum()) {
- return "";
- }
- if (definition.maximum() != null && numericValue > definition.maximum()) {
- return "";
- }
- if ("integer".equalsIgnoreCase(definition.jsonType())) {
- return String.valueOf((int) numericValue);
- }
- return stripTrailingZero(numericValue);
- } catch (NumberFormatException error) {
- return "";
- }
- }
- return normalized;
- }
-
- private static String stripTrailingZero(double value) {
- if (Math.rint(value) == value) {
- return String.valueOf((long) value);
- }
- return String.valueOf(value);
- }
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/ClientSlotSchemaLoader.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/ClientSlotSchemaLoader.java
deleted file mode 100644
index e801f10f..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/ClientSlotSchemaLoader.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.loader;
-
-import net.openan.a2at.sdk.prompt.resources.model.PromptSlotSchema;
-
-/**
- * Loads slot schema definitions for client-side prompt generation.
- *
- * @since 2026-06
- */
-@FunctionalInterface
-public interface ClientSlotSchemaLoader {
-
- /**
- * Loads the slot schema for one scenario and language.
- *
- * @param scenarioCode scenario code to resolve
- * @param language locale identifier to resolve
- * @return slot schema for the requested scenario
- */
- PromptSlotSchema loadSlotSchema(String scenarioCode, String language);
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/ClientTemplateLoader.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/ClientTemplateLoader.java
deleted file mode 100644
index 96161154..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/ClientTemplateLoader.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.loader;
-
-/**
- * Loads prompt templates for client-side prompt generation.
- *
- * @since 2026-06
- */
-@FunctionalInterface
-public interface ClientTemplateLoader {
-
- /**
- * Loads one prompt template for the requested scenario and language.
- *
- * @param scenarioCode scenario code to resolve
- * @param language locale identifier to resolve
- * @return template text
- */
- String loadTemplate(String scenarioCode, String language);
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/DefaultClasspathClientSlotSchemaLoader.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/DefaultClasspathClientSlotSchemaLoader.java
deleted file mode 100644
index f7b5d08b..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/DefaultClasspathClientSlotSchemaLoader.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.loader;
-
-import net.openan.a2at.sdk.prompt.resources.loader.ClasspathPromptSlotSchemaLoader;
-import net.openan.a2at.sdk.prompt.resources.model.PromptSlotSchema;
-import net.openan.a2at.sdk.resources.ClasspathPromptResourceLoader;
-
-/**
- * Default slot schema loader backed by packaged classpath resources.
- *
- * @since 2026-06
- */
-public final class DefaultClasspathClientSlotSchemaLoader implements ClientSlotSchemaLoader {
-
- private final ClasspathPromptSlotSchemaLoader delegate;
-
- /**
- * Creates a slot schema loader backed by one classpath resource loader.
- *
- * @param resourceLoader shared resource loader
- */
- public DefaultClasspathClientSlotSchemaLoader(ClasspathPromptResourceLoader resourceLoader) {
- this.delegate = new ClasspathPromptSlotSchemaLoader(resourceLoader);
- }
-
- @Override
- public PromptSlotSchema loadSlotSchema(String scenarioCode, String language) {
- return delegate.loadSlotSchema(scenarioCode, language);
- }
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/DefaultClasspathClientTemplateLoader.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/DefaultClasspathClientTemplateLoader.java
deleted file mode 100644
index e98501b7..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/DefaultClasspathClientTemplateLoader.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.loader;
-
-import net.openan.a2at.sdk.prompt.resources.loader.ClasspathPromptTemplateLoader;
-import net.openan.a2at.sdk.resources.ClasspathPromptResourceLoader;
-
-/**
- * Default template loader backed by packaged classpath resources.
- *
- * @since 2026-06
- */
-public final class DefaultClasspathClientTemplateLoader implements ClientTemplateLoader {
-
- private final ClasspathPromptTemplateLoader delegate;
-
- /**
- * Creates a template loader backed by one classpath resource loader.
- *
- * @param resourceLoader shared resource loader
- */
- public DefaultClasspathClientTemplateLoader(ClasspathPromptResourceLoader resourceLoader) {
- this.delegate = new ClasspathPromptTemplateLoader(resourceLoader);
- }
-
- @Override
- public String loadTemplate(String scenarioCode, String language) {
- return delegate.loadTemplate(scenarioCode, language);
- }
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/LocalFileClientSlotSchemaLoader.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/LocalFileClientSlotSchemaLoader.java
deleted file mode 100644
index 51ea22de..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/LocalFileClientSlotSchemaLoader.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.loader;
-
-import java.nio.file.Path;
-import net.openan.a2at.sdk.prompt.resources.loader.LocalFilePromptSlotSchemaLoader;
-import net.openan.a2at.sdk.prompt.resources.model.PromptSlotSchema;
-
-/**
- * Loads slot schema definitions from one local prompt resource root.
- *
- * @since 2026-06
- */
-public final class LocalFileClientSlotSchemaLoader implements ClientSlotSchemaLoader {
- private final LocalFilePromptSlotSchemaLoader delegate;
-
- /**
- * Creates one local slot schema loader.
- *
- * @param promptRootDir prompt resource root directory
- */
- public LocalFileClientSlotSchemaLoader(Path promptRootDir) {
- this.delegate = new LocalFilePromptSlotSchemaLoader(promptRootDir);
- }
-
- @Override
- public PromptSlotSchema loadSlotSchema(String scenarioCode, String language) {
- return delegate.loadSlotSchema(scenarioCode, language);
- }
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/LocalFileClientTemplateLoader.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/LocalFileClientTemplateLoader.java
deleted file mode 100644
index ad6a9f0d..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/loader/LocalFileClientTemplateLoader.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.loader;
-
-import java.nio.file.Path;
-import net.openan.a2at.sdk.prompt.resources.loader.LocalFilePromptTemplateLoader;
-
-/**
- * Loads client templates from one local prompt resource root.
- *
- * @since 2026-06
- */
-public final class LocalFileClientTemplateLoader implements ClientTemplateLoader {
- private final LocalFilePromptTemplateLoader delegate;
-
- /**
- * Creates one local template loader.
- *
- * @param promptRootDir prompt resource root directory
- */
- public LocalFileClientTemplateLoader(Path promptRootDir) {
- this.delegate = new LocalFilePromptTemplateLoader(promptRootDir);
- }
-
- @Override
- public String loadTemplate(String scenarioCode, String language) {
- return delegate.loadTemplate(scenarioCode, language);
- }
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/ClientPromptGenerationOrchestrator.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/ClientPromptGenerationOrchestrator.java
index 4f317beb..21e59af5 100644
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/ClientPromptGenerationOrchestrator.java
+++ b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/ClientPromptGenerationOrchestrator.java
@@ -1,6 +1,9 @@
package net.openan.a2at.sdk.client.prompt.orchestration;
+import java.util.Map;
import net.openan.a2at.sdk.client.model.PromptGenerationResult;
+import net.openan.a2at.sdk.core.model.MetadataContent;
+import net.openan.a2at.sdk.core.model.TemplateUri;
/**
* Internal prompt-generation orchestration contract used by the client facade.
@@ -12,8 +15,90 @@ public interface ClientPromptGenerationOrchestrator {
/**
* Generates a processed task prompt from user input.
*
- * @param userInput raw or structured user input
+ * @param userInput raw or structured user input; a String input longer than the configured maximum length fails
+ * fast with the code {@code input.text_too_long}
* @return prompt-generation result
*/
PromptGenerationResult generateTaskPrompt(Object userInput);
+
+ /**
+ * Generates a task prompt with metadata from natural-language input using the template identified by the template
+ * URI, bypassing scenario recognition.
+ *
+ * @param text natural-language task input
+ * @param templateUri template URI identifying the target template
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the text or template URI is null
+ * @throws net.openan.a2at.sdk.core.exception.PromptGenerationException with the code {@code input.text_too_long}
+ * when the text exceeds the configured maximum length
+ */
+ MetadataContent generateTaskPromptFromText(String text, TemplateUri templateUri);
+
+ /**
+ * Generates a task prompt with metadata from structured input and a data schema using the template identified by
+ * the template URI, bypassing scenario recognition.
+ *
+ * @param data structured task input as a string-to-object map
+ * @param schema data schema map describing the meaning of each input field; must not be null or empty
+ * @param templateUri template URI identifying the target template
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the template URI, data or schema is null
+ * @throws IllegalArgumentException if the schema is empty
+ */
+ MetadataContent generateTaskPromptFromDataWithSchema(
+ Map data, Map schema, TemplateUri templateUri);
+
+ /**
+ * Generates an authorization prompt with metadata from natural-language input using the template identified by the
+ * template URI, bypassing scenario recognition.
+ *
+ * @param text natural-language authorization input
+ * @param templateUri template URI identifying the target template
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the text or template URI is null
+ * @throws net.openan.a2at.sdk.core.exception.PromptGenerationException with the code {@code input.text_too_long}
+ * when the text exceeds the configured maximum length
+ */
+ MetadataContent generateAuthPromptFromText(String text, TemplateUri templateUri);
+
+ /**
+ * Generates an authorization prompt with metadata from structured input and a data schema using the template
+ * identified by the template URI, bypassing scenario recognition.
+ *
+ * @param data structured authorization input as a string-to-object map
+ * @param schema data schema map describing the meaning of each input field; must not be null or empty
+ * @param templateUri template URI identifying the target template
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the template URI, data or schema is null
+ * @throws IllegalArgumentException if the schema is empty
+ */
+ MetadataContent generateAuthPromptFromDataWithSchema(
+ Map data, Map schema, TemplateUri templateUri);
+
+ /**
+ * Generates a notification prompt with metadata from natural-language input using the template identified by the
+ * template URI, bypassing scenario recognition.
+ *
+ * @param text natural-language notification input
+ * @param templateUri template URI identifying the target template
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the text or template URI is null
+ * @throws net.openan.a2at.sdk.core.exception.PromptGenerationException with the code {@code input.text_too_long}
+ * when the text exceeds the configured maximum length
+ */
+ MetadataContent generateNotificationPromptFromText(String text, TemplateUri templateUri);
+
+ /**
+ * Generates a notification prompt with metadata from structured input and a data schema using the template
+ * identified by the template URI, bypassing scenario recognition.
+ *
+ * @param data structured notification input as a string-to-object map
+ * @param schema data schema map describing the meaning of each input field; must not be null or empty
+ * @param templateUri template URI identifying the target template
+ * @return metadata content carrying the resolved template URI, rendered prompt text, and extension URI
+ * @throws NullPointerException if the template URI, data or schema is null
+ * @throws IllegalArgumentException if the schema is empty
+ */
+ MetadataContent generateNotificationPromptFromDataWithSchema(
+ Map data, Map schema, TemplateUri templateUri);
}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/DefaultClientPromptGenerationOrchestrator.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/DefaultClientPromptGenerationOrchestrator.java
index 08fb571f..eb7fbc10 100644
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/DefaultClientPromptGenerationOrchestrator.java
+++ b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/orchestration/DefaultClientPromptGenerationOrchestrator.java
@@ -1,17 +1,42 @@
package net.openan.a2at.sdk.client.prompt.orchestration;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import net.openan.a2at.sdk.client.model.PromptGenerationFailure;
import net.openan.a2at.sdk.client.model.PromptGenerationResult;
-import net.openan.a2at.sdk.client.prompt.extractor.ClientSlotValueExtractor;
-import net.openan.a2at.sdk.client.prompt.loader.ClientTemplateLoader;
-import net.openan.a2at.sdk.client.prompt.recognition.ClientScenarioRecognizer;
+import net.openan.a2at.sdk.core.exception.A2ATError;
+import net.openan.a2at.sdk.core.exception.ErrorCatalog;
+import net.openan.a2at.sdk.core.exception.ErrorMessages;
+import net.openan.a2at.sdk.core.exception.PromptGenerationException;
import net.openan.a2at.sdk.core.exception.ResourceNotFoundException;
+import net.openan.a2at.sdk.core.model.ExtensionUriConstants;
+import net.openan.a2at.sdk.core.model.InputLimitConfig;
+import net.openan.a2at.sdk.core.model.MetadataContent;
+import net.openan.a2at.sdk.core.model.SlotValidationError;
+import net.openan.a2at.sdk.core.model.TemplateUri;
+import net.openan.a2at.sdk.llm.LLMConfigError;
+import net.openan.a2at.sdk.llm.LLMError;
+import net.openan.a2at.sdk.prompt.analysis.exception.ScenarioRecognitionException;
+import net.openan.a2at.sdk.prompt.analysis.impl.PromptSlotValueExtractor;
+import net.openan.a2at.sdk.prompt.analysis.impl.ScenarioRecognizer;
import net.openan.a2at.sdk.prompt.analysis.model.ScenarioRecognitionResult;
+import net.openan.a2at.sdk.prompt.analysis.model.StructuredSlotExtractionResult;
+import net.openan.a2at.sdk.prompt.analysis.model.StructuredSlotValidationError;
+import net.openan.a2at.sdk.prompt.resources.loader.PromptSlotSchemaLoader;
+import net.openan.a2at.sdk.prompt.resources.loader.PromptTemplateTextLoader;
+import net.openan.a2at.sdk.prompt.resources.model.PromptSlotDefinition;
+import net.openan.a2at.sdk.prompt.resources.model.PromptSlotSchema;
import net.openan.a2at.sdk.prompt.resources.model.ScenarioDefinition;
-import net.openan.a2at.sdk.prompt.taskrendering.api.TaskPromptRenderer;
+import net.openan.a2at.sdk.prompt.taskrendering.TaskPromptRenderer;
import net.openan.a2at.sdk.prompt.taskrendering.exception.TaskPromptRenderException;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Minimal runnable client prompt generation orchestrator.
@@ -20,7 +45,24 @@
*/
public final class DefaultClientPromptGenerationOrchestrator implements ClientPromptGenerationOrchestrator {
- private final ClientScenarioRecognizer scenarioRecognizer;
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClientPromptGenerationOrchestrator.class);
+
+ /** Old slot-extraction error code for a required slot that could not be extracted. */
+ private static final String LEGACY_CODE_MISSING_INPUT = "missing_input";
+
+ /** Old slot-extraction error code for a value violating a closed constraint. */
+ private static final String LEGACY_CODE_INVALID_VALUE = "invalid_value";
+
+ /** Step label used when the scenario-recognition LLM call fails. */
+ private static final String STEP_SCENARIO_RECOGNITION = "scenario recognition";
+
+ /** Step label used when the slot-extraction LLM call fails. */
+ private static final String STEP_SLOT_EXTRACTION = "slot extraction";
+
+ private static final List RESPONSE_CONTRACT_MARKERS = List.of(
+ "returned invalid json", "returned empty content", "must return a json object", "response did not include");
+
+ private final ScenarioRecognizer scenarioRecognizer;
private final List scenarios;
@@ -30,13 +72,15 @@ public final class DefaultClientPromptGenerationOrchestrator implements ClientPr
private final String userPrompt;
- private final ClientTemplateLoader templateLoader;
+ private final PromptTemplateTextLoader templateLoader;
+
+ private final PromptSlotValueExtractor slotValueExtractor;
- private final ClientSlotValueExtractor slotValueExtractor;
+ private final PromptSlotSchemaLoader slotSchemaLoader;
private final TaskPromptRenderer renderer;
- private String lastNormalizedInput;
+ private final int maxTextChars;
/**
* Creates a client prompt-generation orchestrator with explicit collaborators.
@@ -49,16 +93,56 @@ public final class DefaultClientPromptGenerationOrchestrator implements ClientPr
* @param templateLoader template loader
* @param slotValueExtractor slot value extractor
* @param renderer task prompt renderer
+ * @param slotSchemaLoader slot schema loader
+ */
+ public DefaultClientPromptGenerationOrchestrator(
+ ScenarioRecognizer scenarioRecognizer,
+ List scenarios,
+ String language,
+ String systemPrompt,
+ String userPrompt,
+ PromptTemplateTextLoader templateLoader,
+ PromptSlotValueExtractor slotValueExtractor,
+ TaskPromptRenderer renderer,
+ PromptSlotSchemaLoader slotSchemaLoader) {
+ this(
+ scenarioRecognizer,
+ scenarios,
+ language,
+ systemPrompt,
+ userPrompt,
+ templateLoader,
+ slotValueExtractor,
+ renderer,
+ slotSchemaLoader,
+ InputLimitConfig.DEFAULT_MAX_TEXT_CHARS);
+ }
+
+ /**
+ * Creates a client prompt-generation orchestrator with explicit collaborators and one free-text input length limit.
+ *
+ * @param scenarioRecognizer scenario recognizer
+ * @param scenarios supported scenario definitions
+ * @param language locale identifier for resource lookup
+ * @param systemPrompt system prompt for scenario recognition
+ * @param userPrompt user prompt for scenario recognition
+ * @param templateLoader template loader
+ * @param slotValueExtractor slot value extractor
+ * @param renderer task prompt renderer
+ * @param slotSchemaLoader slot schema loader
+ * @param maxTextChars maximum accepted length in characters for free-text inputs
*/
public DefaultClientPromptGenerationOrchestrator(
- ClientScenarioRecognizer scenarioRecognizer,
+ ScenarioRecognizer scenarioRecognizer,
List scenarios,
String language,
String systemPrompt,
String userPrompt,
- ClientTemplateLoader templateLoader,
- ClientSlotValueExtractor slotValueExtractor,
- TaskPromptRenderer renderer) {
+ PromptTemplateTextLoader templateLoader,
+ PromptSlotValueExtractor slotValueExtractor,
+ TaskPromptRenderer renderer,
+ PromptSlotSchemaLoader slotSchemaLoader,
+ int maxTextChars) {
this.scenarioRecognizer = scenarioRecognizer;
this.scenarios = scenarios;
this.language = language;
@@ -67,49 +151,380 @@ public DefaultClientPromptGenerationOrchestrator(
this.templateLoader = templateLoader;
this.slotValueExtractor = slotValueExtractor;
this.renderer = renderer;
+ this.slotSchemaLoader = slotSchemaLoader;
+ this.maxTextChars = maxTextChars;
}
@Override
public PromptGenerationResult generateTaskPrompt(Object userInput) {
+ if (userInput instanceof String text && InputLimitConfig.isTooLong(text, maxTextChars)) {
+ return failure(
+ ErrorCatalog.INPUT_TEXT_TOO_LONG,
+ Map.of("actual_length", String.valueOf(text.length()), "max_chars", String.valueOf(maxTextChars)),
+ "input");
+ }
String normalizedInput = String.valueOf(userInput);
- this.lastNormalizedInput = normalizedInput;
final ScenarioRecognitionResult recognition;
try {
recognition = scenarioRecognizer.recognize(normalizedInput, scenarios, systemPrompt, userPrompt);
} catch (ResourceNotFoundException error) {
- return PromptGenerationResult.failure(
- new PromptGenerationFailure("prompt_resource_load_error", error.getMessage(), "generation"));
+ return failure(
+ ErrorCatalog.TEMPLATE_LOAD_FAILED, Map.of("resource_path", error.resourcePath()), "generation");
+ } catch (ScenarioRecognitionException error) {
+ return failure(ErrorCatalog.SCENARIO_NOT_MATCHED, Map.of("reason", error.getMessage()), "scenario");
+ } catch (LLMError error) {
+ return PromptGenerationResult.failure(llmFailure(error, STEP_SCENARIO_RECOGNITION));
}
if (!recognition.matched()
|| recognition.scenarioCode() == null
|| recognition.scenarioCode().isBlank()) {
- return PromptGenerationResult.failure(new PromptGenerationFailure(
- "scenario_not_matched",
- recognition.errorMessage() == null ? "Scenario recognition failed." : recognition.errorMessage(),
- "scenario"));
+ return failure(
+ ErrorCatalog.SCENARIO_NOT_MATCHED,
+ Map.of(
+ "reason",
+ recognition.errorMessage() == null
+ ? "Scenario recognition failed."
+ : recognition.errorMessage()),
+ "scenario");
}
final String templateText;
try {
templateText = templateLoader.loadTemplate(recognition.scenarioCode(), language);
} catch (ResourceNotFoundException error) {
- return PromptGenerationResult.failure(
- new PromptGenerationFailure("template_not_found", error.getMessage(), "generation"));
+ return failure(
+ ErrorCatalog.TEMPLATE_NOT_FOUND,
+ Map.of("template_uri", recognition.scenarioCode(), "language", language),
+ "generation");
+ } catch (A2ATError error) {
+ return failure(
+ ErrorCatalog.TEMPLATE_LOAD_FAILED,
+ Map.of("resource_path", recognition.scenarioCode()),
+ "generation");
}
try {
- Map slots =
- slotValueExtractor.extractSlots(userInput, recognition.scenarioCode(), language, templateText);
+ StructuredSlotExtractionResult extractionResult =
+ slotValueExtractor.extractSlots(userInput, recognition.scenarioCode(), language);
+ PromptGenerationFailure slotFailure = slotExtractionFailure(extractionResult.slotErrors());
+ if (slotFailure != null) {
+ return PromptGenerationResult.failure(slotFailure);
+ }
+ Map slots = extractionResult.slots();
String renderedPrompt = renderer.render(templateText, slots);
return PromptGenerationResult.success(renderedPrompt);
+ } catch (ResourceNotFoundException error) {
+ return failure(
+ ErrorCatalog.SLOT_SCHEMA_NOT_FOUND,
+ Map.of("template_uri", recognition.scenarioCode(), "language", language),
+ "generation");
+ } catch (LLMError error) {
+ return PromptGenerationResult.failure(llmFailure(error, STEP_SLOT_EXTRACTION));
} catch (TaskPromptRenderException error) {
- return PromptGenerationResult.failure(
- new PromptGenerationFailure("render_failed", error.getMessage(), "generation"));
+ return failure(
+ ErrorCatalog.TEMPLATE_RENDER_FAILED,
+ Map.of("template_uri", recognition.scenarioCode(), "reason", error.getMessage()),
+ "generation");
+ } catch (A2ATError error) {
+ return failure(ErrorCatalog.LLM_INVOCATION_FAILED, Map.of("reason", error.getMessage()), "generation");
+ }
+ }
+
+ @Override
+ public MetadataContent generateTaskPromptFromText(String text, TemplateUri templateUri) {
+ return generateFromTemplateUriWithMetadata(text, templateUri, ExtensionUriConstants.TASK_T_EXTENSION_URI);
+ }
+
+ @Override
+ public MetadataContent generateTaskPromptFromDataWithSchema(
+ Map data, Map schema, TemplateUri templateUri) {
+ return generateFromDataWithSchema(data, schema, templateUri, ExtensionUriConstants.TASK_T_EXTENSION_URI);
+ }
+
+ @Override
+ public MetadataContent generateAuthPromptFromText(String text, TemplateUri templateUri) {
+ return generateFromTemplateUriWithMetadata(
+ text, templateUri, ExtensionUriConstants.AUTHORIZATION_T_EXTENSION_URI);
+ }
+
+ @Override
+ public MetadataContent generateAuthPromptFromDataWithSchema(
+ Map data, Map schema, TemplateUri templateUri) {
+ return generateFromDataWithSchema(
+ data, schema, templateUri, ExtensionUriConstants.AUTHORIZATION_T_EXTENSION_URI);
+ }
+
+ @Override
+ public MetadataContent generateNotificationPromptFromText(String text, TemplateUri templateUri) {
+ return generateFromTemplateUriWithMetadata(
+ text, templateUri, ExtensionUriConstants.NOTIFICATION_T_EXTENSION_URI);
+ }
+
+ @Override
+ public MetadataContent generateNotificationPromptFromDataWithSchema(
+ Map data, Map schema, TemplateUri templateUri) {
+ return generateFromDataWithSchema(
+ data, schema, templateUri, ExtensionUriConstants.NOTIFICATION_T_EXTENSION_URI);
+ }
+
+ private MetadataContent generateFromTemplateUriWithMetadata(
+ String userInput, TemplateUri templateUri, String extensionUri) {
+ Objects.requireNonNull(userInput, "userInput");
+ if (InputLimitConfig.isTooLong(userInput, maxTextChars)) {
+ throw new PromptGenerationException(
+ ErrorCatalog.INPUT_TEXT_TOO_LONG.getCode(),
+ render(
+ ErrorCatalog.INPUT_TEXT_TOO_LONG,
+ Map.of(
+ "actual_length",
+ String.valueOf(userInput.length()),
+ "max_chars",
+ String.valueOf(maxTextChars))));
+ }
+ return generateWithMetadata(
+ templateUri,
+ extensionUri,
+ templateIdentifier -> slotValueExtractor.extractSlots(userInput, templateIdentifier, language));
+ }
+
+ private MetadataContent generateFromDataWithSchema(
+ Map data, Map schema, TemplateUri templateUri, String extensionUri) {
+ Objects.requireNonNull(data, "data");
+ Objects.requireNonNull(schema, "schema");
+ if (schema.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Data schema must not be empty; it describes the meaning of each input field.");
}
+ return generateWithMetadata(
+ templateUri,
+ extensionUri,
+ templateIdentifier -> slotValueExtractor.extractSlots(data, templateIdentifier, language, schema));
+ }
+
+ private MetadataContent generateWithMetadata(
+ TemplateUri templateUri,
+ String extensionUri,
+ Function slotExtractor) {
+ Objects.requireNonNull(templateUri, "templateUri");
+ String templateIdentifier = templateUri.uri();
+ final String templateText;
+ try {
+ templateText = templateLoader.loadTemplate(templateIdentifier, language);
+ } catch (ResourceNotFoundException e) {
+ throw new PromptGenerationException(
+ ErrorCatalog.TEMPLATE_NOT_FOUND.getCode(),
+ render(
+ ErrorCatalog.TEMPLATE_NOT_FOUND,
+ Map.of("template_uri", templateIdentifier, "language", language)),
+ e);
+ } catch (A2ATError e) {
+ throw new PromptGenerationException(
+ ErrorCatalog.TEMPLATE_LOAD_FAILED.getCode(),
+ render(ErrorCatalog.TEMPLATE_LOAD_FAILED, Map.of("resource_path", templateIdentifier)),
+ e);
+ }
+ final StructuredSlotExtractionResult extractionResult;
+ try {
+ extractionResult = slotExtractor.apply(templateIdentifier);
+ } catch (ResourceNotFoundException e) {
+ throw new PromptGenerationException(
+ ErrorCatalog.SLOT_SCHEMA_NOT_FOUND.getCode(),
+ render(
+ ErrorCatalog.SLOT_SCHEMA_NOT_FOUND,
+ Map.of("template_uri", templateIdentifier, "language", language)),
+ e);
+ } catch (LLMConfigError e) {
+ throw new PromptGenerationException(
+ ErrorCatalog.LLM_NOT_CONFIGURED.getCode(), render(ErrorCatalog.LLM_NOT_CONFIGURED, Map.of()), e);
+ } catch (LLMError e) {
+ throw llmException(e, STEP_SLOT_EXTRACTION);
+ } catch (A2ATError e) {
+ throw new PromptGenerationException(
+ ErrorCatalog.LLM_INVOCATION_FAILED.getCode(),
+ render(ErrorCatalog.LLM_INVOCATION_FAILED, Map.of("reason", e.getMessage())),
+ e);
+ }
+ List extractionErrors = mapSlotErrors(extractionResult.slotErrors());
+ if (!extractionErrors.isEmpty()) {
+ throw new PromptGenerationException(
+ extractionErrors.get(0).code(), joinMessages(extractionErrors), extractionErrors);
+ }
+ Map slots = extractionResult.slots();
+ validateRequiredSlots(slots, templateIdentifier);
+ final String renderedPrompt;
+ try {
+ renderedPrompt = renderer.render(templateText, slots);
+ } catch (TaskPromptRenderException e) {
+ throw new PromptGenerationException(
+ ErrorCatalog.TEMPLATE_RENDER_FAILED.getCode(),
+ render(
+ ErrorCatalog.TEMPLATE_RENDER_FAILED,
+ Map.of("template_uri", templateIdentifier, "reason", e.getMessage())),
+ e);
+ }
+ return new MetadataContent(templateIdentifier, renderedPrompt, extensionUri);
+ }
+
+ private void validateRequiredSlots(Map slots, String templateIdentifier) {
+ final PromptSlotSchema schema;
+ try {
+ schema = slotSchemaLoader.loadSlotSchema(templateIdentifier, language);
+ } catch (A2ATError e) {
+ throw new PromptGenerationException(
+ ErrorCatalog.SLOT_SCHEMA_NOT_FOUND.getCode(),
+ render(
+ ErrorCatalog.SLOT_SCHEMA_NOT_FOUND,
+ Map.of("template_uri", templateIdentifier, "language", language)),
+ e);
+ }
+ if (schema == null) {
+ throw new PromptGenerationException(
+ ErrorCatalog.SLOT_SCHEMA_NOT_FOUND.getCode(),
+ render(
+ ErrorCatalog.SLOT_SCHEMA_NOT_FOUND,
+ Map.of("template_uri", templateIdentifier, "language", language)));
+ }
+ List defs = schema.slotDefinitions();
+ if (defs == null) {
+ return;
+ }
+ Map effectiveSlots = slots;
+ if (effectiveSlots == null) {
+ effectiveSlots = Map.of();
+ }
+ List failed = new ArrayList<>();
+ for (PromptSlotDefinition def : defs) {
+ if (def == null) {
+ continue;
+ }
+ if (def.required()) {
+ String name = def.name();
+ if (name == null) {
+ continue;
+ }
+ String value = effectiveSlots.get(name);
+ if (value == null || value.trim().isEmpty()) {
+ failed.add(notProvidedError(name, slotLabel(def)));
+ }
+ }
+ }
+ if (!failed.isEmpty()) {
+ throw new PromptGenerationException(failed.get(0).code(), joinMessages(failed), failed);
+ }
+ }
+
+ /**
+ * Maps the extraction-time slot errors reported by the slot-extraction step to catalog errors.
+ *
+ * Unknown codes returned by the LLM step are mapped to the {@code slot.rule_violation} fallback and logged.
+ *
+ * @param slotErrors extraction-time slot errors, may be null
+ * @return mapped slot validation errors, possibly empty, never null
+ */
+ private List mapSlotErrors(@Nullable List slotErrors) {
+ if (slotErrors == null || slotErrors.isEmpty()) {
+ return List.of();
+ }
+ List mapped = new ArrayList<>();
+ for (StructuredSlotValidationError error : slotErrors) {
+ if (error == null) {
+ continue;
+ }
+ mapped.add(mapSlotError(error));
+ }
+ return mapped;
+ }
+
+ private SlotValidationError mapSlotError(StructuredSlotValidationError error) {
+ String label = error.slotName() == null ? "" : error.slotName();
+ ErrorCatalog entry;
+ if (ErrorCatalog.SLOT_NOT_PROVIDED.getCode().equals(error.code())
+ || LEGACY_CODE_MISSING_INPUT.equals(error.code())) {
+ entry = ErrorCatalog.SLOT_NOT_PROVIDED;
+ } else if (ErrorCatalog.SLOT_CONSTRAINT_VIOLATED.getCode().equals(error.code())
+ || LEGACY_CODE_INVALID_VALUE.equals(error.code())) {
+ entry = ErrorCatalog.SLOT_CONSTRAINT_VIOLATED;
+ } else {
+ LOGGER.warn(
+ "Unknown slot-extraction error code '{}' mapped to '{}'.",
+ error.code(),
+ ErrorCatalog.SLOT_RULE_VIOLATION.getCode());
+ entry = ErrorCatalog.SLOT_RULE_VIOLATION;
+ }
+ Map facts = Map.of("slot_label", label);
+ return new SlotValidationError(label, entry.getCode(), render(entry, facts), facts);
+ }
+
+ private SlotValidationError notProvidedError(String slotName, String slotLabel) {
+ Map facts = Map.of("slot_label", slotLabel);
+ return new SlotValidationError(
+ slotName,
+ ErrorCatalog.SLOT_NOT_PROVIDED.getCode(),
+ render(ErrorCatalog.SLOT_NOT_PROVIDED, facts),
+ facts);
+ }
+
+ private String slotLabel(PromptSlotDefinition definition) {
+ if (definition.description() != null && !definition.description().isBlank()) {
+ return definition.description();
+ }
+ return definition.name() == null ? "" : definition.name();
+ }
+
+ private @Nullable PromptGenerationFailure slotExtractionFailure(
+ @Nullable List slotErrors) {
+ List mapped = mapSlotErrors(slotErrors);
+ if (mapped.isEmpty()) {
+ return null;
+ }
+ return new PromptGenerationFailure(mapped.get(0).code(), joinMessages(mapped), "generation");
+ }
+
+ private PromptGenerationFailure llmFailure(LLMError error, String step) {
+ if (error instanceof LLMConfigError) {
+ return new PromptGenerationFailure(
+ ErrorCatalog.LLM_NOT_CONFIGURED.getCode(),
+ render(ErrorCatalog.LLM_NOT_CONFIGURED, Map.of()),
+ "generation");
+ }
+ if (isResponseContractViolation(error)) {
+ return new PromptGenerationFailure(
+ ErrorCatalog.LLM_RESPONSE_INVALID.getCode(),
+ render(ErrorCatalog.LLM_RESPONSE_INVALID, Map.of("step", step)),
+ "generation");
+ }
+ return new PromptGenerationFailure(
+ ErrorCatalog.LLM_INVOCATION_FAILED.getCode(),
+ render(ErrorCatalog.LLM_INVOCATION_FAILED, Map.of("reason", error.getMessage())),
+ "generation");
+ }
+
+ private PromptGenerationException llmException(LLMError error, String step) {
+ PromptGenerationFailure failure = llmFailure(error, step);
+ return new PromptGenerationException(failure.code(), failure.message(), error);
+ }
+
+ private boolean isResponseContractViolation(LLMError error) {
+ String message = error.getMessage() == null ? "" : error.getMessage().toLowerCase(Locale.ROOT);
+ for (String marker : RESPONSE_CONTRACT_MARKERS) {
+ if (message.contains(marker)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String joinMessages(List errors) {
+ return errors.stream().map(SlotValidationError::message).collect(Collectors.joining("; "));
+ }
+
+ private String render(ErrorCatalog entry, Map facts) {
+ return ErrorMessages.render(entry, language, facts);
}
- String lastNormalizedInput() {
- return lastNormalizedInput;
+ private PromptGenerationResult failure(ErrorCatalog entry, Map facts, String stage) {
+ return PromptGenerationResult.failure(
+ new PromptGenerationFailure(entry.getCode(), render(entry, facts), stage));
}
}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/recognition/ClientScenarioRecognizer.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/recognition/ClientScenarioRecognizer.java
deleted file mode 100644
index 64ecaada..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/recognition/ClientScenarioRecognizer.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.recognition;
-
-import java.util.List;
-import net.openan.a2at.sdk.prompt.analysis.model.ScenarioRecognitionResult;
-import net.openan.a2at.sdk.prompt.resources.model.ScenarioDefinition;
-
-/**
- * Client-side adapter for scenario recognition.
- *
- * @since 2026-06
- */
-@FunctionalInterface
-public interface ClientScenarioRecognizer {
-
- /**
- * Resolves one normalized user input into a scenario recognition result.
- *
- * @param normalizedInput normalized user input text
- * @param scenarios available scenarios
- * @param systemPrompt scenario-recognition system prompt
- * @param userPrompt scenario-recognition user prompt
- * @return recognition result describing whether a scenario matched
- */
- ScenarioRecognitionResult recognize(
- String normalizedInput, List scenarios, String systemPrompt, String userPrompt);
-}
diff --git a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/recognition/LocalFileClientScenarioCatalogLoader.java b/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/recognition/LocalFileClientScenarioCatalogLoader.java
deleted file mode 100644
index 6da0a618..00000000
--- a/a2a-t-client/src/main/java/net/openan/a2at/sdk/client/prompt/recognition/LocalFileClientScenarioCatalogLoader.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.openan.a2at.sdk.client.prompt.recognition;
-
-import java.nio.file.Path;
-import java.util.List;
-import net.openan.a2at.sdk.prompt.resources.loader.LocalFilePromptScenarioCatalogLoader;
-import net.openan.a2at.sdk.prompt.resources.model.ScenarioDefinition;
-
-/**
- * Loads scenario catalogs from one local prompt resource root.
- *
- * @since 2026-06
- */
-public final class LocalFileClientScenarioCatalogLoader {
- private final LocalFilePromptScenarioCatalogLoader delegate;
-
- /**
- * Creates one local scenario catalog loader.
- *
- * @param promptRootDir prompt resource root directory
- */
- public LocalFileClientScenarioCatalogLoader(Path promptRootDir) {
- this.delegate = new LocalFilePromptScenarioCatalogLoader(promptRootDir);
- }
-
- /**
- * Loads scenario definitions for one language.
- *
- * @param language resource language
- * @return parsed scenario definitions
- */
- public List load(String language) {
- return delegate.load(language);
- }
-}
diff --git a/a2a-t-client/src/test/java/net/openan/a2at/sdk/client/api/A2ATClientNegotiationApiTest.java b/a2a-t-client/src/test/java/net/openan/a2at/sdk/client/api/A2ATClientNegotiationApiTest.java
new file mode 100644
index 00000000..4a0d8a1c
--- /dev/null
+++ b/a2a-t-client/src/test/java/net/openan/a2at/sdk/client/api/A2ATClientNegotiationApiTest.java
@@ -0,0 +1,219 @@
+package net.openan.a2at.sdk.client.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import net.openan.a2at.sdk.client.A2ATClient;
+import net.openan.a2at.sdk.core.model.MetadataContent;
+import net.openan.a2at.sdk.core.model.NegotiationContext;
+import net.openan.a2at.sdk.core.model.NegotiationPerformative;
+import net.openan.a2at.sdk.core.model.PromptTemplate;
+import net.openan.a2at.sdk.core.model.StandardTemplates;
+import net.openan.a2at.sdk.core.model.TemplateUri;
+import net.openan.a2at.sdk.llm.LLMClient;
+import net.openan.a2at.sdk.llm.LLMClientConfig;
+import net.openan.a2at.sdk.llm.LLMClientFactory;
+import net.openan.a2at.sdk.llm.LLMResponse;
+import net.openan.a2at.sdk.negotiation.content.InformationProposeContent;
+import net.openan.a2at.sdk.negotiation.content.NegotiationAbortContent;
+import net.openan.a2at.sdk.negotiation.content.NegotiationAbortData;
+import net.openan.a2at.sdk.negotiation.content.NegotiationItem;
+import net.openan.a2at.sdk.negotiation.content.NegotiationProposeData;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class A2ATClientNegotiationApiTest {
+
+ private static final String TEST_MOCK_PROVIDER = "test-negotiation-mock";
+
+ @BeforeAll
+ static void registerMockProvider() {
+ if (!LLMClientFactory.availableProviders().contains(TEST_MOCK_PROVIDER)) {
+ LLMClientFactory.register(TEST_MOCK_PROVIDER, RecordingClient.class);
+ }
+ }
+
+ private static final String UUID = "3dbc13b5-bd57-4c2b-b503-24e381b6c8d3";
+
+ private static final TemplateUri INFORMATION_PROPOSE = StandardTemplates.INFORMATION_NEGOTIATION_PROPOSE;
+
+ private static final String INFORMATION_PROPOSE_URI = INFORMATION_PROPOSE.uri();
+
+ @Test
+ void generatesInformationProposeFromDataWithBuiltinChineseTemplates() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("zh-CN"));
+
+ MetadataContent result = client.generateNegotiationProposePromptFromData(
+ new NegotiationProposeData(
+ new NegotiationContext(UUID, 1, 5, NegotiationPerformative.PROPOSE),
+ new InformationProposeContent(List.of(new NegotiationItem("节能区域", "松山湖")), null)),
+ INFORMATION_PROPOSE_URI);
+
+ assertEquals(INFORMATION_PROPOSE_URI, result.templateUri());
+ assertFalse(result.promptText().isBlank());
+ assertFalse(result.promptText().contains("协商上下文"), "the context section must not be rendered");
+ Map metadata = result.buildMetadataContent();
+ assertEquals(3, metadata.size());
+ assertEquals(result.promptText(), metadata.get(result.extensionUri()));
+ assertEquals(result.templateUri(), metadata.get(MetadataContent.TEMPLATE_URI_METADATA_KEY));
+ assertEquals(new NegotiationContext(UUID, 1, 5, NegotiationPerformative.PROPOSE), result.negotiationContext());
+ }
+
+ @Test
+ void generatesInformationProposeFromDataWithBuiltinEnglishTemplates() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("en-US"));
+
+ MetadataContent result = client.generateNegotiationProposePromptFromData(
+ new NegotiationProposeData(
+ new NegotiationContext(UUID, 1, 5, NegotiationPerformative.PROPOSE),
+ new InformationProposeContent(List.of(new NegotiationItem("Region", "Songshan Lake")), null)),
+ INFORMATION_PROPOSE_URI);
+
+ assertEquals(INFORMATION_PROPOSE_URI, result.templateUri());
+ assertFalse(result.promptText().isBlank());
+ assertFalse(result.promptText().contains("Negotiation Context"), "the context section must not be rendered");
+ assertTrue(result.promptText().contains("Required Information Items"));
+ }
+
+ @Test
+ void listsAllSevenNegotiationTemplatesPerLanguage() throws IOException {
+ assertEquals(7, negotiationPrompts(new A2ATClient(writeEnv("zh-CN"))).size());
+ assertEquals(7, negotiationPrompts(new A2ATClient(writeEnv("en-US"))).size());
+ }
+
+ @Test
+ void generatesAbortPromptFromDataWithBuiltinChineseTemplates() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("zh-CN"));
+
+ MetadataContent result = client.generateNegotiationAbortPromptFromData(
+ new NegotiationAbortData(
+ new NegotiationContext(UUID, 5, 5, NegotiationPerformative.ABORT),
+ new NegotiationAbortContent("达到协商轮次上限,本次协商确认结束。")),
+ StandardTemplates.NEGOTIATION_ABORT_URI);
+
+ assertEquals(StandardTemplates.NEGOTIATION_ABORT.uri(), result.templateUri());
+ assertTrue(result.promptText().contains("## 协商结果\nAbort"));
+ assertTrue(result.promptText().contains("## 协商终止原因"));
+ assertTrue(result.promptText().contains("达到协商轮次上限,本次协商确认结束。"));
+ assertEquals(5, result.negotiationContext().round());
+ }
+
+ @Test
+ void generatesAbortPromptFromDataWithBuiltinEnglishTemplates() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("en-US"));
+
+ MetadataContent result = client.generateNegotiationAbortPromptFromData(
+ new NegotiationAbortData(
+ new NegotiationContext(UUID, 3, 5, NegotiationPerformative.ABORT),
+ new NegotiationAbortContent(
+ "Reached the negotiation round limit. This negotiation is confirmed and ended.")),
+ StandardTemplates.NEGOTIATION_ABORT_URI);
+
+ assertEquals(StandardTemplates.NEGOTIATION_ABORT.uri(), result.templateUri());
+ assertTrue(result.promptText().contains("## Negotiation Result\nAbort"));
+ assertTrue(result.promptText().contains("## Negotiation Termination Reason"));
+ assertTrue(result.promptText().contains("Reached the negotiation round limit."));
+ }
+
+ @Test
+ void queriesTheCommonAbortTemplateWithoutThrowing() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("zh-CN"));
+
+ PromptTemplate template =
+ client.getPrompt(StandardTemplates.NEGOTIATION_ABORT_URI).orElseThrow();
+ assertEquals(StandardTemplates.NEGOTIATION_ABORT, template.templateUri());
+ assertFalse(template.content().isBlank());
+ }
+
+ @Test
+ void queriesSingleNegotiationTemplateWithoutThrowing() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("zh-CN"));
+
+ assertTrue(client.getPrompt(INFORMATION_PROPOSE_URI).isPresent());
+ assertTrue(client.getPrompt(StandardTemplates.FEASIBILITY_NEGOTIATION_ACCEPT_REJECT_URI)
+ .isPresent());
+ assertFalse(client.getPrompt("Negotiation-T/information-negotiation/propose/v9").isPresent());
+ PromptTemplate template = client.getPrompt(INFORMATION_PROPOSE_URI).orElseThrow();
+ assertEquals(INFORMATION_PROPOSE, template.templateUri());
+ assertFalse(template.content().isBlank());
+ }
+
+ @Test
+ void generateNegotiationPromptFromDataThrowsOnMalformedTemplateUriString() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("zh-CN"));
+
+ IllegalArgumentException ex = assertThrows(
+ IllegalArgumentException.class,
+ () -> client.generateNegotiationProposePromptFromData(
+ new NegotiationProposeData(
+ new NegotiationContext(UUID, 1, 5, NegotiationPerformative.PROPOSE),
+ new InformationProposeContent(List.of(new NegotiationItem("节能区域", "松山湖")), null)),
+ "Task-T/only-one-segment"));
+ assertTrue(ex.getMessage().contains("Unparseable template URI"), "message was: " + ex.getMessage());
+ }
+
+ @Test
+ void validateNegotiationPromptThrowsOnBlankTemplateUriString() throws IOException {
+ A2ATClient client = new A2ATClient(writeEnv("zh-CN"));
+
+ IllegalArgumentException ex = assertThrows(
+ IllegalArgumentException.class,
+ () -> client.validateProposePromptAndDataFilling(
+ "rendered negotiation message",
+ new NegotiationContext(UUID, 1, 5, NegotiationPerformative.PROPOSE),
+ Map.of("type", "object"),
+ " "));
+ assertTrue(ex.getMessage().contains("Unparseable template URI"), "message was: " + ex.getMessage());
+ }
+
+ private static List negotiationPrompts(A2ATClient client) {
+ return client.getPrompts().stream()
+ .filter(template -> StandardTemplates.NEGOTIATION_EXTENSION_NAME.equals(
+ template.templateUri().extensionName()))
+ .toList();
+ }
+
+ private static Path writeEnv(String language) throws IOException {
+ Path tempDir = Files.createTempDirectory("a2at-client-negotiation-env");
+ Path envFile = tempDir.resolve("client.env");
+ Files.writeString(
+ envFile,
+ """
+ A2AT_LANGUAGE=%s
+ A2AT_PROMPT_SOURCE_TYPE=classpath
+ A2AT_PROMPT_RESOURCE_LOCAL_ROOT_DIR=
+ A2AT_LLM_PROVIDER=test-negotiation-mock
+ A2AT_LLM_MODEL=example-model
+ A2AT_LLM_BASE_URL=https://llm.example.test/v1
+ A2AT_LLM_API_KEY=test-key
+ A2AT_NEGOTIATION_STATE_STORE_TYPE=in_memory
+ """
+ .formatted(language));
+ return envFile;
+ }
+
+ public static final class RecordingClient implements LLMClient {
+
+ private final LLMClientConfig config;
+
+ public RecordingClient(LLMClientConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public LLMResponse structured(
+ List