Skip to content

Commit 7f08eea

Browse files
committed
Fix context prompt snapshot ordering
1 parent b6be363 commit 7f08eea

15 files changed

Lines changed: 321 additions & 128 deletions

apps/api/src/evidence-reference-context.ts

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import type {
44
EvidenceResolutionIssue
55
} from "@datafoundry/contracts";
66
import {
7-
createContextItem,
8-
createContextSourceMetadata,
9-
type ContextItem,
10-
type RuntimeContextSource
7+
createAgentContextItem,
8+
createAgentContextSourceMetadata,
9+
type AgentContextItem
1110
} from "@datafoundry/agent-runtime";
1211
import type {
1312
MetadataStore,
@@ -27,14 +26,14 @@ type ResolveEvidenceReferenceContextInput = {
2726

2827
export type ResolvedEvidenceReferenceContext = {
2928
diagnostics: EvidenceResolutionDiagnostics;
30-
items: ContextItem[];
29+
items: AgentContextItem[];
3130
};
3231

3332
/** Resolves client EvidenceRef handles into server-authoritative ContextItems for prompt assembly. */
3433
export function resolveEvidenceReferenceContext(
3534
input: ResolveEvidenceReferenceContextInput
3635
): ResolvedEvidenceReferenceContext {
37-
const items: ContextItem[] = [];
36+
const items: AgentContextItem[] = [];
3837
const accepted: string[] = [];
3938
const dropped: EvidenceResolutionIssue[] = [];
4039
const seen = new Set<string>();
@@ -58,21 +57,10 @@ export function resolveEvidenceReferenceContext(
5857
return { diagnostics: { accepted, dropped }, items };
5958
}
6059

61-
/** Creates a runtime context source from already-resolved evidence ContextItems. */
62-
export function createEvidenceRuntimeSource(items: ContextItem[]): RuntimeContextSource | undefined {
63-
if (items.length === 0) {
64-
return undefined;
65-
}
66-
return {
67-
sourceType: "evidence-focus",
68-
collect: () => items
69-
};
70-
}
71-
7260
const resolveEvidenceRef = (
7361
input: ResolveEvidenceReferenceContextInput,
7462
ref: EvidenceRef
75-
): { items: ContextItem[]; issue?: never } | { items?: never; issue: EvidenceResolutionIssue } => {
63+
): { items: AgentContextItem[]; issue?: never } | { items?: never; issue: EvidenceResolutionIssue } => {
7664
try {
7765
if (ref.source.artifactId) {
7866
return { items: artifactEvidenceItems(input, ref, ref.source.artifactId) };
@@ -99,7 +87,7 @@ const artifactEvidenceItems = (
9987
input: ResolveEvidenceReferenceContextInput,
10088
ref: EvidenceRef,
10189
artifactId: string
102-
): ContextItem[] => {
90+
): AgentContextItem[] => {
10391
const artifact = input.metadataStore.artifacts.get({ user_id: input.userId, artifact_id: artifactId });
10492
assertSession(ref, artifact.session_id);
10593
const preview = parseJsonValue(artifact.preview_json);
@@ -125,7 +113,7 @@ const sqlAuditEvidenceItems = (
125113
input: ResolveEvidenceReferenceContextInput,
126114
ref: EvidenceRef,
127115
auditLogId: string
128-
): ContextItem[] => {
116+
): AgentContextItem[] => {
129117
const audit = input.metadataStore.sqlAuditLogs.get({ user_id: input.userId, audit_log_id: auditLogId });
130118
if (audit.run_id) {
131119
const run = input.metadataStore.runs.get({ user_id: input.userId, run_id: audit.run_id });
@@ -154,7 +142,7 @@ const sqlAuditEvidenceItems = (
154142
const toolEventEvidenceItems = (
155143
input: ResolveEvidenceReferenceContextInput,
156144
ref: EvidenceRef
157-
): ContextItem[] => {
145+
): AgentContextItem[] => {
158146
const run = requireEvidenceRun(input, ref);
159147
const events = input.metadataStore.runEvents.listByRun({ user_id: input.userId, run_id: run.id });
160148
const event = findToolEvent(events, ref);
@@ -180,11 +168,11 @@ const toolEventEvidenceItems = (
180168
const createEvidenceItem = (
181169
input: ResolveEvidenceReferenceContextInput,
182170
ref: EvidenceRef,
183-
visibility: ContextItem["visibility"],
171+
visibility: AgentContextItem["visibility"],
184172
content: unknown,
185-
trust: ContextItem["trust"],
173+
trust: AgentContextItem["trust"],
186174
datasourceId?: string
187-
): ContextItem => createContextItem({
175+
): AgentContextItem => createAgentContextItem({
188176
id: `evidence:${ref.id}:${visibility}`,
189177
sourceType: "evidence-focus",
190178
sourceId: ref.id,
@@ -194,7 +182,7 @@ const createEvidenceItem = (
194182
retention: visibility === "model" ? "active" : "reference",
195183
priority: visibility === "model" ? 70 : 65,
196184
content,
197-
metadata: createContextSourceMetadata({
185+
metadata: createAgentContextSourceMetadata({
198186
dedupeKeys: evidenceDedupeKeys(ref),
199187
exclusivityKey: `evidence:${ref.id}`,
200188
overlapKeys: evidenceOverlapKeys(ref),

apps/api/src/run-agent-assembly.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
createDataFoundry,
66
createDataFoundryRunContext,
77
type AgentRunContext,
8+
type AgentContextItem,
89
type AgUiEventEmitter,
910
type GoalRuntimeAdapter,
10-
type RuntimeContextSource,
1111
type TaskStateRuntime,
1212
type WorkspaceAttachment
1313
} from "@datafoundry/agent-runtime";
@@ -54,7 +54,7 @@ type CreateRunAgentAssemblyInput = {
5454
dataGateway: DataGateway;
5555
effectiveRunConfig: EffectiveRunConfig;
5656
emitter: AgUiEventEmitter;
57-
evidenceRuntimeSources?: RuntimeContextSource[] | undefined;
57+
evidenceContextItems?: AgentContextItem[] | undefined;
5858
fileAssetService: FileAssetService;
5959
goal?: EffectiveRunConfig["goal"] | undefined;
6060
interactionResume?: InteractionResume | undefined;
@@ -143,9 +143,7 @@ export const createRunAgentAssembly = async (
143143
...(input.modelContextProfile ? { modelContextProfile: input.modelContextProfile } : {}),
144144
modelProvider: input.modelProvider,
145145
...(input.modelSettings ? { modelSettings: input.modelSettings } : {}),
146-
...(input.evidenceRuntimeSources?.length
147-
? { additionalRuntimeSources: input.evidenceRuntimeSources }
148-
: {}),
146+
...(input.evidenceContextItems?.length ? { evidenceContextItems: input.evidenceContextItems } : {}),
149147
...(input.longTermMemories.length > 0 ? { longTermMemory: { records: input.longTermMemories } } : {}),
150148
runContext: input.runContext,
151149
selectedSkills: input.selectedSkills,

apps/api/src/run-config-resolver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { RunAgentInput } from "@ag-ui/client";
22
import {
33
createModelProviderFromEnv,
44
createModelProviderFromProfile,
5-
type ModelContextProfile
5+
type AgentModelContextProfile
66
} from "@datafoundry/agent-runtime";
77
import type { MetadataStore } from "@datafoundry/metadata";
88
import {
@@ -26,7 +26,7 @@ export type ResolvedRunConfig = {
2626
temperature?: number;
2727
topP?: number;
2828
};
29-
modelContextProfile?: ModelContextProfile;
29+
modelContextProfile?: AgentModelContextProfile;
3030
reasoningModel?: boolean;
3131
runTimeoutMs?: number;
3232
skillSelection: SkillSelectionResult;
@@ -377,7 +377,7 @@ const resolveModelContextProfile = (
377377
metadataStore: MetadataStore,
378378
userId: string,
379379
workspaceId: string
380-
): ModelContextProfile | undefined => {
380+
): AgentModelContextProfile | undefined => {
381381
if (!profileId || profileId === "server-default") {
382382
return undefined;
383383
}

apps/api/src/server.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ import {
4848
sendAuthError
4949
} from "./auth/routes.js";
5050
import { persistCurrentUserMessage } from "./conversation-memory.js";
51-
import {
52-
createEvidenceRuntimeSource,
53-
resolveEvidenceReferenceContext
54-
} from "./evidence-reference-context.js";
51+
import { resolveEvidenceReferenceContext } from "./evidence-reference-context.js";
5552
import { createRunAgentAssembly, createRunAgentContext } from "./run-agent-assembly.js";
5653
import { resolveRunConfig } from "./run-config-resolver.js";
5754
import { resolveRunIdentity } from "./run-identity-orchestrator.js";
@@ -524,7 +521,6 @@ class DataFoundryAgUiAgent extends AbstractAgent {
524521
userId: this.input.user.id,
525522
workspaceId: this.input.workspaceId
526523
});
527-
const evidenceRuntimeSource = createEvidenceRuntimeSource(evidenceContext.items);
528524
const taskPlanProjector = new TaskPlanProjector(runContext);
529525
const toolCallResultBridge = new ToolCallResultBridge();
530526
const runAbortController = new AbortController();
@@ -552,7 +548,7 @@ class DataFoundryAgUiAgent extends AbstractAgent {
552548
dataGateway: this.input.dataGateway,
553549
artifactService: this.input.artifactService,
554550
effectiveRunConfig,
555-
...(evidenceRuntimeSource ? { evidenceRuntimeSources: [evidenceRuntimeSource] } : {}),
551+
...(evidenceContext.items.length ? { evidenceContextItems: evidenceContext.items } : {}),
556552
fileAssetService: this.input.fileAssetService,
557553
emitter: { emit },
558554
...(effectiveRunConfig.goal ? { goal: effectiveRunConfig.goal } : {}),

apps/web/src/app/data-tasks/__tests__/dashscope-prompt-compat.test.ts renamed to apps/web/src/app/data-tasks/__tests__/non-empty-message-content-compat.test.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { describe, expect, it } from "vitest";
22

33
import {
4-
ensureDashScopeCompatiblePrompt,
5-
shouldApplyDashScopePromptCompat,
6-
} from "../../../../../../packages/agent-runtime/src/context/dashscope-prompt-compat";
7-
8-
describe("dashscope prompt compat", () => {
9-
it("enables compat for openai-compatible providers", () => {
10-
expect(shouldApplyDashScopePromptCompat("openai-compatible")).toBe(true);
11-
expect(shouldApplyDashScopePromptCompat("unsupported-provider")).toBe(false);
4+
ensureNonEmptyMessageContentPrompt,
5+
shouldApplyNonEmptyMessageContentCompat,
6+
} from "../../../../../../packages/agent-runtime/src/provider-compat/non-empty-message-content-compat";
7+
8+
describe("non-empty message content compat", () => {
9+
it("enables compat only for providers that require non-empty message content", () => {
10+
expect(shouldApplyNonEmptyMessageContentCompat({
11+
prompt_compat: { requires_non_empty_message_content: true },
12+
})).toBe(true);
13+
expect(shouldApplyNonEmptyMessageContentCompat({})).toBe(false);
1214
});
1315

1416
it("adds placeholder text to assistant messages that only contain tool calls", () => {
15-
const prompt = ensureDashScopeCompatiblePrompt([
17+
const prompt = ensureNonEmptyMessageContentPrompt([
1618
{
1719
role: "assistant",
1820
content: [
@@ -30,7 +32,7 @@ describe("dashscope prompt compat", () => {
3032
});
3133

3234
it("adds placeholder text to assistant messages that only contain reasoning", () => {
33-
const prompt = ensureDashScopeCompatiblePrompt([
35+
const prompt = ensureNonEmptyMessageContentPrompt([
3436
{
3537
role: "assistant",
3638
content: [{ type: "reasoning", text: "thinking..." }],
@@ -41,7 +43,7 @@ describe("dashscope prompt compat", () => {
4143
});
4244

4345
it("fills empty tool result outputs", () => {
44-
const prompt = ensureDashScopeCompatiblePrompt([
46+
const prompt = ensureNonEmptyMessageContentPrompt([
4547
{
4648
role: "tool",
4749
content: [

packages/agent-runtime/src/context/inventory/context-run-state.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ export class ContextRunState {
4242
return this.currentPackage;
4343
}
4444

45+
replaceMatchingItems(shouldReplace: (item: ContextItem) => boolean, contextPackage: ContextPackage): ContextPackage {
46+
const retainedItems = this.currentPackage.items.filter((item) => !shouldReplace(item));
47+
const incomingItems = contextPackage.items.filter(shouldReplace);
48+
this.currentPackage = this.builder.build([...retainedItems, ...incomingItems], {
49+
packageId: this.currentPackage.packageId,
50+
revision: this.currentPackage.revision + 1,
51+
resourceId: this.identity.resourceId,
52+
sessionId: this.identity.sessionId,
53+
runId: this.identity.runId
54+
});
55+
return this.currentPackage;
56+
}
57+
4558
replaceSourceItems(sourceTypes: string[], contextPackage: ContextPackage): ContextPackage {
4659
const sourceTypeSet = new Set(sourceTypes);
4760
const retainedItems = this.currentPackage.items.filter((item) => !sourceTypeSet.has(item.sourceType));

packages/agent-runtime/src/context/protocol/mastra/mastra-context-budget-processor.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ProcessInputStepArgs, ProcessInputStepResult, Processor } from "@mastra/core/processors";
22

33
import { ContextPackageBuilder } from "../../inventory/context-package-builder.js";
4+
import type { ContextItem } from "../../inventory/context-item.js";
45
import type { ContextRunState } from "../../inventory/context-run-state.js";
56
import { ContextSourcePolicy } from "../../policy/context-source-policy.js";
67
import { isContextSourceOmissionDecision } from "../../policy/context-source-policy.js";
@@ -19,6 +20,7 @@ import {
1920
type MastraContextProtocolOutput
2021
} from "./mastra-context-protocol-adapter.js";
2122
import { createMastraConversationContextItems } from "./mastra-conversation-context-adapter.js";
23+
import { applyMastraPromptSnapshot } from "./mastra-prompt-snapshot-applicator.js";
2224
import type { MastraToolObservationRouter } from "./mastra-tool-observation-router.js";
2325

2426
export type MastraContextBudgetProcessorOptions = {
@@ -58,7 +60,7 @@ export class MastraContextBudgetProcessor implements Processor<"context-budget">
5860
runId: this.options.runState.identity.runId
5961
}
6062
);
61-
const contextPackage = this.options.runState.merge(livePackage);
63+
const contextPackage = this.options.runState.replaceMatchingItems(isProtocolMessageItem, livePackage);
6264
const sourcePolicyResult = this.sourcePolicy.applyPackage(contextPackage);
6365
const groupPlan = this.materializer.createGroupPlan({
6466
contextPackage,
@@ -93,10 +95,27 @@ export class MastraContextBudgetProcessor implements Processor<"context-budget">
9395
createMastraContextCompiledEventPayload(contextPackage, result.plan, this.options.modelName)
9496
);
9597
const protocolOutput = this.protocolAdapter.toProtocol(promptView);
98+
const systemMessages = protocolOutput.systemMessages as NonNullable<ProcessInputStepResult["systemMessages"]>;
99+
100+
if (!args.messageList) {
101+
return {
102+
messages: protocolOutput.messages,
103+
systemMessages
104+
};
105+
}
106+
107+
applyMastraPromptSnapshot(args.messageList, { messages: protocolOutput.messages });
96108

97109
return {
98-
messages: protocolOutput.messages,
99-
systemMessages: protocolOutput.systemMessages as NonNullable<ProcessInputStepResult["systemMessages"]>
110+
messageList: args.messageList,
111+
systemMessages
100112
};
101113
}
102114
}
115+
116+
const PROTOCOL_MESSAGE_KINDS = new Set(["message", "source-message", "system"]);
117+
118+
const isProtocolMessageItem = (item: ContextItem): boolean => {
119+
const messageKind = item.metadata.messageKind;
120+
return typeof messageKind === "string" && PROTOCOL_MESSAGE_KINDS.has(messageKind);
121+
};

packages/agent-runtime/src/context/protocol/mastra/mastra-context-processor-boundary.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from "../../policy/context-reduction-strategy.js";
88
import { createDefaultContextSourcePolicy } from "../../policy/context-source-authority-profile.js";
99
import { ContextStepPlanner } from "../../policy/context-step-planner.js";
10-
import { ModelContextProfileRegistry } from "../../policy/model-context-profile.js";
10+
import { ModelContextProfileRegistry, type ModelContextProfile } from "../../policy/model-context-profile.js";
1111
import { PromptTokenCounter } from "../../policy/prompt-token-counter.js";
1212
import { ContextPromptMaterializer } from "../../projection/context-prompt-materializer.js";
1313
import type { ContextSourcePromptMaterializer } from "../../projection/context-source-prompt-materializer.js";
@@ -42,6 +42,7 @@ export type CreateMastraContextProcessorBoundaryInput = {
4242
dispatcher: ToolObservationDispatcher;
4343
eventSink: ContextProtocolEventSink;
4444
longTermMemory?: CreateDefaultRuntimeContextSourceRegistryInput["longTermMemory"];
45+
modelContextProfile?: ModelContextProfile;
4546
modelName: string | undefined;
4647
runScope: RuntimeContextRunScope;
4748
runState: ContextRunState;
@@ -56,7 +57,9 @@ export type MastraContextProcessorBoundary = {
5657
export const createMastraContextProcessorBoundary = (
5758
input: CreateMastraContextProcessorBoundaryInput
5859
): MastraContextProcessorBoundary => {
59-
const profileRegistry = input.contextCompilation?.profileRegistry ?? new ModelContextProfileRegistry();
60+
const profileRegistry = input.contextCompilation?.profileRegistry ?? new ModelContextProfileRegistry({
61+
...(input.modelContextProfile ? { defaultProfile: input.modelContextProfile } : {})
62+
});
6063
const tokenCounter = input.contextCompilation?.tokenCounter ?? new PromptTokenCounter();
6164
const planner = new ContextStepPlanner({
6265
profileRegistry,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { MastraDBMessage } from "@mastra/core/agent";
2+
import type { ProcessInputStepArgs } from "@mastra/core/processors";
3+
4+
export type MastraPromptSnapshot = {
5+
messages: MastraDBMessage[];
6+
};
7+
8+
type MastraMessageList = NonNullable<ProcessInputStepArgs["messageList"]>;
9+
10+
// Mastra applies returned message arrays with per-message remove/add. Context compilation
11+
// produces a complete prompt snapshot, so apply it atomically to avoid assistant-message merging.
12+
export const applyMastraPromptSnapshot = (
13+
messageList: MastraMessageList,
14+
snapshot: MastraPromptSnapshot
15+
): void => {
16+
const sourceCheck = messageList.makeMessageSourceChecker();
17+
const currentIds = messageList.get.all.db().map((message) => message.id).filter(isString);
18+
19+
if (currentIds.length > 0) {
20+
messageList.removeByIds(currentIds);
21+
}
22+
23+
for (const message of snapshot.messages) {
24+
messageList.add(message, sourceCheck.getSource(message) ?? "input");
25+
}
26+
};
27+
28+
const isString = (value: unknown): value is string => typeof value === "string";

0 commit comments

Comments
 (0)