Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public static class ChannelMetadataKeys
/// channel-delivery route selected by the inbound adapter.
/// </summary>
public const string OutboundProviderSlug = "channel.outbound.provider_slug";
/// <summary>Exact NyxID UserService id for <see cref="OutboundProviderSlug"/> when known by the host.</summary>
public const string OutboundProviderUserServiceId = "channel.outbound.user_service_id";
/// <summary>Provider-interpreted primary outbound address for the current channel turn.</summary>
public const string DeliveryAddressId = "channel.delivery.address_id";
/// <summary>Provider-interpreted type for <see cref="DeliveryAddressId"/>.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ public sealed class AgentTurnToolCatalogMaterializer : IAgentProfileTurnToolCata
"agent_builder",
};

private static readonly IReadOnlySet<string> DirectScheduledAutomationToolNames =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"ask_user",
"use_skill",
"scheduled_agent_creator",
"agent_builder",
};

private static readonly IReadOnlySet<string> ScheduledAutomationClarificationToolNames =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"ask_user",
};

private static readonly IReadOnlySet<string> ScheduledAutomationExclusiveToolNames =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
Expand Down Expand Up @@ -1036,11 +1051,15 @@ private static bool HasScheduledAutomationIntent(string userMessage)
"recurring",
"repeat",
"daily",
"weekday",
"weekdays",
"weekly",
"monthly",
"hourly",
"remind",
"reminds",
"reminder",
"reminders",
"automation",
"automate") ||
ContainsAny(
Expand Down Expand Up @@ -1107,12 +1126,20 @@ private static void ApplyProfileTaskRouteIntentFilter(
return;
}

if (HasScheduledAutomationIntent(userMessage ?? string.Empty))
var normalizedUserMessage = userMessage ?? string.Empty;
if (HasScheduledAutomationIntent(normalizedUserMessage))
{
AddAvailableScheduledAutomationTools(availableToolNames, selectedToolNames);
var scheduledAutomationToolNames = SelectScheduledAutomationToolNames(normalizedUserMessage, availableToolNames);
AddAvailableScheduledAutomationTools(
availableToolNames,
selectedToolNames,
scheduledAutomationToolNames);
selectedToolNames.RemoveWhere(name =>
ManagedWorkflowExecutionToolNames.Contains(name) &&
!ScheduledAutomationToolNames.Contains(name));
selectedToolNames.RemoveWhere(name =>
ScheduledAutomationToolNames.Contains(name) &&
!scheduledAutomationToolNames.Contains(name));
return;
}

Expand Down Expand Up @@ -1151,13 +1178,17 @@ private static bool TryCreateScheduledAutomationFallbackNames(
out HashSet<string> fallbackNames)
{
fallbackNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var normalizedUserMessage = userMessage ?? string.Empty;
if (!string.Equals(intentId, ProfileTaskRouteIntentId, StringComparison.Ordinal) ||
!HasScheduledAutomationIntent(userMessage ?? string.Empty))
!HasScheduledAutomationIntent(normalizedUserMessage))
{
return false;
}

AddAvailableScheduledAutomationTools(availableToolNames, fallbackNames);
AddAvailableScheduledAutomationTools(
availableToolNames,
fallbackNames,
SelectScheduledAutomationToolNames(normalizedUserMessage, availableToolNames));
return fallbackNames.Count > 0;
}

Expand Down Expand Up @@ -1187,10 +1218,14 @@ private static bool TryCreateOrdinaryScheduledAutomationFallbackNames(
out HashSet<string> fallbackNames)
{
fallbackNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (!HasScheduledAutomationIntent(userMessage ?? string.Empty))
var normalizedUserMessage = userMessage ?? string.Empty;
if (!HasScheduledAutomationIntent(normalizedUserMessage))
return false;

AddAvailableScheduledAutomationTools(availableToolNames, fallbackNames);
AddAvailableScheduledAutomationTools(
availableToolNames,
fallbackNames,
SelectScheduledAutomationToolNames(normalizedUserMessage, availableToolNames));
return fallbackNames.Count > 0;
}

Expand All @@ -1207,15 +1242,64 @@ private static void AddAvailableManagedWorkflowTools(

private static void AddAvailableScheduledAutomationTools(
IReadOnlySet<string> availableToolNames,
HashSet<string> selectedToolNames)
HashSet<string> selectedToolNames,
IReadOnlySet<string> candidateToolNames)
{
foreach (var name in ScheduledAutomationToolNames)
foreach (var name in candidateToolNames)
{
if (availableToolNames.Contains(name))
selectedToolNames.Add(name);
}
}

private static IReadOnlySet<string> SelectScheduledAutomationToolNames(
string userMessage,
IReadOnlySet<string> availableToolNames)
{
if (HasExplicitOrnnSkillAutomationIntent(userMessage))
return ScheduledAutomationToolNames;

if (!HasRecurringScheduledAutomationIntent(userMessage))
return DirectScheduledAutomationToolNames;

return HasAvailableDirectScheduledAutomationTool(availableToolNames)
? ScheduledAutomationClarificationToolNames
: ScheduledAutomationToolNames;
}

private static bool HasAvailableDirectScheduledAutomationTool(IReadOnlySet<string> availableToolNames) =>
availableToolNames.Contains("use_skill") ||
availableToolNames.Contains("scheduled_agent_creator") ||
availableToolNames.Contains("agent_builder");

private static bool HasExplicitOrnnSkillAutomationIntent(string userMessage)
{
var tokens = TokenizeSelectionText(userMessage);
return HasAnyToken(tokens, "ornn", "skill", "skills", "publish", "published", "reuse", "reusable") ||
ContainsAny(userMessage, "技能", "发布", "复用", "可复用");
}

private static bool HasRecurringScheduledAutomationIntent(string userMessage)
{
var tokens = TokenizeSelectionText(userMessage);
return HasAnyToken(
tokens,
"every",
"each",
"per",
"daily",
"weekday",
"weekdays",
"weekly",
"monthly",
"hourly",
"recurring",
"repeat",
"repeats",
"repeated") ||
ContainsAny(userMessage, "每天", "每日", "每周", "每月", "每小时", "周期", "循环", "重复", "长期", "持续");
}

private static bool HasContextToken(IReadOnlySet<string> tokens) =>
HasAnyToken(tokens, "profile", "context", "preference", "preferences", "settings", "config", "account", "user", "me");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2123,10 +2123,9 @@ private async Task<ReplyChannelContext> BuildReplyChannelContextAsync(
// as the failure-notification provider so a failed outbound delivery
// (e.g. cross-tenant Lark 99992364) can still notify the user via the bot they just
// successfully messaged. See issue #423 §C and ChannelMetadataKeys.InboundChannelBotProxySlug.
ScheduledDeliveryMetadataBuilder.ApplyDefaultOutboundProvider(metadata, inboundEvent.NyxProviderSlug);
if (!string.IsNullOrWhiteSpace(inboundEvent.NyxProviderSlug))
{
metadata[ChannelMetadataKeys.InboundChannelBotProxySlug] = inboundEvent.NyxProviderSlug;
metadata[ChannelMetadataKeys.OutboundProviderSlug] = inboundEvent.NyxProviderSlug;
// The inbound bot is also the default OUTBOUND delivery provider for a chat-triggered
// scheduled task: the scheduled run replies via the same Lark bot that received the
// message, so scheduled_agent_creator can resolve a provider without manual Studio/Web
Expand Down Expand Up @@ -2156,21 +2155,12 @@ private async Task<ReplyChannelContext> BuildReplyChannelContextAsync(
AddIdentityHint(identityHints, "conversation", "platform", larkChatId);
}

var deliveryAddressId = NormalizeOptional(activity?.TransportExtras?.DeliveryAddressId);
if (!string.IsNullOrWhiteSpace(deliveryAddressId))
metadata[ChannelMetadataKeys.DeliveryAddressId] = deliveryAddressId;

var deliveryAddressType = NormalizeOptional(activity?.TransportExtras?.DeliveryAddressType);
if (!string.IsNullOrWhiteSpace(deliveryAddressType))
metadata[ChannelMetadataKeys.DeliveryAddressType] = deliveryAddressType;

var deliveryFallbackAddressId = NormalizeOptional(activity?.TransportExtras?.DeliveryFallbackAddressId);
if (!string.IsNullOrWhiteSpace(deliveryFallbackAddressId))
metadata[ChannelMetadataKeys.DeliveryFallbackAddressId] = deliveryFallbackAddressId;

var deliveryFallbackAddressType = NormalizeOptional(activity?.TransportExtras?.DeliveryFallbackAddressType);
if (!string.IsNullOrWhiteSpace(deliveryFallbackAddressType))
metadata[ChannelMetadataKeys.DeliveryFallbackAddressType] = deliveryFallbackAddressType;
ScheduledDeliveryMetadataBuilder.ApplyDeliveryAddress(
metadata,
activity?.TransportExtras?.DeliveryAddressId,
activity?.TransportExtras?.DeliveryAddressType,
activity?.TransportExtras?.DeliveryFallbackAddressId,
activity?.TransportExtras?.DeliveryFallbackAddressType);

var larkOperatorUserId = NormalizeOptional(activity?.TransportExtras?.NyxLarkOperatorUserId);
if (!string.IsNullOrWhiteSpace(larkOperatorUserId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public sealed class NyxIdAssistantActionsOptions
public const string ConfigSection = "Aevatar:NyxId:AssistantActions";

public bool Enabled { get; set; }

public string? ScheduledDeliveryProviderSlug { get; set; }

public string? ScheduledDeliveryProviderUserServiceId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public async Task HandleCreateConversationAsync(
ArgumentNullException.ThrowIfNull(command);
var scopeId = NormalizeRequired(command.ScopeId, nameof(command.ScopeId));
var ownerSubject = command.FirstTurn is null
? null
? NormalizeRequired(command.OwnerSubject, nameof(command.OwnerSubject))
: NormalizeRequired(
command.FirstTurn.ToolContext?.Caller?.OwnerSubject,
"owner_subject");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,11 @@ await NyxIdChatAguiSseEventWriter.WriteAsync(
}
else
{
var metadata = new Dictionary<string, string>(StringComparer.Ordinal);
var assistantActionsOptions = http.RequestServices.GetService<NyxIdAssistantActionsOptions>();
var metadata = ScheduledDeliveryMetadataBuilder.CreateNyxIdAssistantMetadata(
actorId,
assistantActionsOptions?.ScheduledDeliveryProviderSlug,
assistantActionsOptions?.ScheduledDeliveryProviderUserServiceId);
Comment on lines +285 to +288

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Configure the NyxID assistant delivery provider

With the checked Mainnet configuration, both new option values passed here are null: appsettings.json configures only AssistantActions:Enabled, and MainnetHostBuilderExtensions only supplies that same default. Consequently CreateNyxIdAssistantMetadata emits no outbound provider slug, and an ordinary public-chat reminder that does not invent nyx_provider_slug reaches ScheduledAgentCreateRequestMapper.Plan and fails with channel_outbound_provider_slug_unavailable; configure or resolve the provider for the Mainnet NyxID assistant path rather than exposing a creator that cannot use its default metadata.

Useful? React with 👍 / 👎.

var llmControl = await BuildLlmControlAsync(http, accessToken, ct);
var rawInputParts = request.InputParts?.Select(static part => part.ToProto()).ToArray() ?? [];
var commandId = NyxIdChatPublicIdentity.CreateChatCommandId(
Expand Down
5 changes: 4 additions & 1 deletion agents/Aevatar.GAgents.NyxidChat/NyxIdChatEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ private static async Task<IResult> HandleCreateConversationAsync(
// Refactor (iter56/cluster-891-endpoint-ack-honesty): old=200-shaped accepted, new=202 + Location
// The create facade returns accepted/admission-visible command trace, not read-model-observed conversation state.
// Clients must poll the conversation list or observe the stream/status path instead of treating this body as committed.
var receipt = await lifecycleFacade.CreateConversationAsync(scopeId, ct);
if (!AevatarPrincipalSubjectResolver.TryResolveNyxIdSubject(http.User, out var ownerSubject))
return Results.Unauthorized();

var receipt = await lifecycleFacade.CreateConversationAsync(scopeId, ownerSubject, ct);
return receipt.Status switch
{
NyxIdChatConversationCreateStatus.Accepted => Results.Accepted(
Expand Down
2 changes: 2 additions & 0 deletions agents/Aevatar.GAgents.NyxidChat/NyxIdChatInteraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ public async Task<CommandTargetResolution<NyxIdChatCommandTarget, NyxIdChatStart
{
ScopeId = command.ScopeId,
RequestedActorId = command.ActorId,
OwnerSubject = command.OwnerSubject?.Trim() ?? string.Empty,
AgentProfileReference = command.AgentProfileReference?.Clone(),
ContextAttachments = ConversationContextAttachmentAdmission.CloneOptionalSet(command.ContextAttachments),
FirstTurn = new NyxIdChatStartTurnCommand
Expand Down Expand Up @@ -581,6 +582,7 @@ public EventEnvelope CreateEnvelope(NyxIdChatCommand command, CommandContext con
return CreateDirectEnvelope(context, new NyxIdChatConversationCreateCommand
{
ScopeId = command.ScopeId,
OwnerSubject = command.OwnerSubject?.Trim() ?? string.Empty,
CreatedLocally = command.CreatedLocally,
AgentProfile = command.AgentProfile?.Clone(),
AgentProfileReference = command.AgentProfileReference?.Clone(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public NyxIdChatLifecycleFacade(

public async Task<NyxIdChatConversationCreateReceipt> CreateConversationAsync(
string scopeId,
string ownerSubject,
CancellationToken ct = default)
{
// Refactor (iter77/cluster-077-cqrs-command-outcome-stream-rpc):
Expand All @@ -86,6 +87,7 @@ public async Task<NyxIdChatConversationCreateReceipt> CreateConversationAsync(
new NyxIdChatConversationCreateCommand
{
ScopeId = NormalizeRequired(scopeId, nameof(scopeId)),
OwnerSubject = NormalizeRequired(ownerSubject, nameof(ownerSubject)),
},
ct);

Expand Down
13 changes: 8 additions & 5 deletions agents/Aevatar.GAgents.NyxidChat/NyxIdChatTaskLifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,7 @@ NyxIdChatAuthorizationResumeRequirement.CompleteOriginalServiceRequest or
next,
normalizedSignal.Key,
now,
normalizedSignal.Tool.Receipt.MutationStage ==
AgentToolReceiptMutationStage.ReadModelObserved);
HasProviderConfirmedMutationStage(normalizedSignal.Tool.Receipt.MutationStage));
}
else if (currentStep.Kind == NyxIdChatStepKind.Tool &&
FindCurrentStep(next, operationKey)?.Status is
Expand Down Expand Up @@ -1542,11 +1541,15 @@ private static NyxIdChatTaskStepState BuildVerificationStep(
return step;
}

private static bool HasProviderConfirmedMutationStage(AgentToolReceiptMutationStage mutationStage) =>
mutationStage is AgentToolReceiptMutationStage.Accepted or
AgentToolReceiptMutationStage.ReadModelObserved;

private static NyxIdChatOperationDispatchCommand? ActivatePlannedVerificationStep(
NyxIdChatConversationGAgentState state,
NyxIdChatOperationKey completedToolKey,
Timestamp now,
bool mutationReadModelObserved = false)
bool providerConfirmedMutationStage = false)
{
var step = state.ActiveTask.Steps.SingleOrDefault(candidate =>
candidate.Kind is (NyxIdChatStepKind.Llm or NyxIdChatStepKind.Postcondition) &&
Expand All @@ -1556,10 +1559,10 @@ candidate.Kind is (NyxIdChatStepKind.Llm or NyxIdChatStepKind.Postcondition) &&
if (step?.Operation?.Key is null)
return null;

if (mutationReadModelObserved)
if (providerConfirmedMutationStage)
{
step.Kind = NyxIdChatStepKind.Llm;
step.Description = "Communicate the typed mutation result observed from its canonical read model.";
step.Description = "Communicate the provider-confirmed typed mutation result.";
step.Source = new NyxIdChatStepSource { Llm = new NyxIdChatLLMStepSource() };
step.Operation.Kind = NyxIdChatStepKind.Llm;
}
Expand Down
Loading
Loading