blob: 87b6d9022df532365f2d8b0ecba2a5a49c288def [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { randomUUID } from 'node:crypto';
import { createRunCompositionSnapshot } from '@maka/core/run-composition';
import { resolveModelVisionSupport } from '@maka/core/model-metadata';
import { modelOverride } from '@maka/core/model-thinking';
import type { ModelCallAttempt } from '@maka/core/model-call-attempt';
import type { ModelCallCommit } from '@maka/core/agent-run';
import type { PermissionMode } from '@maka/core/permission';
import { resolveCollaborationPermissionMode } from '@maka/core/collaboration';
import { AiSdkBackend } from '@maka/runtime/ai-sdk-backend';
import {
buildDefaultContextBudgetPolicy,
resolveSelectedModelContextWindow,
} from '@maka/runtime/context-budget-policy';
import { buildLlmHistorySummarizer } from '@maka/runtime/history-compact-summarizer';
import {
buildOpenAiCodexHistoryCompactor,
withOpenAiCodexHistoryCompactionFallback,
} from '@maka/runtime/openai-codex-history-compactor';
import { buildPricingLookup, recordToolInvocation } from '@maka/runtime/telemetry';
import { buildProviderOptions, getAIModel } from '@maka/runtime/model-factory';
import {
createProxiedFetchTransport,
type ProxiedFetchProxy,
type ProxiedFetchTransport,
} from '@maka/runtime/network/scoped-fetch-transport';
import { stableHash, toolCatalogHash } from '@maka/runtime/request-shape';
import { toolAvailabilityHash } from '@maka/runtime/tool-availability';
import type { MakaTool } from '@maka/runtime/tool-runtime';
import {
type BackendFactoryContext,
type BackendPreparationContext,
type PreparedBackendActivation,
} from '@maka/runtime/session-manager';
import { type RuntimeCommitSink } from '@maka/runtime/runtime-commit-sink';
import {
createAttachmentByteReader,
createReadImageSnapshotPlanner,
type InteractiveArtifactStoreWriter,
} from '@maka/storage/artifact-stores';
import type { InteractiveContextOffloadReader } from '@maka/storage/context-offload-store';
import { createReadImageSnapshotReader } from '@maka/storage/read-image-snapshot-store';
import type { RuntimePolicyStoresWriter } from '@maka/storage/runtime-policy-stores';
import type { InteractiveUsageStoresWriter } from '@maka/storage/usage-stores';
import {
createHostOAuthModelFetch,
type HostOAuthExecutionAuthority,
} from './oauth-execution-authority.js';
import type { HostChildAgentBackendCapabilities } from './child-agent-composition.js';
import type { HostExecutionArtifactServices } from './execution-artifacts.js';
import type { HostMemoryExtractionCoordinator } from './memory-extraction-coordinator.js';
import {
readDuringBackendCreation,
resolveExecutionTarget,
type ResolvedExecutionTarget,
} from './execution-model-authority.js';
import { toRuntimePolicyProxy } from './runtime-policy-proxy.js';
import type { HostRunComposer, HostRunComposerFactory } from './host-run-composer.js';
export interface HostAiSdkBackendInput {
readonly context: BackendFactoryContext;
readonly runtimePolicy: HostExecutionRuntimePolicyAuthority;
readonly oauthCredentials: HostOAuthExecutionAuthority;
readonly createRunComposer: HostRunComposerFactory;
readonly memoryExtraction?: HostMemoryExtractionCoordinator;
readonly artifacts: HostExecutionArtifactAuthority;
readonly contextOffload?: InteractiveContextOffloadReader;
readonly contextOffloadUnavailable?: boolean;
readonly executionArtifacts: HostExecutionArtifactServices;
readonly usage: HostExecutionUsageAuthority;
readonly requestDrain: () => void;
readonly runtimeCommitSink?: RuntimeCommitSink;
readonly childAgents?: HostChildAgentBackendCapabilities;
readonly createFetchTransport?: (proxy: ProxiedFetchProxy | null) => ProxiedFetchTransport;
}
export type HostAiSdkBackendPreparationInput = Omit<HostAiSdkBackendInput, 'context'> & {
readonly context: BackendPreparationContext;
};
type HostExecutionRuntimePolicyAuthority = {
readonly operations: Pick<RuntimePolicyStoresWriter['operations'], 'resolveExecutionConnection'>;
readonly runtimePolicy: Pick<RuntimePolicyStoresWriter['runtimePolicy'], 'getSnapshot'>;
};
type HostExecutionArtifactAuthority = Pick<
InteractiveArtifactStoreWriter,
'create' | 'readDurableAttachmentBinary'
>;
type HostExecutionUsageAuthority = {
readonly telemetry: Pick<InteractiveUsageStoresWriter['telemetry'], 'recordToolInvocation'>;
readonly modelCalls: Pick<
InteractiveUsageStoresWriter['modelCalls'],
'catchUpModelCallProjection'
>;
readonly pricing: Pick<InteractiveUsageStoresWriter['pricing'], 'snapshot'>;
};
/** Builds one real provider backend from canonical Host state. */
export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Promise<AiSdkBackend> {
const createFetchTransport = input.createFetchTransport ?? createProxiedFetchTransport;
const target = await readDuringBackendCreation(
() =>
resolveExecutionTarget(
input.context.header,
input.runtimePolicy,
input.oauthCredentials,
createFetchTransport,
),
input.context.abortSignal,
);
return await buildHostAiSdkBackend(input, target);
}
async function buildHostAiSdkBackend(
input: HostAiSdkBackendInput,
target: ResolvedExecutionTarget,
): Promise<AiSdkBackend> {
const createFetchTransport = input.createFetchTransport ?? createProxiedFetchTransport;
const pricingSnapshot = await readDuringBackendCreation(
() => input.usage.pricing.snapshot(),
input.context.abortSignal,
);
const pricing = buildPricingLookup(pricingSnapshot.overrides);
const runtimePolicySnapshot = await readDuringBackendCreation(
() => input.runtimePolicy.runtimePolicy.getSnapshot(),
input.context.abortSignal,
);
const transport = createFetchTransport(
toRuntimePolicyProxy(target.networkProxy, target.proxySecret),
);
let apiKey = target.apiKey;
let modelFetch: typeof fetch = transport.fetch;
const oauthBinding = target.oauthBinding;
if (oauthBinding) {
try {
const initialOAuthTokens = await readDuringBackendCreation(
() => oauthBinding.resolve(),
input.context.abortSignal,
);
apiKey = initialOAuthTokens.access_token;
modelFetch = createHostOAuthModelFetch({
binding: oauthBinding,
initialTokens: initialOAuthTokens,
connection: target.connection,
sessionId: input.context.sessionId,
modelId: target.model,
fetchFn: transport.fetch,
});
} catch (error) {
await transport.close();
throw error;
}
}
const providerOptions = buildProviderOptions(
target.connection,
target.model,
input.context.header.thinkingLevel,
);
const contextWindow = resolveSelectedModelContextWindow(target.connection, target.model);
let modelComposition: HostRunComposer;
try {
modelComposition = await readDuringBackendCreation(
async () =>
await input.createRunComposer({
backendContext: input.context,
connection: target.connection,
modelId: target.model,
runtimePolicy: runtimePolicySnapshot,
contextWindow: contextWindow ?? null,
}),
input.context.abortSignal,
);
} catch (error) {
await transport.close();
throw error;
}
const modelFactory = (
modelInput: Parameters<typeof getAIModel>[0],
): ReturnType<typeof getAIModel> =>
getAIModel({
...modelInput,
fetch: modelFetch,
requestHeaders: target.requestHeaders,
});
const resolveHistoryCompactModel = () =>
getAIModel({
sessionId: input.context.sessionId,
connection: target.connection,
apiKey,
modelId: target.model,
fetch: modelFetch,
requestHeaders: target.requestHeaders,
});
const textHistorySummarizer = buildLlmHistorySummarizer({
resolveModel: resolveHistoryCompactModel,
providerOptions,
});
const summarizeHistoryCompact =
target.connection.providerType === 'openai-codex' && input.context.header.llmConnectionId
? withOpenAiCodexHistoryCompactionFallback(
buildOpenAiCodexHistoryCompactor({
resolveModel: resolveHistoryCompactModel,
connectionId: input.context.header.llmConnectionId,
providerStateIdentity: target.providerStateIdentity,
modelId: target.model,
providerOptions,
}),
textHistorySummarizer,
)
: textHistorySummarizer;
const historyCompactRoute =
target.connection.providerType === 'openai-codex' && input.context.header.llmConnectionId
? 'provider_native'
: 'text_summary';
let telemetryDrainRequested = false;
const persistTelemetry = async (operation: () => Promise<void>): Promise<void> => {
try {
await operation();
} catch (error) {
if (!telemetryDrainRequested) {
telemetryDrainRequested = true;
input.requestDrain();
}
throw error;
}
};
const telemetry = {
insertToolInvocation: (
record: Parameters<typeof input.usage.telemetry.recordToolInvocation>[0],
) => persistTelemetry(() => input.usage.telemetry.recordToolInvocation(record)),
};
/**
* One canonical record, one commit point (#1679).
*
* The AgentRun stream is the only durable authority. The Usage ledger is a
* projection of it and is written only once the authority holds the record —
* writing both in parallel would make the ledger a second source of truth,
* free to diverge with no way back.
*
* A failed projection is recoverable, not lost: its checkpoint remains
* behind the AgentRun sequence until a later catch-up consumes it. The
* projection may not fail the turn — the provider call has already completed
* and billed.
*/
let accountingAuthorityFailed = false;
const recordModelCallAttempt = async (
commit: ModelCallCommit<ModelCallAttempt>,
): Promise<void> => {
const attempt = commit.attempt;
try {
// Forwarded whole. Taking `attempt` alone here is what silently dropped
// the derived latest-context row before it reached storage (#2323).
await input.context.recordModelCallAttempt?.(commit);
} catch (error) {
accountingAuthorityFailed = true;
throw error;
}
await input.usage.modelCalls
.catchUpModelCallProjection({ sessionId: attempt.sessionId, runId: attempt.runId })
.catch(() => undefined);
};
/**
* Fail-closed pre-dispatch gate, keyed on the authority alone. A stale
* projection is recoverable and must not block a send; an authority that has
* stopped accepting records means the next dispatch produces spend nothing
* will ever hold, so the send fails before the provider is called.
*
* Not `telemetryDrainRequested`: that flag tracks the frozen legacy table,
* which no longer meters main sends at all.
*/
const assertModelCallAccountingReady = (): void => {
if (accountingAuthorityFailed) {
throw new Error('Canonical model-call accounting authority is unavailable');
}
};
const resolveRunPrompt = async (context: {
readonly turnId: string;
readonly emitSkillCatalogTrace?: (message: string, data?: Record<string, unknown>) => void;
}) => {
const resolved = await modelComposition.resolveSystemPrompt({
sessionId: input.context.sessionId,
turnId: context.turnId,
cwd: input.context.header.cwd,
...(context.emitSkillCatalogTrace
? { emitSkillCatalogTrace: context.emitSkillCatalogTrace }
: {}),
});
const model = target.model.replace(/[\r\n\t]+/g, ' ').trim();
return Object.freeze({
...resolved,
text: [`Active model: ${model}`, resolved.text].filter(Boolean).join('\n\n'),
});
};
const recordRunComposition = input.context.recordRunComposition;
const recordRequestComposition = input.context.recordRequestComposition;
const resolveModelTools = (): readonly MakaTool[] =>
modelComposition.resolveTools?.() ?? modelComposition.tools;
// RunComposition remains the immutable C0 baseline. Dynamic Tool changes
// belong exclusively to RequestComposition epochs, so never re-sample them
// while committing the baseline immediately before provider dispatch.
const initialModelTools = Object.freeze([...modelComposition.tools]);
const runCompositionCommits = new Map<string, Promise<void>>();
const commitRunComposition = recordRunComposition
? async (context: { readonly turnId: string; readonly runId: string }): Promise<void> => {
let commit = runCompositionCommits.get(context.runId);
if (!commit) {
commit = (async (): Promise<void> => {
const resolved = await resolveRunPrompt(context);
await recordRunComposition(
context.runId,
createRunCompositionSnapshot({
composerId: modelComposition.composerId,
composerRevision: modelComposition.composerRevision,
sourceRevisions: resolved.sourceRevisions,
baseSystemPromptHash: stableHash(resolved.text ?? ''),
toolCatalogHash: toolCatalogHash(initialModelTools),
toolAvailabilityHash: toolAvailabilityHash(modelComposition.toolAvailability),
baseProviderOptionsHash: stableHash(providerOptions),
toolNames: initialModelTools.map(({ name }) => name),
contextWindow: contextWindow ?? null,
}),
);
})();
runCompositionCommits.set(context.runId, commit);
}
try {
await commit;
} catch (error) {
if (runCompositionCommits.get(context.runId) === commit) {
runCompositionCommits.delete(context.runId);
}
throw error;
}
}
: undefined;
const planProjectionImage = createReadImageSnapshotPlanner(input.artifacts);
try {
return new HostAiSdkBackend(
{
sessionId: input.context.sessionId,
header: {
...input.context.header,
model: target.model,
permissionMode: resolveCollaborationPermissionMode({
collaborationMode: input.context.header.collaborationMode ?? 'agent',
permissionMode: input.context.header.permissionMode,
}),
},
...(input.context.recordSystemNote
? { recordSystemNote: input.context.recordSystemNote }
: {}),
readExecutionBoundary: () =>
input.context.store.readExecutionBoundary(input.context.sessionId),
readPermissionMode: async () =>
(await input.context.store.readHeader(input.context.sessionId)).permissionMode,
...(input.context.store.createSandboxBoundaryRequest
? {
createSandboxBoundaryRequest: (request) =>
input.context.store.createSandboxBoundaryRequest!(request),
}
: {}),
...(input.context.store.settleSandboxBoundaryRequest
? {
settleSandboxBoundaryRequest: (request) =>
input.context.store.settleSandboxBoundaryRequest!(request),
}
: {}),
connection: target.connection,
providerStateIdentity: target.providerStateIdentity,
apiKey,
modelId: target.model,
modelFactory,
tools: [...resolveModelTools()],
resolveTools: resolveModelTools,
toolAvailability: modelComposition.toolAvailability,
...(modelComposition.planTraceContext
? { planTraceContext: modelComposition.planTraceContext }
: {}),
...(!input.context.tools && input.childAgents ? input.childAgents : {}),
providerOptions,
contextBudget: buildDefaultContextBudgetPolicy({
name: 'runtime-host-default-history-budget',
modelId: target.model,
}),
supportsVision: resolveModelVisionSupport(
target.connection.providerType,
target.connection.models,
target.model,
modelOverride(target.connection, target.model)?.vision,
),
readAttachmentBytes: createAttachmentByteReader({
artifactStore: input.artifacts,
sessionId: input.context.sessionId,
...(input.contextOffload
? {
readImageSnapshots: createReadImageSnapshotReader(
input.contextOffload,
input.context.sessionId,
),
}
: {}),
...(!input.contextOffload && input.contextOffloadUnavailable
? { readImageSnapshotsUnavailable: true }
: {}),
}),
prepareDurableProjectionArtifact: ({ turnId, bytes, mediaType }) =>
planProjectionImage({
sessionId: input.context.sessionId,
turnId,
name: 'Tool Result image',
bytes,
mimeType: mediaType,
}),
recordToolArtifacts: input.executionArtifacts.recordToolArtifacts,
toolResultArchive: input.executionArtifacts.toolResultArchive,
...(!input.context.tools &&
!input.context.header.subagentParent &&
input.context.header.collaborationMode !== 'plan' &&
input.memoryExtraction
? {
memoryExtraction: input.memoryExtraction.sourceCapabilities(
runtimePolicySnapshot.policy.privacy.incognitoActive
? { allowed: false, reason: 'incognito' }
: runtimePolicySnapshot.policy.memory.enabled
? { allowed: true }
: { allowed: false, reason: 'disabled' },
),
}
: {}),
loadHistoryCompactCheckpoint: input.context.loadHistoryCompactCheckpoint,
summarizeHistoryCompact,
historyCompactRoute,
recordHistoryCompactCheckpoint: input.context.recordHistoryCompactCheckpoint,
loadModelProjectionTransitions: input.context.loadModelProjectionTransitions,
recordModelProjectionTransition: input.context.recordModelProjectionTransition,
loadTurnRuntimeEvents: input.context.loadTurnRuntimeEvents,
allowMidTurnHistoryCompaction: input.context.allowMidTurnHistoryCompaction,
recordRunTrace: input.context.recordRunTrace,
...(commitRunComposition
? {
beforeRunProviderDispatch: commitRunComposition,
}
: {}),
...(recordRequestComposition
? {
recordRequestComposition: (runId, snapshot) =>
recordRequestComposition(runId, snapshot),
}
: {}),
systemPrompt: async (context) => {
const resolved = await resolveRunPrompt({
turnId: context.turnId,
...(context.emitSkillCatalogTrace
? { emitSkillCatalogTrace: context.emitSkillCatalogTrace }
: {}),
});
return { text: resolved.text, sourceRevisions: resolved.sourceRevisions };
},
lookupPricing: pricing,
recordModelCallAttempt,
assertModelCallAccountingReady,
recordToolInvocation: (event) => recordToolInvocation({ repo: telemetry }, event),
...(input.runtimeCommitSink ? { runtimeCommitSink: input.runtimeCommitSink } : {}),
newId: randomUUID,
now: Date.now,
},
transport.close,
() => modelComposition.release?.(),
);
} catch (error) {
try {
await transport.close();
} finally {
modelComposition.release?.();
}
throw error;
}
}
export async function prepareHostAiSdkBackend(
input: HostAiSdkBackendPreparationInput,
): Promise<PreparedBackendActivation> {
const createFetchTransport = input.createFetchTransport ?? createProxiedFetchTransport;
const preparedTarget = await readDuringBackendCreation(
() =>
resolveExecutionTarget(
input.context.header,
input.runtimePolicy,
input.oauthCredentials,
createFetchTransport,
),
input.context.abortSignal,
);
return {
providerStateIdentity: preparedTarget.providerStateIdentity,
build: (context) =>
buildHostAiSdkBackend(
{
...input,
context,
},
preparedTarget,
),
};
}
class HostAiSdkBackend extends AiSdkBackend {
constructor(
input: ConstructorParameters<typeof AiSdkBackend>[0],
private readonly closeTransport: () => Promise<void>,
private readonly releaseClientCapabilities: () => void,
) {
super(input);
}
override async dispose(): Promise<void> {
try {
await super.dispose();
} finally {
try {
await this.closeTransport();
} finally {
this.releaseClientCapabilities();
}
}
}
}