[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
549
infrastructure/ai/harness/turnDrivers/cattyTurnDriver.ts
Normal file
549
infrastructure/ai/harness/turnDrivers/cattyTurnDriver.ts
Normal file
@@ -0,0 +1,549 @@
|
||||
import type { ModelMessage } from 'ai';
|
||||
import type { OpenAIChatAssistantFields } from '../../providerContinuation';
|
||||
import {
|
||||
DEFAULT_CONTEXT_WINDOW_TOKENS,
|
||||
DEFAULT_PROTECT_RECENT_MESSAGES,
|
||||
estimateUnknownTokens,
|
||||
findSafeChatMessageCompactionSplitIndex,
|
||||
resolveContextWindow,
|
||||
} from '../../contextCompaction';
|
||||
import { buildSystemPrompt } from '../../cattyAgent/systemPrompt';
|
||||
import {
|
||||
isWebSearchReady,
|
||||
normalizeCommandTimeoutSeconds,
|
||||
normalizeResponseIdleTimeoutSeconds,
|
||||
resolveOpenAIApi,
|
||||
resolveProviderStyle,
|
||||
} from '../../types';
|
||||
import {
|
||||
applyResponsesApiStatelessStoreOption,
|
||||
buildCattyReasoningProviderOptions,
|
||||
estimateReasoningOutputReserve,
|
||||
} from '../../cattyReasoning';
|
||||
import { createModelFromConfig } from '../../sdk/providers';
|
||||
import { createCattyToolsFromCatalog } from '../capabilityTools';
|
||||
import { createInitialCattyRuntimeContext } from '../cattyRuntimeContext';
|
||||
import { prepareStepContext, extractLatestUserGoal } from '../contextManager';
|
||||
import {
|
||||
compactCattyMessages,
|
||||
prepareCattyMessagesForStream,
|
||||
} from '../cattyRuntime';
|
||||
import { computeTotalInputTokens, DEFAULT_MAX_OUTPUT_TOKENS } from '../contextBudget';
|
||||
import { clearChatSessionCancelled } from '../agentStop';
|
||||
import { isRequestTooLargeError } from '../../errorClassifier';
|
||||
import { getNetcattyBridge, generateId, resolveUserSkillsContext } from '../../aiChatStreamingSupport';
|
||||
import {
|
||||
buildCattySdkMessages,
|
||||
collectOpenAIChatAssistantFieldsForMessages,
|
||||
collectPreservedTerminalWriteFingerprints,
|
||||
collectToolResultsAfterMessage,
|
||||
createContinuationContext,
|
||||
} from './cattyMessageBuilder';
|
||||
import { hadToolProgressBeforeRequestTooLarge, processCattyStream } from './cattyStreamProcessor';
|
||||
import type { CattyTurnInput, TurnDriver, TurnDriverContext } from './types';
|
||||
import { fitLargeUserInputForModel } from '../largeUserInput';
|
||||
import { buildPromptContextSnapshot } from '../promptContextSnapshot';
|
||||
|
||||
export class CattyTurnDriver implements TurnDriver {
|
||||
readonly backend = 'catty' as const;
|
||||
|
||||
async run(input: import('./types').TurnInput, ctx: TurnDriverContext): Promise<void> {
|
||||
if (input.backend !== 'catty') {
|
||||
throw new Error('CattyTurnDriver received non-catty input');
|
||||
}
|
||||
await runCattyTurn(input, ctx);
|
||||
}
|
||||
|
||||
abort(): void {
|
||||
// Abort is handled via AbortSignal on the turn input.
|
||||
}
|
||||
}
|
||||
|
||||
async function runCattyTurn(input: CattyTurnInput, ctx: TurnDriverContext): Promise<void> {
|
||||
const {
|
||||
chatSessionId: sessionId,
|
||||
userText: trimmed,
|
||||
signal,
|
||||
currentSession,
|
||||
assistantMsgId,
|
||||
context,
|
||||
attachments,
|
||||
maxIterations,
|
||||
bridge,
|
||||
ui,
|
||||
} = input;
|
||||
|
||||
const netcattyBridge = (bridge ?? getNetcattyBridge()) as NonNullable<ReturnType<typeof getNetcattyBridge>>;
|
||||
const toolOutputTempBridge = netcattyBridge as typeof netcattyBridge & {
|
||||
getToolOutputPersistenceStatus?: () => Promise<{ durable: boolean; reason?: string }>;
|
||||
writeToolOutputTemp?: (
|
||||
record: import('../toolOutputStore').PersistedToolOutputRecord,
|
||||
content: string,
|
||||
) => Promise<{ ok: boolean; path?: string; error?: string }>;
|
||||
restoreToolOutputTemp?: (
|
||||
handleId: string,
|
||||
chatSessionId: string,
|
||||
) => Promise<{ path: string; record: import('../toolOutputStore').PersistedToolOutputRecord } | null>;
|
||||
readToolOutputTemp?: (
|
||||
path: string,
|
||||
request: import('../toolOutputStore').ReadToolOutputInput,
|
||||
) => Promise<Omit<import('../toolOutputStore').ToolOutputReadResult, 'handleId' | 'storedChars' | 'sourceTruncated'> | null>;
|
||||
deleteToolOutputTemp?: (path: string) => Promise<{ ok: boolean }>;
|
||||
deleteChatToolOutputsTemp?: (chatSessionId: string) => Promise<{ deletedCount: number }>;
|
||||
deleteTerminalToolOutputsTemp?: (
|
||||
chatSessionId: string,
|
||||
terminalSessionId: string,
|
||||
) => Promise<{ deletedCount: number }>;
|
||||
};
|
||||
const persistenceStatus = await toolOutputTempBridge.getToolOutputPersistenceStatus?.()
|
||||
.catch(() => ({ durable: false }));
|
||||
if (
|
||||
toolOutputTempBridge.writeToolOutputTemp
|
||||
&& toolOutputTempBridge.readToolOutputTemp
|
||||
&& toolOutputTempBridge.deleteToolOutputTemp
|
||||
) {
|
||||
ctx.toolOutputStore.setPersistence?.({
|
||||
write: async (record, content) => {
|
||||
if (!persistenceStatus?.durable) {
|
||||
throw new Error(persistenceStatus?.reason || 'Secure local storage is unavailable.');
|
||||
}
|
||||
const result = await toolOutputTempBridge.writeToolOutputTemp!(record, content);
|
||||
if (!result.ok || !result.path) {
|
||||
throw new Error(result.error || 'Unable to persist tool output.');
|
||||
}
|
||||
return result.path;
|
||||
},
|
||||
restore: persistenceStatus?.durable && toolOutputTempBridge.restoreToolOutputTemp
|
||||
? (handleId, chatSessionId) => toolOutputTempBridge.restoreToolOutputTemp!(handleId, chatSessionId)
|
||||
: undefined,
|
||||
read: (path, request) => toolOutputTempBridge.readToolOutputTemp!(path, request),
|
||||
delete: async path => {
|
||||
await toolOutputTempBridge.deleteToolOutputTemp!(path);
|
||||
},
|
||||
deleteSession: toolOutputTempBridge.deleteChatToolOutputsTemp
|
||||
? async chatSessionId => {
|
||||
await toolOutputTempBridge.deleteChatToolOutputsTemp!(chatSessionId);
|
||||
}
|
||||
: undefined,
|
||||
deleteTerminalSession: toolOutputTempBridge.deleteTerminalToolOutputsTemp
|
||||
? async (chatSessionId, terminalSessionId) => {
|
||||
await toolOutputTempBridge.deleteTerminalToolOutputsTemp!(chatSessionId, terminalSessionId);
|
||||
}
|
||||
: undefined,
|
||||
deleteTerminalEverywhere: toolOutputTempBridge.deleteTerminalToolOutputsEverywhereTemp
|
||||
? async terminalSessionId => {
|
||||
await toolOutputTempBridge.deleteTerminalToolOutputsEverywhereTemp!(terminalSessionId);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
} else {
|
||||
ctx.toolOutputStore.setPersistence?.(undefined);
|
||||
}
|
||||
await clearChatSessionCancelled(sessionId, netcattyBridge);
|
||||
if (netcattyBridge.aiMcpUpdateSessions) {
|
||||
await netcattyBridge.aiMcpUpdateSessions(context.terminalSessions, sessionId);
|
||||
}
|
||||
if (attachments?.length && netcattyBridge.aiMcpUpdateAttachments) {
|
||||
await netcattyBridge.aiMcpUpdateAttachments(attachments, sessionId);
|
||||
}
|
||||
const userSkillsContext = await resolveUserSkillsContext(
|
||||
netcattyBridge,
|
||||
trimmed,
|
||||
context.selectedUserSkillSlugs,
|
||||
);
|
||||
const modelUserText = fitLargeUserInputForModel(trimmed, sessionId, ctx.toolOutputStore);
|
||||
const getExecutorContext = context.getExecutorContext ?? (() => ({
|
||||
sessions: context.terminalSessions,
|
||||
workspaceId: context.scopeType === 'workspace' ? context.scopeTargetId : undefined,
|
||||
workspaceName: context.scopeType === 'workspace' ? context.scopeLabel : undefined,
|
||||
}));
|
||||
const toolsBundle = createCattyToolsFromCatalog(
|
||||
netcattyBridge,
|
||||
getExecutorContext,
|
||||
context.commandBlocklist,
|
||||
context.globalPermissionMode,
|
||||
context.webSearchConfig ?? undefined,
|
||||
sessionId,
|
||||
ctx.toolOutputStore,
|
||||
ctx.toolResultDedup,
|
||||
);
|
||||
const { tools } = toolsBundle;
|
||||
|
||||
const systemPrompt = buildSystemPrompt({
|
||||
scopeType: context.scopeType,
|
||||
scopeLabel: context.scopeLabel,
|
||||
hosts: context.terminalSessions,
|
||||
permissionMode: context.globalPermissionMode,
|
||||
webSearchEnabled: isWebSearchReady(context.webSearchConfig),
|
||||
userSkillsContext,
|
||||
});
|
||||
|
||||
if (!context.activeProvider) {
|
||||
ui.reportStreamError(sessionId, signal, 'No AI provider configured. Please configure a provider in Settings → AI.');
|
||||
return;
|
||||
}
|
||||
|
||||
const activeModelId = context.activeModelId || context.activeProvider.defaultModel || '';
|
||||
const promptContext = buildPromptContextSnapshot({
|
||||
providerId: context.activeProvider.providerId,
|
||||
modelId: activeModelId,
|
||||
permissionMode: context.permissionMode ?? context.globalPermissionMode,
|
||||
scopeType: context.scopeType,
|
||||
scopeLabel: context.scopeLabel,
|
||||
toolNames: Object.keys(tools),
|
||||
selectedSkillSlugs: context.selectedUserSkillSlugs,
|
||||
systemPrompt,
|
||||
webSearchEnabled: isWebSearchReady(context.webSearchConfig),
|
||||
hostSessionIds: context.terminalSessions.map(session => session.sessionId),
|
||||
});
|
||||
ctx.emit({
|
||||
id: `context-snapshot-${ctx.turnId}`,
|
||||
type: 'context_snapshot',
|
||||
snapshot: promptContext,
|
||||
} as import('../types').AgentEvent);
|
||||
const continuationContext = createContinuationContext(
|
||||
context.activeProvider.id,
|
||||
context.activeProvider.providerId,
|
||||
activeModelId,
|
||||
resolveProviderStyle(context.activeProvider) === 'openai'
|
||||
&& resolveOpenAIApi(context.activeProvider) === 'responses',
|
||||
);
|
||||
|
||||
ui.setStreamingForScope(sessionId, true);
|
||||
|
||||
try {
|
||||
const openAIChatAssistantFieldsByMessage = new Map<ModelMessage, OpenAIChatAssistantFields | undefined>();
|
||||
|
||||
const buildSdkMessages = (
|
||||
allMessages: import('../../types').ChatMessage[],
|
||||
includeCurrentUserMessage: boolean,
|
||||
options: { preserveTerminalToolResults?: ReadonlySet<import('../../types').ToolResult> } = {},
|
||||
sessionContextCompaction = currentSession?.contextCompaction,
|
||||
) => buildCattySdkMessages({
|
||||
allMessages,
|
||||
contextCompaction: sessionContextCompaction,
|
||||
includeCurrentUserMessage,
|
||||
trimmed: modelUserText,
|
||||
attachments: includeCurrentUserMessage ? attachments : undefined,
|
||||
continuationContext,
|
||||
preserveTerminalToolResults: options.preserveTerminalToolResults,
|
||||
chatSessionId: sessionId,
|
||||
toolOutputStore: ctx.toolOutputStore,
|
||||
fieldsByMessage: openAIChatAssistantFieldsByMessage,
|
||||
});
|
||||
|
||||
const responseIdleTimeoutSeconds = normalizeResponseIdleTimeoutSeconds(
|
||||
context.responseIdleTimeout ?? Number.NaN,
|
||||
);
|
||||
const responseIdleTimeoutMs = responseIdleTimeoutSeconds * 1000;
|
||||
|
||||
let model;
|
||||
try {
|
||||
model = createModelFromConfig(
|
||||
{
|
||||
...context.activeProvider,
|
||||
defaultModel: activeModelId,
|
||||
},
|
||||
{
|
||||
getOpenAIChatAssistantFields: () => continuationContext.openAIChatAssistantFields,
|
||||
streamIdleTimeoutMs: responseIdleTimeoutMs,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('[Catty] Model creation failed:', e);
|
||||
ui.reportStreamError(sessionId, signal, `Model creation failed: ${e instanceof Error ? e.message : String(e)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contextWindow = resolveContextWindow({
|
||||
provider: context.activeProvider,
|
||||
modelId: activeModelId,
|
||||
defaultContextWindow: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
||||
});
|
||||
const maxOutputTokens = context.activeProvider.advancedParams?.maxTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
||||
const reasoningProviderOptions = applyResponsesApiStatelessStoreOption(
|
||||
context.activeProvider,
|
||||
buildCattyReasoningProviderOptions(
|
||||
context.activeProvider,
|
||||
context.reasoningEffort,
|
||||
activeModelId,
|
||||
),
|
||||
);
|
||||
const preserveStatelessResponsesReasoning = reasoningProviderOptions?.openai?.store === false;
|
||||
const reasoningReserveTokens = estimateReasoningOutputReserve(reasoningProviderOptions);
|
||||
// Fold thinking budget into compaction maxOutput only. reservedTokens is
|
||||
// added to estimated input separately, so adding the budget there too
|
||||
// would count the same 10k/20k twice.
|
||||
const compactionMaxOutputTokens = maxOutputTokens + reasoningReserveTokens;
|
||||
const providerId = context.activeProvider.providerId;
|
||||
const outputReserveTokens = Math.min(maxOutputTokens, Math.ceil(contextWindow * 0.05));
|
||||
const getRequestReserveTokens = () => outputReserveTokens + estimateUnknownTokens({
|
||||
systemPrompt,
|
||||
toolNames: Object.keys(tools),
|
||||
openAIChatAssistantFields: Array.from(openAIChatAssistantFieldsByMessage.values()),
|
||||
}, providerId);
|
||||
|
||||
const prepareMessagesForStream = (messages: ModelMessage[]): ModelMessage[] => {
|
||||
const pruned = prepareCattyMessagesForStream(messages, {
|
||||
preserveReasoning: preserveStatelessResponsesReasoning,
|
||||
});
|
||||
continuationContext.openAIChatAssistantFields = collectOpenAIChatAssistantFieldsForMessages(
|
||||
pruned,
|
||||
openAIChatAssistantFieldsByMessage,
|
||||
);
|
||||
return pruned;
|
||||
};
|
||||
|
||||
const compactMessages = async (
|
||||
messages: ModelMessage[],
|
||||
options: {
|
||||
force?: boolean;
|
||||
compressForRequestTooLargeRetry?: boolean;
|
||||
protectRecentMessages?: number;
|
||||
},
|
||||
) => {
|
||||
const pendingHandles = ctx.toolOutputStore.listPendingHandles(sessionId);
|
||||
const sessionStateText = ctx.sessionStateStore.toReinjectionText(sessionId);
|
||||
const result = await compactCattyMessages({
|
||||
messages,
|
||||
sessionId,
|
||||
chatSessionId: sessionId,
|
||||
provider: context.activeProvider,
|
||||
modelId: activeModelId || context.activeProvider?.defaultModel,
|
||||
reservedTokens: getRequestReserveTokens,
|
||||
maxOutputTokens: compactionMaxOutputTokens,
|
||||
model,
|
||||
toolOutputStore: ctx.toolOutputStore,
|
||||
abortSignal: signal,
|
||||
trigger: options.force ? 'force' : options.compressForRequestTooLargeRetry ? '413-retry' : 'pre-turn',
|
||||
force: options.force,
|
||||
compressForRequestTooLargeRetry: options.compressForRequestTooLargeRetry,
|
||||
protectRecentMessages: options.protectRecentMessages,
|
||||
onCompactionStart: (trigger) => {
|
||||
ctx.emit({
|
||||
id: `compaction-start-${Date.now()}`,
|
||||
type: 'compaction_start',
|
||||
sessionId,
|
||||
chatSessionId: sessionId,
|
||||
backend: 'catty',
|
||||
timestamp: Date.now(),
|
||||
trigger,
|
||||
} as import('../types').AgentEvent);
|
||||
},
|
||||
onCompaction: (trace) => {
|
||||
ctx.emit({
|
||||
id: `compaction-${Date.now()}`,
|
||||
type: 'compaction',
|
||||
trace,
|
||||
} as import('../types').AgentEvent);
|
||||
if (options.compressForRequestTooLargeRetry && trace.did413Fallback) {
|
||||
console.warn('[Catty] Request content compressed after forced context compaction.');
|
||||
}
|
||||
},
|
||||
reinjection: {
|
||||
permissionMode: context.permissionMode ?? context.globalPermissionMode,
|
||||
sessionStateText,
|
||||
sessionScopeSummary: pendingHandles.length
|
||||
? `Pending tool output handles: ${pendingHandles.map(h => h.id).join(', ')}`
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const emitContextSnapshot = (messages: ModelMessage[]) => {
|
||||
ctx.emit({
|
||||
id: `context-snapshot-${Date.now()}-${ctx.turnId}`,
|
||||
type: 'context_snapshot',
|
||||
snapshot: {
|
||||
...promptContext,
|
||||
contextWindow,
|
||||
estimatedInputTokens: computeTotalInputTokens({
|
||||
messages,
|
||||
providerId,
|
||||
systemPrompt,
|
||||
toolNames: Object.keys(tools),
|
||||
}),
|
||||
},
|
||||
} as import('../types').AgentEvent);
|
||||
};
|
||||
|
||||
if (context.forceCompaction) {
|
||||
// Persist a tool-safe UI-message boundary and summarize only that head
|
||||
// slice so the durable compact coordinate system matches buildCattySdkMessages.
|
||||
const uiMessages = currentSession?.messages ?? [];
|
||||
const compactedMessageCount = findSafeChatMessageCompactionSplitIndex(
|
||||
uiMessages,
|
||||
DEFAULT_PROTECT_RECENT_MESSAGES,
|
||||
);
|
||||
if (compactedMessageCount <= 0) {
|
||||
emitContextSnapshot(prepareMessagesForStream(buildSdkMessages(uiMessages, false)));
|
||||
return;
|
||||
}
|
||||
|
||||
const headSdkMessages = buildSdkMessages(
|
||||
uiMessages.slice(0, compactedMessageCount),
|
||||
false,
|
||||
{},
|
||||
undefined,
|
||||
);
|
||||
const compacted = await compactMessages(headSdkMessages, {
|
||||
force: true,
|
||||
// Summarize the entire head; the protected tail lives in UI messages.
|
||||
protectRecentMessages: 0,
|
||||
});
|
||||
if (compacted.summary) {
|
||||
const nextCompaction = {
|
||||
summary: compacted.summary,
|
||||
compactedMessageCount,
|
||||
};
|
||||
ui.persistContextCompaction?.(sessionId, nextCompaction);
|
||||
emitContextSnapshot(prepareMessagesForStream(
|
||||
buildSdkMessages(uiMessages, false, {}, nextCompaction),
|
||||
));
|
||||
} else {
|
||||
// Non-durable outcome: keep the meter honest (full current context).
|
||||
emitContextSnapshot(prepareMessagesForStream(buildSdkMessages(uiMessages, false)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let messagesForStream = buildSdkMessages(currentSession?.messages ?? [], true);
|
||||
messagesForStream = (await compactMessages(messagesForStream, {})).messages;
|
||||
messagesForStream = prepareMessagesForStream(messagesForStream);
|
||||
emitContextSnapshot(messagesForStream);
|
||||
|
||||
const runtimeContext = createInitialCattyRuntimeContext({
|
||||
chatSessionId: sessionId,
|
||||
turnId: ctx.turnId,
|
||||
providerId: context.activeProvider?.providerId,
|
||||
modelId: activeModelId,
|
||||
permissionMode: context.permissionMode ?? context.globalPermissionMode,
|
||||
scopeType: context.scopeType,
|
||||
scopeLabel: context.scopeLabel,
|
||||
userGoal: extractLatestUserGoal(messagesForStream),
|
||||
promptContext,
|
||||
});
|
||||
const commandTimeoutSeconds =
|
||||
Number.isFinite(context.commandTimeout) && context.commandTimeout > 0
|
||||
? normalizeCommandTimeoutSeconds(context.commandTimeout)
|
||||
: undefined;
|
||||
const commandTimeoutMs =
|
||||
commandTimeoutSeconds != null
|
||||
? commandTimeoutSeconds * 1000
|
||||
: undefined;
|
||||
|
||||
const runStream = async (streamMessages: ModelMessage[], streamAssistantMsgId: string) => {
|
||||
await processCattyStream({
|
||||
streamSessionId: sessionId,
|
||||
model,
|
||||
systemPrompt,
|
||||
toolsBundle,
|
||||
sdkMessages: streamMessages,
|
||||
signal,
|
||||
currentAssistantMsgId: streamAssistantMsgId,
|
||||
maxIterations,
|
||||
advancedParams: context.activeProvider?.advancedParams,
|
||||
reasoningProviderOptions,
|
||||
continuationContext,
|
||||
turnId: ctx.turnId,
|
||||
commandTimeoutMs,
|
||||
responseIdleTimeoutMs,
|
||||
runtimeContext,
|
||||
onAgentEvent: (event) => ctx.emit(event),
|
||||
prepareStep: async ({ stepNumber, messages, runtimeContext: stepRuntimeContext }) => {
|
||||
const prepared = await prepareStepContext({
|
||||
messages,
|
||||
stepNumber,
|
||||
sessionId,
|
||||
chatSessionId: sessionId,
|
||||
providerId: context.activeProvider?.providerId,
|
||||
modelId: activeModelId,
|
||||
contextWindow,
|
||||
reservedTokens: getRequestReserveTokens(),
|
||||
maxOutputTokens: compactionMaxOutputTokens,
|
||||
toolOutputStore: ctx.toolOutputStore,
|
||||
runtimeContext: stepRuntimeContext,
|
||||
onEvent: (event) => ctx.emit(event),
|
||||
});
|
||||
emitContextSnapshot(prepared.messages);
|
||||
return {
|
||||
messages: prepared.messages,
|
||||
runtimeContext: prepared.runtimeContext,
|
||||
};
|
||||
},
|
||||
ui: {
|
||||
addMessageToSession: ui.addMessageToSession,
|
||||
updateMessageById: ui.updateMessageById,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await runStream(messagesForStream, assistantMsgId);
|
||||
} catch (streamErr) {
|
||||
if (signal.aborted || !isRequestTooLargeError(streamErr)) {
|
||||
throw streamErr;
|
||||
}
|
||||
|
||||
console.warn('[Catty] Request hit HTTP 413; forcing context compaction and retrying once.', streamErr);
|
||||
const hadToolProgress = hadToolProgressBeforeRequestTooLarge(streamErr);
|
||||
let retryBaseMessages = messagesForStream;
|
||||
let retryAssistantMsgId = assistantMsgId;
|
||||
let preservedWriteFingerprints: string[] = [];
|
||||
if (hadToolProgress) {
|
||||
const latestSession = ui.getLatestSession?.(sessionId);
|
||||
if (latestSession) {
|
||||
preservedWriteFingerprints = collectPreservedTerminalWriteFingerprints(
|
||||
latestSession.messages,
|
||||
assistantMsgId,
|
||||
sessionId,
|
||||
);
|
||||
retryBaseMessages = buildSdkMessages(latestSession.messages, false, {
|
||||
preserveTerminalToolResults: collectToolResultsAfterMessage(
|
||||
latestSession.messages,
|
||||
assistantMsgId,
|
||||
),
|
||||
}, latestSession.contextCompaction);
|
||||
}
|
||||
retryAssistantMsgId = generateId();
|
||||
ui.addMessageToSession(sessionId, {
|
||||
id: retryAssistantMsgId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
model: activeModelId || context.activeProvider?.defaultModel || '',
|
||||
providerId: context.activeProvider?.providerId,
|
||||
});
|
||||
} else {
|
||||
ui.updateMessageById(sessionId, assistantMsgId, msg => ({
|
||||
...msg,
|
||||
content: '',
|
||||
thinking: undefined,
|
||||
thinkingDurationMs: undefined,
|
||||
providerContinuation: undefined,
|
||||
toolCalls: undefined,
|
||||
errorInfo: undefined,
|
||||
executionStatus: undefined,
|
||||
pendingApproval: undefined,
|
||||
}));
|
||||
}
|
||||
ctx.toolResultDedup.enableWriteReplay(preservedWriteFingerprints);
|
||||
const retryMessages = prepareMessagesForStream((await compactMessages(retryBaseMessages, {
|
||||
force: true,
|
||||
compressForRequestTooLargeRetry: true,
|
||||
})).messages);
|
||||
emitContextSnapshot(retryMessages);
|
||||
await runStream(retryMessages, retryAssistantMsgId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Catty] streamText error:', err);
|
||||
ui.reportStreamError(sessionId, signal, err);
|
||||
} finally {
|
||||
ui.updateLastMessage(sessionId, msg => msg.statusText ? { ...msg, statusText: '' } : msg);
|
||||
ui.setStreamingForScope(sessionId, false);
|
||||
context.autoTitleSession(sessionId, context.titleText ?? trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
export const cattyTurnDriver = new CattyTurnDriver();
|
||||
Reference in New Issue
Block a user