[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:
@@ -0,0 +1,575 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { ToolOutputStore } from '../toolOutputStore';
|
||||
import {
|
||||
buildCattySdkMessages,
|
||||
createContinuationContext,
|
||||
} from './cattyMessageBuilder';
|
||||
import type { ChatMessage } from '../../types';
|
||||
import { prepareCattyMessagesForStream } from '../cattyRuntime';
|
||||
|
||||
function buildHistory(messages: ChatMessage[]) {
|
||||
return buildCattySdkMessages({
|
||||
allMessages: messages,
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext('provider-1', 'openai', 'model-1', true),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
test('legacy reasoning parts with an rs_ item id but no encrypted content are dropped from replay', () => {
|
||||
const messages: ChatMessage[] = [{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Done.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [
|
||||
{
|
||||
text: 'legacy reasoning',
|
||||
providerOptions: { openai: { itemId: 'rs_legacy' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
}];
|
||||
|
||||
const sdkMessages = buildHistory(messages);
|
||||
|
||||
assert.equal(sdkMessages.length, 1);
|
||||
// With the legacy reasoning part dropped, only text remains, so the
|
||||
// assistant content collapses to a plain string.
|
||||
assert.equal(sdkMessages[0].content, 'Done.');
|
||||
});
|
||||
|
||||
test('reasoning parts with encrypted content and non-OpenAI reasoning parts survive replay', () => {
|
||||
const messages: ChatMessage[] = [{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Done.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [
|
||||
{
|
||||
text: 'replayable reasoning',
|
||||
providerOptions: { openai: { itemId: 'rs_new', reasoningEncryptedContent: 'enc-abc' } },
|
||||
},
|
||||
{
|
||||
text: 'plain reasoning',
|
||||
},
|
||||
],
|
||||
},
|
||||
}];
|
||||
|
||||
const sdkMessages = buildHistory(messages);
|
||||
|
||||
assert.equal(sdkMessages.length, 1);
|
||||
const content = sdkMessages[0].content;
|
||||
assert.ok(Array.isArray(content));
|
||||
assert.deepEqual(
|
||||
content.map((part) => (part as { type: string; text?: string }).text),
|
||||
['replayable reasoning', 'plain reasoning', 'Done.'],
|
||||
);
|
||||
});
|
||||
|
||||
test('an unreplayable reasoning item discards the whole tool-call exchange from replay', () => {
|
||||
const toolCall = { id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } };
|
||||
const toolResult = {
|
||||
toolCallId: 'call-1',
|
||||
content: 'output',
|
||||
};
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [
|
||||
{
|
||||
text: 'legacy reasoning',
|
||||
providerOptions: { openai: { itemId: 'rs_legacy' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
toolCalls: [toolCall],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 3,
|
||||
toolResults: [toolResult],
|
||||
},
|
||||
{
|
||||
id: 'assistant-2',
|
||||
role: 'assistant',
|
||||
content: 'All done.',
|
||||
timestamp: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const sdkMessages = buildHistory(messages);
|
||||
|
||||
// The tool-call/tool-result exchange is discarded entirely; only the plain
|
||||
// assistant text messages survive replay.
|
||||
assert.equal(sdkMessages.length, 2);
|
||||
assert.equal(sdkMessages[0].content, 'Running it.');
|
||||
assert.equal(sdkMessages[1].content, 'All done.');
|
||||
|
||||
const chatMessages = buildCattySdkMessages({
|
||||
allMessages: messages,
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext('provider-1', 'openai', 'model-1', false),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
assert.equal(chatMessages.length, 3);
|
||||
assert.equal(chatMessages[1].role, 'tool');
|
||||
});
|
||||
|
||||
test('a Responses model switch discards a reasoning-backed tool exchange from the old source', () => {
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [{
|
||||
text: 'replayable only for the original model',
|
||||
providerOptions: {
|
||||
openai: { itemId: 'rs_old_model', reasoningEncryptedContent: 'enc-old-model' },
|
||||
},
|
||||
}],
|
||||
},
|
||||
toolCalls: [{ id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } }],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 2,
|
||||
toolResults: [{ toolCallId: 'call-1', content: 'output' }],
|
||||
},
|
||||
];
|
||||
const buildForModel = (usesOpenAIResponses: boolean) => buildCattySdkMessages({
|
||||
allMessages: messages,
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext(
|
||||
'provider-1',
|
||||
'openai',
|
||||
'model-2',
|
||||
usesOpenAIResponses,
|
||||
),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
|
||||
const responsesMessages = buildForModel(true);
|
||||
assert.deepEqual(responsesMessages, [{ role: 'assistant', content: 'Running it.' }]);
|
||||
|
||||
// Chat Completions does not require a Responses reasoning item alongside
|
||||
// the generic call/result pair, so keep the existing cross-model behavior.
|
||||
const chatMessages = buildForModel(false);
|
||||
assert.equal(chatMessages.length, 2);
|
||||
assert.equal(chatMessages[1].role, 'tool');
|
||||
});
|
||||
|
||||
test('a Responses provider switch keeps a non-OpenAI reasoning tool exchange', () => {
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'anthropic-1', providerType: 'anthropic', modelId: 'claude' },
|
||||
reasoningParts: [{
|
||||
text: 'prior Anthropic thinking',
|
||||
providerOptions: { anthropic: { signature: 'sig-1' } },
|
||||
}],
|
||||
},
|
||||
toolCalls: [{ id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } }],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 2,
|
||||
toolResults: [{ toolCallId: 'call-1', content: 'output' }],
|
||||
},
|
||||
];
|
||||
|
||||
const sdkMessages = buildCattySdkMessages({
|
||||
allMessages: messages,
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext('openai-1', 'openai', 'gpt-5', true),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
|
||||
assert.equal(sdkMessages.length, 2);
|
||||
const assistantContent = sdkMessages[0].content;
|
||||
assert.ok(Array.isArray(assistantContent));
|
||||
assert.deepEqual(
|
||||
assistantContent.map(part => part.type),
|
||||
['text', 'tool-call'],
|
||||
);
|
||||
assert.equal(sdkMessages[1].role, 'tool');
|
||||
});
|
||||
|
||||
test('metadata-free Responses reasoning discards its paired tool exchange', () => {
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [{ text: 'relay reasoning without replay metadata' }],
|
||||
},
|
||||
toolCalls: [{ id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } }],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 2,
|
||||
toolResults: [{ toolCallId: 'call-1', content: 'output' }],
|
||||
},
|
||||
];
|
||||
|
||||
const responsesMessages = buildHistory(messages);
|
||||
assert.deepEqual(responsesMessages, [{ role: 'assistant', content: 'Running it.' }]);
|
||||
|
||||
const responsesAfterModelSwitch = buildCattySdkMessages({
|
||||
allMessages: messages,
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext('provider-1', 'openai', 'model-2', true),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
assert.deepEqual(
|
||||
responsesAfterModelSwitch,
|
||||
[{ role: 'assistant', content: 'Running it.' }],
|
||||
);
|
||||
|
||||
const chatMessages = buildCattySdkMessages({
|
||||
allMessages: messages,
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext('provider-1', 'openai', 'model-1', false),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
assert.equal(chatMessages.length, 2);
|
||||
assert.equal(chatMessages[1].role, 'tool');
|
||||
});
|
||||
|
||||
test('same-provider Chat to Responses switches keep the generic tool exchange', () => {
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [{ text: 'OpenAI Chat reasoning' }],
|
||||
openAIChatAssistantFields: { reasoning_content: 'OpenAI Chat reasoning' },
|
||||
},
|
||||
toolCalls: [{ id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } }],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 2,
|
||||
toolResults: [{ toolCallId: 'call-1', content: 'output' }],
|
||||
},
|
||||
];
|
||||
|
||||
for (const modelId of ['model-1', 'model-2']) {
|
||||
const responsesMessages = buildCattySdkMessages({
|
||||
allMessages: messages,
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext('provider-1', 'openai', modelId, true),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
|
||||
assert.equal(responsesMessages.length, 2);
|
||||
const assistantContent = responsesMessages[0].content;
|
||||
assert.ok(Array.isArray(assistantContent));
|
||||
assert.deepEqual(
|
||||
assistantContent.map(part => part.type),
|
||||
['text', 'tool-call'],
|
||||
);
|
||||
assert.equal(responsesMessages[1].role, 'tool');
|
||||
}
|
||||
});
|
||||
|
||||
test('a durable summary survives when storage trimming shifts its boundary to zero', () => {
|
||||
const sdkMessages = buildCattySdkMessages({
|
||||
allMessages: [{
|
||||
id: 'recent-user',
|
||||
role: 'user',
|
||||
content: 'What should I do next?',
|
||||
timestamp: 1,
|
||||
}],
|
||||
contextCompaction: {
|
||||
summary: 'Earlier work completed the deployment.',
|
||||
compactedMessageCount: 0,
|
||||
},
|
||||
includeCurrentUserMessage: false,
|
||||
trimmed: '',
|
||||
continuationContext: createContinuationContext('provider-1', 'openai', 'model-1', true),
|
||||
chatSessionId: 'chat-1',
|
||||
toolOutputStore: new ToolOutputStore(),
|
||||
fieldsByMessage: new Map(),
|
||||
});
|
||||
|
||||
assert.equal(sdkMessages.length, 3);
|
||||
assert.match(String(sdkMessages[0].content), /Earlier work completed the deployment/);
|
||||
assert.equal(sdkMessages[2].content, 'What should I do next?');
|
||||
});
|
||||
|
||||
test('tool exchanges with replayable reasoning are kept intact', () => {
|
||||
const toolCall = { id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } };
|
||||
const toolResult = {
|
||||
toolCallId: 'call-1',
|
||||
content: 'output',
|
||||
};
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [
|
||||
{
|
||||
text: 'replayable reasoning',
|
||||
providerOptions: { openai: { itemId: 'rs_new', reasoningEncryptedContent: 'enc-abc' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
toolCalls: [toolCall],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 3,
|
||||
toolResults: [toolResult],
|
||||
},
|
||||
];
|
||||
|
||||
const sdkMessages = buildHistory(messages);
|
||||
|
||||
assert.equal(sdkMessages.length, 2);
|
||||
const assistantContent = sdkMessages[0].content;
|
||||
assert.ok(Array.isArray(assistantContent));
|
||||
assert.deepEqual(
|
||||
assistantContent.map((part) => (part as { type: string }).type),
|
||||
['reasoning', 'text', 'tool-call'],
|
||||
);
|
||||
assert.equal(sdkMessages[1].role, 'tool');
|
||||
});
|
||||
|
||||
test('final Responses preparation preserves encrypted reasoning for a tool exchange', () => {
|
||||
const toolCall = { id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } };
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [{
|
||||
text: 'replayable reasoning',
|
||||
providerOptions: {
|
||||
openai: { itemId: 'rs_new', reasoningEncryptedContent: 'enc-abc' },
|
||||
},
|
||||
}],
|
||||
},
|
||||
toolCalls: [toolCall],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 2,
|
||||
toolResults: [{ toolCallId: 'call-1', content: 'output' }],
|
||||
},
|
||||
];
|
||||
|
||||
const built = buildHistory(messages);
|
||||
const prepared = prepareCattyMessagesForStream(built, { preserveReasoning: true });
|
||||
const assistantContent = prepared[0].content;
|
||||
assert.ok(Array.isArray(assistantContent));
|
||||
const reasoning = assistantContent.find(part => part.type === 'reasoning');
|
||||
assert.deepEqual(reasoning?.providerOptions?.openai, {
|
||||
itemId: 'rs_new',
|
||||
reasoningEncryptedContent: 'enc-abc',
|
||||
});
|
||||
|
||||
const defaultPrepared = prepareCattyMessagesForStream(built);
|
||||
const defaultAssistantContent = defaultPrepared[0].content;
|
||||
assert.ok(Array.isArray(defaultAssistantContent));
|
||||
assert.equal(defaultAssistantContent.some(part => part.type === 'reasoning'), false);
|
||||
});
|
||||
|
||||
test('freshly streamed reasoning fragments with a null start payload keep the tool exchange', () => {
|
||||
// Mirrors a live `@ai-sdk/openai` Responses stream (store: false): the
|
||||
// reasoning-start fragment carries only the item id with
|
||||
// `reasoningEncryptedContent: null`, deltas omit the key, and the ciphertext
|
||||
// arrives on the reasoning-end fragment. The merge keeps both fragments for
|
||||
// the same item, which must still replay.
|
||||
const toolCall = { id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } };
|
||||
const toolResult = {
|
||||
toolCallId: 'call-1',
|
||||
content: 'output',
|
||||
};
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [
|
||||
{
|
||||
text: '',
|
||||
providerOptions: { openai: { itemId: 'rs_new', reasoningEncryptedContent: null } },
|
||||
},
|
||||
{
|
||||
text: 'replayable reasoning',
|
||||
providerOptions: { openai: { itemId: 'rs_new', reasoningEncryptedContent: 'enc-abc' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
toolCalls: [toolCall],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 3,
|
||||
toolResults: [toolResult],
|
||||
},
|
||||
];
|
||||
|
||||
const sdkMessages = buildHistory(messages);
|
||||
|
||||
assert.equal(sdkMessages.length, 2);
|
||||
const assistantContent = sdkMessages[0].content;
|
||||
assert.ok(Array.isArray(assistantContent));
|
||||
// The empty ID-only start fragment is dropped from the replayed content;
|
||||
// the encrypted fragment for the same item carries the ciphertext.
|
||||
assert.deepEqual(
|
||||
assistantContent.map((part) => (part as { type: string }).type),
|
||||
['reasoning', 'text', 'tool-call'],
|
||||
);
|
||||
assert.equal(sdkMessages[1].role, 'tool');
|
||||
});
|
||||
|
||||
test('every text fragment of a multi-fragment replayable reasoning item is kept', () => {
|
||||
// A Responses reasoning item with several summary parts gives every
|
||||
// fragment the same item id; the ciphertext arrives only on the final
|
||||
// `output_item.done` fragment. Replayability is per item id, so all text
|
||||
// fragments of the item must survive the replay filter.
|
||||
const messages: ChatMessage[] = [{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Done.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [
|
||||
{
|
||||
text: 'first summary',
|
||||
providerOptions: { openai: { itemId: 'rs_new', reasoningEncryptedContent: null } },
|
||||
},
|
||||
{
|
||||
text: 'second summary',
|
||||
providerOptions: { openai: { itemId: 'rs_new', reasoningEncryptedContent: null } },
|
||||
},
|
||||
{
|
||||
text: '',
|
||||
providerOptions: { openai: { itemId: 'rs_new', reasoningEncryptedContent: 'enc-abc' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
}];
|
||||
|
||||
const sdkMessages = buildHistory(messages);
|
||||
|
||||
assert.equal(sdkMessages.length, 1);
|
||||
const content = sdkMessages[0].content;
|
||||
assert.ok(Array.isArray(content));
|
||||
assert.deepEqual(
|
||||
content.map((part) => (part as { type: string; text?: string }).text),
|
||||
['first summary', 'second summary', '', 'Done.'],
|
||||
);
|
||||
});
|
||||
|
||||
test('an item id whose every fragment lacks ciphertext still discards the tool exchange', () => {
|
||||
const toolCall = { id: 'call-1', name: 'terminal_execute', arguments: { command: 'ls' } };
|
||||
const toolResult = {
|
||||
toolCallId: 'call-1',
|
||||
content: 'output',
|
||||
};
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
role: 'assistant',
|
||||
content: 'Running it.',
|
||||
timestamp: 1,
|
||||
providerContinuation: {
|
||||
source: { providerConfigId: 'provider-1', providerType: 'openai', modelId: 'model-1' },
|
||||
reasoningParts: [
|
||||
{
|
||||
text: 'legacy reasoning',
|
||||
providerOptions: { openai: { itemId: 'rs_legacy', reasoningEncryptedContent: null } },
|
||||
},
|
||||
],
|
||||
},
|
||||
toolCalls: [toolCall],
|
||||
},
|
||||
{
|
||||
id: 'tool-1',
|
||||
role: 'tool',
|
||||
content: '',
|
||||
timestamp: 3,
|
||||
toolResults: [toolResult],
|
||||
},
|
||||
];
|
||||
|
||||
const sdkMessages = buildHistory(messages);
|
||||
|
||||
assert.equal(sdkMessages.length, 1);
|
||||
assert.equal(sdkMessages[0].content, 'Running it.');
|
||||
});
|
||||
Reference in New Issue
Block a user