Optionalprovider: IMetadataProviderProtectedCheckChecks whether the serialized content for a new artifact version is identical to the latest existing version of the same artifact, using SHA-256 content hashing.
When the content is unchanged, creating a new version adds noise without value.
This method computes the hash of the candidate content and compares it against
the ContentHash stored on the most recent version (populated by
MJArtifactVersionEntityServer.Save()).
The artifact whose latest version to compare against
The serialized (JSON-stringified) content that would become the new version
The version number of the current latest version
User context for the RunView query
Optionalprovider: IMetadataProviderThe existing version's ID if content is identical, or null if a new version should be created
Creates MJ: Artifact + MJ: Artifact Version + MJ: Conversation Detail Artifact
junction records for agent-produced media outputs (images, audio, video).
Replaces the deprecated CreateConversationMediaAttachments path which wrote to
the MJ: Conversation Detail Attachments table (then auto-paired to an artifact
via the server-side hook). Writing artifacts directly removes the deprecated-entity
dependency and the redundant dual-write.
Reuses createArtifactWithVersion — the same helper that ProcessFileArtifacts
uses — so artifact creation logic (MIME resolution, transaction wrapping, junction
linking) lives in exactly one place.
The conversation detail to link artifacts to
Media outputs to persist as artifacts
User context for DB operations
Optionalprovider: IMetadataProviderOptional metadata provider for multi-provider support
OptionalresolvedStorageAccountId: stringPre-resolved storage account from the agent's resolution chain. Optional, and optional on purpose: this is a public method, and callers that predate storage-backed media keep working — omitting it just falls back to the first active account inside uploadBase64ToStorage.
Finds the most recent artifact for a conversation detail to determine versioning. Queries the junction table to locate artifacts linked to a specific conversation message.
The conversation detail ID to query
The user context for the query
Optionalprovider: IMetadataProviderArtifact info if exists, null if this is the first artifact for this message
Gets the maximum version number for an artifact. Used when creating new versions of explicitly specified artifacts.
The artifact ID to query
The user context for the query
Optionalprovider: IMetadataProviderThe maximum version number, or 0 if no versions exist
ProtectedLinkCreates a ConversationDetailArtifact junction record linking an artifact version
to a conversation detail, then returns the standard artifact result tuple.
Extracted as a helper so both the normal version-creation path and the duplicate-skip path can share the same linking and return logic.
The artifact version ID to link
The conversation detail to link to
The parent artifact ID (passed through to the return value)
The version number (passed through to the return value)
User context for the save operation
Metadata provider for entity creation
The standard artifact result tuple
Processes agent completion to create artifacts from the agent's payload. Handles artifact creation, versioning, and linking to conversation details.
This method implements intelligent artifact versioning: 0. If the agent supplied an artifactDirective, it decides: 'suppress' → nothing; 'create-new' → new artifact (sourceArtifactId ignored); 'version-source' → version targetArtifactId, else sourceArtifactId. A directive-named target is model output and is vetted first (UUID shape, existence, and the caller's right to write to it — see VetArtifactVersionTarget); anything failing falls back down the ladder to the caller's sourceArtifactId and then to the historical chain, so a directive can never widen what the caller was already allowed to do.
Respects the agent's ArtifactCreationMode configuration:
The result from agent execution containing the payload
The conversation detail to link the artifact to. May be
undefined for runs executed OUTSIDE a conversation context (e.g. realtime voice
delegations): the artifact + version are still created, but the previous-artifact lookup
and the ConversationDetailArtifact junction link are skipped.
Optional explicit artifact to version from (agent continuity)
The user context for the operation
Optionalprovider: IMetadataProviderArtifact metadata if created, undefined if skipped or failed
Creates MJ: Artifact records for file outputs collected during agent execution.
Reads directly from ExecuteAgentResult.fileOutputs — no DB query needed.
Called automatically by RunAgentInConversation after the agent completes.
File outputs collected by BaseAgent during action execution
The conversation detail to link artifacts to
User context for DB operations
OptionalresolvedStorageAccountId: stringPre-resolved FileStorageAccount ID from the agent's hierarchical resolution chain (Runtime → Agent → Category → Type → fallback). When provided, uploads use this specific account instead of picking the first active one.
Optionalprovider: IMetadataProviderOptionalacceptUnregisteredFiles: booleanRuns an AI agent with the specified parameters.
This method acts as a thin pass-through that:
The type of the agent's context as provided in the ExecuteAgentParams
The type of the agent's result as returned in ExecuteAgentResult
Parameters for agent execution (same as BaseAgent.Execute)
The execution result (same as BaseAgent.Execute)
Runs an AI agent within a conversation context, handling conversation and artifact management.
This method provides a complete workflow for running agents in conversations:
Core agent execution parameters
Conversation-specific options
OptionalconversationDetailId?: stringOptional existing conversation detail ID. If provided, skips conversation/detail creation
OptionalconversationId?: stringOptional existing conversation ID. If not provided, a new conversation will be created
OptionalconversationName?: stringOptional conversation name (only used when creating new conversation)
OptionalcreateArtifacts?: booleanWhether to create artifacts from the agent's payload (default: true)
OptionalsourceArtifactId?: stringOptional source artifact ID for versioning (agent continuity/refinement)
OptionaltestRunId?: stringOptional test run ID to link conversation and details to (for test execution traceability)
OptionaluserMessage?: stringThe user's message text for this conversation turn (required if conversationDetailId not provided)
Promise containing the agent execution result and conversation/artifact metadata
const runner = new AgentRunner();
const result = await runner.RunAgentInConversation({
agent: myAgent,
conversationMessages: messages,
contextUser: currentUser
}, {
conversationId: existingConvoId, // Optional - creates new if not provided
userMessage: 'User query text',
createArtifacts: true
});
Saves media outputs to AIAgentRunMedia table for permanent storage. Creates a record for each media output promoted during agent execution. All items in the array are saved.
The ID of the agent run
Array of media outputs to save
User context for the operation
Optionalprovider: IMetadataProviderArray of saved AIAgentRunMedia IDs
ProtectedVetVets one rung of the version-target ladder, returning the plan with a normalized id when the
target is usable and null when the caller should fall back.
Three things are checked, in cost order:
ExtraFilter fragments built by GetMaxVersionForArtifact and
CheckForDuplicateVersion; those escape what they are handed, but an id that is not
a UUID cannot name an artifact, so rejecting it here turns a doomed query into a clean
fallback. The id is then TRIMMED for downstream use — IsValidUUID tolerates surrounding
whitespace, so a value with a trailing newline passes validation and would otherwise reach
ArtifactID='<uuid>\n' and fail as a SQL conversion error.vwArtifacts carries no per-user
predicate and no row-level-security filter, so a row loading successfully proves only that
it EXISTS. Without this check an agent could name any artifact id in the instance and have
the run's payload appended to it as a new version. The caller must own the artifact or hold
an explicit CanEdit grant on it.Existence and authorization resolve in ONE RunViews round trip, and via RunView rather
than BaseEntity.Load deliberately: Load THROWS on a permission denial, a SQL conversion
error or any transient DB fault, and a throw here would propagate to the method-wide catch and
lose the whole artifact — the opposite of the graceful fallback this is meant to provide.
A caller-supplied sourceArtifactId is shape-checked but NOT loaded or authorized: it is a
server-side argument rather than model output, and adding a round trip plus a new denial mode
to that path would change behavior for every existing agent. (The pre-existing exposure on
that path — any authenticated caller may name any artifact id — is unchanged by this PR and
wants its own fix.) One exception, and it is deliberate rather than incidental: if the agent
named the SAME id first and it failed authorization, rejectedIds refuses it on the caller
rung too. The caller path still performs no authorization of its own; it just cannot be used
to launder an id this run has already refused.
Why this does not call PermissionEngine / ArtifactPermissionProvider. That provider
(MJCoreEntities/src/custom/PermissionProviders/ArtifactPermissionProvider.ts) answers
"may this user Update this artifact" from the permission ROWS, and is the right home for that
question in general — see guides/UNIFIED_PERMISSIONS_GUIDE.md §1. It is not used here for
two reasons specific to this path. First, it answers only half the question: it reads grant
rows and does not treat the artifact's OWNER as an editor, so the UserID check below would
remain regardless and the owner half would still live in two places. Second, and decisive:
PermissionEngine.CheckPermission returns Allowed: false with "Unknown permission domain"
when its domain is not loaded, and this path's response to a denial is a SILENT fallback to
creating a new artifact. A deployment that has not synced metadata/permission-domains — or
any caller that reaches the runner before PermissionEngine.Config() — would therefore stop
versioning for every legitimate editor, with nothing in the logs to say why. A direct query
has no such dependency on engine state. As of this writing nothing server-side calls
PermissionEngine or runs its Config(): its callers are Explorer's Permissions dashboard,
the Sharing Center, and the integration test suite. (Lists/server,
Communication/notifications and the MJCoreEntities entity extensions call
ResourcePermissionEngine, a different class over MJ: Resource Permissions.)
The cost of that choice is drift: this is a third answer to "can this user edit this
artifact", alongside the provider and ng-conversations' artifact-permission.service.ts.
If artifact sharing grows a new grant shape — role grantees, SupportsDeny, cascade from
collections — this method must be updated with the provider. Revisit the delegation once
PermissionEngine is routinely configured server-side. The cleaner precondition is upstream:
CollectionPermissionProvider already treats a collection's owner as a synthetic full-access
grantee, and the same treatment in ArtifactPermissionProvider would make it a complete
answer, at which point this method should delegate to CheckPermission(..., 'Update')
outright rather than compose an owner check around it.
The version plan to vet.
User the run executes as.
Provider for the lookups.
Agent name, for log lines.
Ids already rejected on a higher rung; mutated with any new rejection so the same id cannot be re-admitted further down the ladder.
The vetted plan with a trimmed id, or null to fall back.
AgentRunner provides a thin wrapper for executing AI agents.
This class handles:
AgentRunner
Example