Skip to content
Closed
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 @@ -41,6 +41,9 @@ export interface SubscriptionDrawerState {
// `data.references` (`application`/`application_variant`); each value is a
// `{id}` reference. Ignored in edit mode.
defaultReferences?: Record<string, {id?: string; slug?: string}>
// Optional create-mode metadata to persist with the trigger. Used by scoped
// playground entrypoints to remember the revision where the trigger was added.
defaultMeta?: Record<string, unknown> | null
// Human-readable label for `defaultReferences` (e.g. the agent's name), so
// the bound-workflow field shows a name instead of a raw id. Ignored in edit mode.
defaultBoundLabel?: string
Expand All @@ -63,6 +66,9 @@ export interface ScheduleDrawerState {
// (`application`/`application_variant`); each value is a `{id}` reference.
// Ignored in edit mode.
defaultReferences?: Record<string, {id?: string; slug?: string}>
// Optional create-mode metadata to persist with the trigger. Used by scoped
// playground entrypoints to remember the revision where the trigger was added.
defaultMeta?: Record<string, unknown> | null
// Human-readable label for `defaultReferences` (e.g. the agent's name), so
// the bound-workflow field shows a name instead of a raw id. Ignored in edit mode.
defaultBoundLabel?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
useTriggerSchedules,
useTriggerSubscription,
useTriggerSubscriptions,
type TriggerReference,
type TriggerSchedule,
type TriggerSubscription,
} from "@agenta/entities/gatewayTrigger"
Expand Down Expand Up @@ -69,6 +68,12 @@ import TriggerSubscriptionDrawer from "../../gatewayTrigger/drawers/TriggerSubsc

import {AddTextLink} from "./AddTextLink"
import {CollapsibleProviderGroup, SubSectionHeader} from "./sectionGroups"
import {
addAgentTriggerOriginMeta,
agentTriggerMatchesContext,
buildAgentTriggerOrigin,
getAgentTriggerContext,
} from "./triggerScope"

// Persisted per-agent expand state for provider groups (key = `${entityId}:${providerKey}`).
const triggerGroupsExpandedAtom = atomWithStorage<Record<string, boolean>>(
Expand Down Expand Up @@ -282,18 +287,6 @@ export interface TriggerManagementSectionProps {
disabled?: boolean
}

/** Whether any id in a trigger's `data.references` matches one of the agent's ids. */
function referencesMatch(
references: Record<string, TriggerReference> | null | undefined,
agentIds: Set<string>,
): boolean {
if (!references || agentIds.size === 0) return false
for (const ref of Object.values(references)) {
if (ref?.id && agentIds.has(ref.id)) return true
}
return false
}

/**
* Resolve the agent's matchable ids, its `defaultReferences`, and the project triggers
* scoped to it. Shared by the section body and the header count badge / add-dropdown in
Expand All @@ -307,6 +300,11 @@ export function useAgentTriggers(entityId: string | null) {
)
const appId = revision?.workflow_id ?? null
const variantId = revision?.workflow_variant_id ?? revision?.variant_id ?? null
const triggerContext = useMemo(
() => getAgentTriggerContext(entityId, revision),
[entityId, revision],
)
const triggerOrigin = useMemo(() => buildAgentTriggerOrigin(triggerContext), [triggerContext])
// The app slug — needed for "By environment" binding, which resolves via the
// application slug + environment (see triggers/service.py `_normalize_references`).
const appSlug = (revision as {slug?: string} | null)?.slug ?? null
Expand Down Expand Up @@ -339,17 +337,18 @@ export function useAgentTriggers(entityId: string | null) {
const {schedules} = useTriggerSchedules()

const scopedSubscriptions = useMemo(
() => subscriptions.filter((s) => referencesMatch(s.data?.references, agentIds)),
[subscriptions, agentIds],
() => subscriptions.filter((s) => agentTriggerMatchesContext(s, triggerContext, agentIds)),
[subscriptions, triggerContext, agentIds],
)
const scopedSchedules = useMemo(
() => schedules.filter((s) => referencesMatch(s.data?.references, agentIds)),
[schedules, agentIds],
() => schedules.filter((s) => agentTriggerMatchesContext(s, triggerContext, agentIds)),
[schedules, triggerContext, agentIds],
)

return {
defaultReferences,
defaultBoundLabel,
defaultMeta: addAgentTriggerOriginMeta(null, triggerOrigin),
scopedSubscriptions,
scopedSchedules,
count: scopedSubscriptions.length + scopedSchedules.length,
Expand Down Expand Up @@ -471,8 +470,14 @@ function TriggerRow({
}

export function TriggerManagementSection({entityId, disabled}: TriggerManagementSectionProps) {
const {scopedSubscriptions, scopedSchedules, count, defaultReferences, defaultBoundLabel} =
useAgentTriggers(entityId)
const {
scopedSubscriptions,
scopedSchedules,
count,
defaultReferences,
defaultBoundLabel,
defaultMeta,
} = useAgentTriggers(entityId)

const {connections} = useTriggerConnectionsQuery()
const {integrations} = useTriggerCatalogIntegrations()
Expand Down Expand Up @@ -742,6 +747,7 @@ export function TriggerManagementSection({entityId, disabled}: TriggerManagement
? () =>
openSubscriptionDrawer({
defaultReferences,
defaultMeta,
defaultBoundLabel,
playgroundEntityId: entityId ?? undefined,
integrationKey: group.key,
Expand Down Expand Up @@ -869,7 +875,7 @@ export function AddTriggerDropdown({
/** Custom trigger element (e.g. an inline text-link for an empty state). Defaults to a `+`. */
trigger?: ReactNode
}) {
const {defaultReferences, defaultBoundLabel} = useAgentTriggers(entityId)
const {defaultReferences, defaultBoundLabel, defaultMeta} = useAgentTriggers(entityId)
const openSubscriptionDrawer = useSetAtom(triggerSubscriptionDrawerAtom)
const openScheduleDrawer = useSetAtom(triggerScheduleDrawerAtom)

Expand All @@ -890,6 +896,7 @@ export function AddTriggerDropdown({
onSelect: () =>
openSubscriptionDrawer({
defaultReferences,
defaultMeta,
defaultBoundLabel,
playgroundEntityId: entityId ?? undefined,
}),
Expand All @@ -903,6 +910,7 @@ export function AddTriggerDropdown({
onSelect: () =>
openScheduleDrawer({
defaultReferences,
defaultMeta,
defaultBoundLabel,
playgroundEntityId: entityId ?? undefined,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type {TriggerReference} from "@agenta/entities/gatewayTrigger"
import type {Workflow} from "@agenta/entities/workflow"

export const AGENT_TRIGGER_ORIGIN_META_KEY = "agenta_agent_trigger_origin"

export interface AgentTriggerOrigin {
revision_id: string
variant_id?: string | null
revision_version?: number | null
}

export interface AgentTriggerContext {
revisionId: string | null
variantId: string | null
revisionVersion: number | null
}

export interface AgentTriggerEntity {
data?: {
references?: Record<string, TriggerReference> | null
} | null
meta?: Record<string, unknown> | null
}

function finiteNumber(value: unknown): number | null {
const number = typeof value === "number" ? value : Number(value)
return Number.isFinite(number) ? number : null
}
Comment on lines +25 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve null revision versions instead of coercing them to 0.

Number(null) returns 0, so missing/null revision_version is treated as v0 and can make origin-scoped triggers match later revisions when version metadata is unavailable.

Proposed fix
 function finiteNumber(value: unknown): number | null {
+    if (value == null || value === "") return null
     const number = typeof value === "number" ? value : Number(value)
     return Number.isFinite(number) ? number : null
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function finiteNumber(value: unknown): number | null {
const number = typeof value === "number" ? value : Number(value)
return Number.isFinite(number) ? number : null
}
function finiteNumber(value: unknown): number | null {
if (value == null || value === "") return null
const number = typeof value === "number" ? value : Number(value)
return Number.isFinite(number) ? number : null
}


export function getAgentTriggerContext(
entityId: string | null,
revision: Workflow | null | undefined,
): AgentTriggerContext {
return {
revisionId: entityId,
variantId: revision?.workflow_variant_id ?? revision?.variant_id ?? null,
revisionVersion: finiteNumber(revision?.version),
}
}

export function buildAgentTriggerOrigin(context: AgentTriggerContext): AgentTriggerOrigin | null {
if (!context.revisionId) return null
return {
revision_id: context.revisionId,
variant_id: context.variantId,
revision_version: context.revisionVersion,
}
}

export function addAgentTriggerOriginMeta(
meta: Record<string, unknown> | null | undefined,
origin: AgentTriggerOrigin | null,
): Record<string, unknown> | null {
if (!origin) return meta ?? null
return {
...(meta ?? {}),
[AGENT_TRIGGER_ORIGIN_META_KEY]: origin,
}
}

function readAgentTriggerOrigin(
meta: Record<string, unknown> | null | undefined,
): AgentTriggerOrigin | null {
const raw = meta?.[AGENT_TRIGGER_ORIGIN_META_KEY]
if (!raw || typeof raw !== "object") return null
const value = raw as Record<string, unknown>
const revisionId = typeof value.revision_id === "string" ? value.revision_id : null
if (!revisionId) return null
return {
revision_id: revisionId,
variant_id: typeof value.variant_id === "string" ? value.variant_id : null,
revision_version: finiteNumber(value.revision_version),
}
}

/** Whether any id in a trigger's `data.references` matches one of the agent's ids. */
function referencesMatch(
references: Record<string, TriggerReference> | null | undefined,
agentIds: Set<string>,
): boolean {
if (!references || agentIds.size === 0) return false
for (const ref of Object.values(references)) {
if (ref?.id && agentIds.has(ref.id)) return true
}
return false
}

function revisionReferenceMatches(
references: Record<string, TriggerReference> | null | undefined,
revisionId: string | null,
): boolean {
if (!references || !revisionId) return false
return (
references.application_revision?.id === revisionId ||
references.workflow_revision?.id === revisionId ||
references.evaluator_revision?.id === revisionId
)
}

// Origin-scoped triggers show on the revision they were created on and any later
// revision of the same variant; never on earlier revisions or sibling variants.
function originMatchesContext(origin: AgentTriggerOrigin, context: AgentTriggerContext): boolean {
if (origin.revision_id === context.revisionId) return true
if (!origin.variant_id || origin.variant_id !== context.variantId) return false
if (origin.revision_version == null || context.revisionVersion === null) return false
return context.revisionVersion >= origin.revision_version
}

export function agentTriggerMatchesContext(
trigger: AgentTriggerEntity,
context: AgentTriggerContext,
agentIds: Set<string>,
): boolean {
const references = trigger.data?.references
const origin = readAgentTriggerOrigin(trigger.meta)

if (origin) {
return originMatchesContext(origin, context)
}

if (revisionReferenceMatches(references, context.revisionId)) {
return true
}

return referencesMatch(references, agentIds)
}
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,7 @@ function ScheduleForm({
} else {
const body: TriggerScheduleCreate = {
name: name || null,
meta: state?.defaultMeta ?? null,
data,
// Honor the Active toggle at creation (otherwise the BE defaults to active).
flags: {is_active: enabled},
Expand Down Expand Up @@ -688,6 +689,7 @@ function ScheduleForm({
schedule,
name,
enabled,
state?.defaultMeta,
edit,
create,
onClose,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ function SubscriptionForm({
} else {
const body: TriggerSubscriptionCreate = {
name: name || null,
meta: state?.defaultMeta ?? null,
connection_id: connectionId,
data,
// Honor the Active toggle at creation (BE defaults to active; is_valid
Expand All @@ -771,6 +772,7 @@ function SubscriptionForm({
subscription,
name,
enabled,
state?.defaultMeta,
edit,
create,
onClose,
Expand Down
Loading
Loading