Skip to content
Merged
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
18 changes: 12 additions & 6 deletions packages/agent-core/src/event-bridge/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,17 @@ export class EventBridge {
* tracing, daemon SSE consumers) can see Pi's full producer-side
* lifecycle without affecting the canonical wire format. Callers can
* still safely forward every event without coordination.
* `observedAtMs` uses the bridge clock and is captured before caller-side
* queues or response hooks; omitted timestamps are measured during processing.
*/
async processEvent(event: AgentEvent): Promise<BridgedEvents> {
async processEvent(event: AgentEvent, observedAtMs?: number): Promise<BridgedEvents> {
switch (event.type) {
case 'message_start':
return this.onMessageStart(event);
case 'message_update':
return this.onMessageUpdate(event);
return this.onMessageUpdate(event, observedAtMs);
case 'message_end':
return this.onMessageEnd(event);
return this.onMessageEnd(event, observedAtMs);
case 'tool_execution_start':
return this.onToolStart(event);
case 'tool_execution_end':
Expand Down Expand Up @@ -378,7 +380,10 @@ export class EventBridge {
return { events: [], requestAssistantMessageId: true };
}

private onMessageUpdate(event: Extract<AgentEvent, { type: 'message_update' }>): BridgedEvents {
private onMessageUpdate(
event: Extract<AgentEvent, { type: 'message_update' }>,
observedAtMs?: number,
): BridgedEvents {
const update = extractDeltaUpdate(event.assistantMessageEvent);
if (!update) return { events: [] };
if (!this.activeAssistantMessageId) {
Expand Down Expand Up @@ -407,7 +412,7 @@ export class EventBridge {
((update.kind === 'toolcall_start' || update.kind === 'toolcall_delta') &&
update.toolName !== undefined))
) {
this.firstTokenMs = this.now();
this.firstTokenMs = observedAtMs ?? this.now();
}
if (
update.kind === 'toolcall_start' ||
Expand Down Expand Up @@ -526,6 +531,7 @@ export class EventBridge {

private async onMessageEnd(
event: Extract<AgentEvent, { type: 'message_end' }>,
observedAtMs?: number,
): Promise<BridgedEvents> {
const role = (event.message as { role?: unknown }).role;
if (role !== 'assistant') return { events: [] };
Expand Down Expand Up @@ -600,7 +606,7 @@ export class EventBridge {
: undefined;
const decodeDurationMs =
this.ctx.includeDetailedUsage === true && this.firstTokenMs !== undefined
? Math.max(0, this.now() - this.firstTokenMs)
? Math.max(0, (observedAtMs ?? this.now()) - this.firstTokenMs)
: undefined;
const usage: TokenUsage = {
...(extractAssistantUsage(event.message, this.contextWindow) ?? {
Expand Down
20 changes: 14 additions & 6 deletions packages/agent-core/src/pi-turn-runner/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ export async function emitTerminal(turn: turnState, reason: TurnTerminationReaso
}
}

export function subscribeEvents(agent: Agent, turn: turnState, history: turnHistory): eventSub {
export function subscribeEvents(
agent: Agent,
turn: turnState,
history: turnHistory,
nowMs: () => number,
): eventSub {
let pendingMessageID: Promise<string> | undefined;
let queue: Promise<void> = Promise.resolve();
let forcedTermination: TurnTerminationReason | undefined;
Expand Down Expand Up @@ -254,10 +259,10 @@ export function subscribeEvents(agent: Agent, turn: turnState, history: turnHist
}
};

const writeAgentEvent = async (event: AgentEvent): Promise<void> => {
const writeAgentEvent = async (event: AgentEvent, observedAtMs: number): Promise<void> => {
let out: Awaited<ReturnType<typeof turn.bridge.processEvent>>;
try {
out = await turn.bridge.processEvent(event);
out = await turn.bridge.processEvent(event, observedAtMs);
} catch (err) {
turn.metrics.recordDegradation('event_bridge', 'translation', err);
throw err;
Expand Down Expand Up @@ -297,14 +302,14 @@ export function subscribeEvents(agent: Agent, turn: turnState, history: turnHist
}
};

const onAgentEvent = async (event: AgentEvent): Promise<void> => {
const onAgentEvent = async (event: AgentEvent, observedAtMs: number): Promise<void> => {
let discardMessage = false;
try {
if (event.type === 'message_end') {
discardMessage = (await applyAfterLlmControl(event)) === 'discard';
if (discardMessage) discardAssistantMessage(event);
}
if (!discardMessage) await writeAgentEvent(event);
if (!discardMessage) await writeAgentEvent(event, observedAtMs);
} catch (err) {
turn.logger.error(
{
Expand Down Expand Up @@ -354,10 +359,13 @@ export function subscribeEvents(agent: Agent, turn: turnState, history: turnHist
};

const unsubscribe = agent.subscribe((event: AgentEvent) => {
// Capture before queueing or awaiting response hooks so decode timing only
// spans model events, not caller-side control or delivery work.
const observedAtMs = nowMs();
// Serialize caller-side work even when Pi emits events from parallel
// tool completions. History delivery failures reject the current Pi
// listener; the resolved queue link still lets Pi's failure events drain.
const queued = queue.then(() => onAgentEvent(event));
const queued = queue.then(() => onAgentEvent(event, observedAtMs));
queue = queued.catch(() => undefined);
return queued;
});
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/pi-turn-runner/pi-turn-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export class PiTurnRunner {
// history.flushTail() at message / tool boundaries, and run onStepEnd. A single queue keeps
// RuntimeEvent ordering identical to the old monolithic runner. The later `events.drain()`
// waits for this queue.
const events = subscribeEvents(agent, turn, history);
const events = subscribeEvents(agent, turn, history, this.now);

// Step 6: Wire cancellation. If the caller's signal is already aborted, do not start
// the Agent: unsubscribe, drain queued events, and emit the terminal `aborted` frame. Otherwise,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const MCODE_TOOLS_MODEL_PREFIX = 'MiniMax-M2.7';

const MCODE_TOOLS_MASTER_REMINDER = [
'<mcode-tools-master-reminder>',
'For all video, image, and audio understanding and generation tasks, use the mcode-tools-master skill.',
'For multimodal generation and understanding, including images/photos, video, audio, music, and documents, load the mcode-tools-master skill and use mcode-tools through Bash.',
'Discover the relevant tools before concluding that a multimodal capability is unavailable.',
'</mcode-tools-master-reminder>',
].join('\n');

Expand Down
70 changes: 50 additions & 20 deletions packages/config/src/byok-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// (they are part of the Config surface); the back edge here is type-only, so
// there is no runtime import cycle.

import { MINIMAX_MODELS } from './minimax-model-catalog.js';

import { createHash, randomUUID } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
Expand All @@ -12,6 +14,7 @@ import { restrictConfigFileSync, writePrivateConfigFileSync } from './private-co

import type {
CustomProvidersConfig,
ModelConfig,
MinimaxApiConfig,
ModelsConfig,
ProviderConfig,
Expand All @@ -37,31 +40,58 @@ export function parseModelContextLimits(raw: unknown): Record<string, number> |
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
}

/** Apply user-owned managed Context selections without mutating the official snapshot. */
/** Apply user-owned Context selections without mutating the official snapshot. */
function applyModelContextLimits<
T extends { limit?: { context?: number }; contextWindowOptions?: number[] },
>(models: Record<string, T>, contextLimits: Record<string, number> | undefined): Record<string, T> {
if (!contextLimits) return models;
return Object.fromEntries(
Object.entries(models).map(([modelId, model]) => {
const context = contextLimits[modelId];
return [
modelId,
context !== undefined && model.contextWindowOptions?.includes(context)
? { ...model, limit: { ...model.limit, context } }
: model,
];
}),
);
}

/** The API route shares the managed catalog, with its own user Context selections. */
export function minimaxApiModels<
T extends {
enabled?: boolean;
limit?: { context?: number };
contextWindowOptions?: ModelConfig['contextWindowOptions'];
},
>(config: {
provider?: Record<string, { models?: Record<string, T>; catalogModels?: Record<string, T> }>;
minimax_api?: { modelContextLimits?: Record<string, number> };
}): Record<string, T | ModelConfig> {
const provider = config.provider?.minimax;
const models = provider?.catalogModels ?? provider?.models ?? MINIMAX_MODELS;
return applyModelContextLimits(
Object.fromEntries(Object.entries(models).filter(([, model]) => model.enabled !== false)),
config.minimax_api?.modelContextLimits,
);
}

/** Keep the shared catalog before applying managed-only user Context selections. */
export function applyManagedMinimaxContextLimits(
provider: ModelsConfig,
contextLimits: Record<string, number> | undefined,
): ModelsConfig {
const minimax = provider.minimax;
if (!minimax?.models || !contextLimits) return provider;

let changed = false;
const models = Object.fromEntries(
Object.entries(minimax.models).map(([modelId, model]) => {
const context = contextLimits[modelId];
if (
context === undefined ||
!model.contextWindowOptions?.includes(context) ||
model.limit?.context === context
) {
return [modelId, model];
}
changed = true;
return [modelId, { ...model, limit: { ...model.limit, context } }];
}),
);

return changed ? { ...provider, minimax: { ...minimax, models } } : provider;
if (!minimax?.models) return provider;
return {
...provider,
minimax: {
...minimax,
catalogModels: minimax.models,
models: applyModelContextLimits(minimax.models, contextLimits),
},
};
}

export function parseCustomProvidersConfig(raw: unknown): CustomProvidersConfig | undefined {
Expand Down
97 changes: 13 additions & 84 deletions packages/config/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MINIMAX_MODELS } from "./minimax-model-catalog.js";
import {
resolveRunawayGuardConfig,
type RunawayGuardSettings,
Expand Down Expand Up @@ -495,11 +496,9 @@ export interface BetaConfig {
*/
mcodeTools: boolean;
/**
* OpenAI Codex OAuth settings entry. Enabled by default in internal builds and development
* environments, regardless of the connected backend environment. Public prod / external staging /
* Inside / test builds expose the OAuth connection button in desktop model settings via
* `beta.codexOAuth: true`. Provider configuration stays in `config.yaml` and OAuth credentials
* stay in `<dataDir>/codex-auth.json`.
* OpenAI Codex OAuth model settings entry. Enabled by default in every build environment and
* release package; set `beta.codexOAuth: false` to hide it. Provider configuration stays in
* `config.yaml` and OAuth credentials stay in `<dataDir>/codex-auth.json`.
*/
codexOAuth: boolean;
}
Expand Down Expand Up @@ -648,9 +647,7 @@ export const BETA_FEATURE_DEFS = {
configurableVisibility: "online",
},
codexOAuth: {
defaultVisibility: "none",
defaultBuildEnvironments: ["dev"],
defaultBuildVariants: ["internal"],
defaultVisibility: "online",
configurableVisibility: "online",
},
} satisfies Record<keyof BetaConfig, BetaFeatureDef>;
Expand All @@ -675,7 +672,7 @@ function getBetaFeatureChannel(
}

// Variant defaults depend only on variant flags: internal builds use buildEnv=staging but remain on the internal release lane
// and must retain internal defaults such as codexOAuth. Inside builds still use buildEnv=prod.
// and must retain internal defaults. Inside builds still use buildEnv=prod.
function isDefaultEnabledForBuildVariant(
def: BetaFeatureDef,
context: BetaFeatureResolutionContext,
Expand Down Expand Up @@ -1019,6 +1016,8 @@ export interface Config {
cuBackend: CuBackend;
/** Built-in local code review behavior. */
review: ReviewConfig;
/** Fetch the selected upstream before creating a new worktree. */
worktreeRefreshBeforeCreate: boolean;
/** Automatic prompt delivery policy. */
promptConfig: PromptConfig;
/** Durable Goal policy, budgets, breaker, and verifier defaults. */
Expand Down Expand Up @@ -1137,6 +1136,8 @@ export interface ProviderConfig {
id?: string;
npm?: string;
models?: Record<string, ModelConfig>;
/** Read-time reference to the official catalog before user Context selections. */
catalogModels?: Record<string, ModelConfig>;
/** Display order for managed models; omitted models retain their catalog order. */
model_order?: string[];
blacklist?: string[];
Expand All @@ -1159,7 +1160,7 @@ export interface MinimaxApiConfig {
apiKey?: string;
/** Optional endpoint override; defaults to the runtime's builtin MiniMax API endpoint. */
baseURL?: string;
/** User-owned context selections applied only to the independent MiniMax API catalog. */
/** User-owned Context selections for the API route of the shared MiniMax catalog. */
modelContextLimits?: Record<string, number>;
}

Expand Down Expand Up @@ -1516,80 +1517,6 @@ export function getConfigPath(): string {
// Default provider presets per environment combo (MAVIS_REGION x MAVIS_BUILD_ENV)
// ---------------------------------------------------------------------------

const MINIMAX_M3_FILE_API_CAPABILITIES: ModelCapabilitiesConfig = {
support_files_api: true,
files_api_upload_endpoint: "/v1/files/upload",
max_image_bytes_inline: 10_485_760,
max_video_bytes_inline: 52_428_800,
max_request_body_bytes: 67_108_864,
max_attachments_count: 4,
};

const MINIMAX_MODELS: Record<string, ModelConfig> = {
"MiniMax-M3": {
name: "MiniMax-M3",
attachment: true,
reasoning: true,
tool_call: true,
temperature: true,
modalities: { input: ["text", "image", "video"], output: ["text"] },
limit: { context: 512000, output: 128000 },
contextWindowOptions: [512000, 1000000],
contextWindowOptionHints: { "1000000": "higher_usage" },
options: { reasoningSummary: "auto" },
thinking_config: { mode: "switchable", default_value: "true" },
variants: {
"none-thinking": { thinking: { type: "disabled" } },
thinking: { thinking: { type: "adaptive" } },
},
capabilities: MINIMAX_M3_FILE_API_CAPABILITIES,
},
"MiniMax-M3.1-Flash-Preview": {
name: "M3.1-Flash-Preview",
attachment: true,
reasoning: true,
tool_call: true,
temperature: true,
modalities: { input: ["text", "image", "video"], output: ["text"] },
limit: { context: 512000, output: 128000 },
contextWindowOptions: [512000, 1000000],
contextWindowOptionHints: { "1000000": "higher_usage" },
options: { reasoningSummary: "auto" },
thinking: {
effortOptions: ["default", "low", "medium", "high", "xhigh", "max"],
defaultEffort: "default",
},
thinking_config: { mode: "forced_on" },
variants: {
"none-thinking": { thinking: { type: "disabled" } },
thinking: { thinking: { type: "adaptive" } },
},
capabilities: MINIMAX_M3_FILE_API_CAPABILITIES,
},
"MiniMax-M2.7-highspeed": {
name: "MiniMax-M2.7-highspeed",
attachment: false,
reasoning: true,
tool_call: true,
temperature: true,
modalities: { input: ["text"], output: ["text"] },
limit: { context: 200000, output: 128000 },
},
"MiniMax-M2.7": {
name: "MiniMax-M2.7",
attachment: false,
reasoning: true,
tool_call: true,
temperature: true,
modalities: { input: ["text"], output: ["text"] },
limit: { context: 200000, output: 128000 },
},
};

/** MiniMax models the user's own API key may call and the first-run managed fallback. */
export const MINIMAX_API_MODEL_CATALOG: Record<string, ModelConfig> =
MINIMAX_MODELS;

const PRESET_BASE_URLS: Record<PresetKey, string> = {
"cn-test": "https://matrix-test.example.invalid/mavis/api/v1/llm/v1",
"cn-dev": "https://matrix-test.example.invalid/mavis/api/v1/llm/v1",
Expand Down Expand Up @@ -1836,6 +1763,7 @@ const DEFAULTS: Omit<
},
logRetentionDays: 3,
cuBackend: DEFAULT_CU_BACKEND,
worktreeRefreshBeforeCreate: true,
review: {
mode: "subagent",
modeSource: "default",
Expand Down Expand Up @@ -2129,6 +2057,7 @@ export function resolveConfigFromRaw(
mcpToolSearch: parseMcpToolSearchConfig(raw),
cuBackend: parseCuBackend(raw.cuBackend),
review: parseReviewConfig(raw.review, DEFAULTS.review),
worktreeRefreshBeforeCreate: raw.worktreeRefreshBeforeCreate !== false,
promptConfig: resolvePromptConfig(raw.promptConfig),
goal: goalParsed.config,
goalWarnings: goalParsed.warnings,
Expand Down
Loading
Loading