🔨 chore: add conversation-flow module (#10052)

* wip

* refactor

* snapshot

* fix input

* fix assistant-tools calling case

* fix assistant-tools calling case

* fix

* fix compare-mode

* fix basic-branch-mode

* refactor branch test case

* refactor branch test case

* implement branch parse

* improve compare

* improve compare

* improve compare

* refactor

* refactor the transformer

* clean tests

* add test workflow

* update

* fix issue
This commit is contained in:
Arvin Xu 2025-11-05 22:24:51 +08:00 committed by GitHub
parent 4153f182fe
commit 0e2bad0a23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 7506 additions and 427 deletions

View file

@ -21,6 +21,7 @@ jobs:
- python-interpreter
- context-engine
- agent-runtime
- conversation-flow
name: Test package ${{ matrix.package }}

View file

@ -1,425 +0,0 @@
# Context Engine 架构设计
## 概述
Context Engine 是一个灵活的消息处理管道系统,用于在 AI 对话中动态管理和处理上下文信息。它提供了一种可扩展的方式来注入、转换和验证消息流。
## 核心概念
### 1. 处理器Processor
处理器是 Context Engine 的基本单元,负责对消息进行特定的处理操作。
#### 处理器分类
根据功能职责,处理器分为以下几类:
1. **注入器Injector**
- 职责:向消息流中添加新的上下文信息
- 示例SystemRoleInjector、HistoryInjector、RAGContextInjector
2. **转换器Transformer**
- 职责:修改现有消息的内容或结构
- 示例MessageRoleTransformer、ImageContentProcessor
3. **截断器Truncator**
- 职责:根据特定规则裁剪消息内容
- 示例HistoryTruncator、TokenBasedTruncator
4. **验证器Validator**
- 职责:验证消息是否符合特定要求
- 示例ModelCapabilityValidator
5. **重排器Reorderer**
- 职责:调整消息的顺序
- 示例ToolMessageReorder
### 2. 管道Pipeline
管道是一系列处理器的有序组合,消息流经管道时会被逐个处理器处理。
```typescript
interface Pipeline {
// 添加处理器到管道
add(processor: BaseProcessor): Pipeline;
// 执行管道处理
execute(context: ProcessorContext): Promise<ProcessorContext>;
// 获取管道中的所有处理器
getProcessors(): BaseProcessor[];
}
```
### 3. 上下文Context
上下文包含了处理器需要的所有信息:
```typescript
interface ProcessorContext {
messages: Message[]; // 消息列表
metadata?: ProcessorMetadata; // 元数据
variables?: Record<string, any>; // 变量
abortSignal?: AbortSignal; // 中止信号
}
```
## 架构设计
### 类层次结构
```
BaseProcessor (抽象基类)
├── BaseInjector (注入器基类)
│ ├── SystemRoleInjector
│ ├── HistoryInjector
│ ├── RAGContextInjector
│ └── ...
├── BaseTransformer (转换器基类)
│ ├── MessageRoleTransformer
│ └── ImageContentProcessor
├── BaseTruncator (截断器基类)
│ ├── HistoryTruncator
│ └── TokenBasedTruncator
├── BaseValidator (验证器基类)
│ └── ModelCapabilityValidator
└── BaseReorderer (重排器基类)
└── ToolMessageReorder
```
### 核心接口设计
#### BaseProcessor
```typescript
abstract class BaseProcessor {
// 处理器类型
abstract readonly type: ProcessorType;
// 处理器名称
abstract readonly name: string;
// 主处理方法
async process(context: ProcessorContext): Promise<ProcessorContext> {
// 1. 前置验证
this.validateInput(context);
// 2. 执行处理
const result = await this.doProcess(context);
// 3. 后置验证
this.validateOutput(result);
return result;
}
// 子类实现的核心处理逻辑
protected abstract doProcess(context: ProcessorContext): Promise<ProcessorContext>;
// 输入验证(可选覆盖)
protected validateInput(context: ProcessorContext): void {}
// 输出验证(可选覆盖)
protected validateOutput(context: ProcessorContext): void {}
}
```
#### BaseInjector
```typescript
abstract class BaseInjector extends BaseProcessor {
readonly type = ProcessorType.Injector;
protected async doProcess(context: ProcessorContext): Promise<ProcessorContext> {
// 1. 判断是否需要注入
if (!this.shouldInject(context)) {
return context;
}
// 2. 构建注入内容
const content = await this.buildContent(context);
// 3. 创建消息
const message = this.createMessage(content, context);
// 4. 确定注入位置
const position = this.getInjectionPosition(context);
// 5. 执行注入
return this.inject(context, message, position);
}
// 子类需要实现的方法
protected abstract shouldInject(context: ProcessorContext): boolean;
protected abstract buildContent(context: ProcessorContext): Promise<string>;
// 可选覆盖的方法
protected getInjectionPosition(context: ProcessorContext): number {
return 0; // 默认注入到开头
}
protected createMessage(content: string, context: ProcessorContext): Message {
return {
role: 'system',
content,
metadata: { injectedBy: this.name }
};
}
}
```
## 使用方式
### 1. 创建自定义处理器
```typescript
// 创建一个自定义注入器
class CustomContextInjector extends BaseInjector {
readonly name = 'custom-context-injector';
protected shouldInject(context: ProcessorContext): boolean {
// 判断逻辑
return !context.messages.some(msg =>
msg.metadata?.injectedBy === this.name
);
}
protected async buildContent(context: ProcessorContext): Promise<string> {
// 构建内容逻辑
return `Custom context: ${context.variables?.customValue}`;
}
}
```
### 2. 构建处理管道
```typescript
// 使用工厂模式创建管道
const pipeline = createPipeline()
.add(new SystemRoleInjector())
.add(new HistoryInjector({ maxMessages: 10 }))
.add(new CustomContextInjector())
.add(new MessageRoleTransformer())
.add(new TokenBasedTruncator({ maxTokens: 4000 }))
.add(new ModelCapabilityValidator());
// 执行管道
const result = await pipeline.execute({
messages: initialMessages,
variables: { customValue: 'test' },
metadata: { model: 'gpt-4' }
});
```
### 3. 条件处理
```typescript
// 基于条件的管道构建
const pipeline = createPipeline();
if (config.enableRAG) {
pipeline.add(new RAGContextInjector());
}
if (config.enableSearch) {
pipeline.add(new SearchContextInjector());
}
// 始终添加的处理器
pipeline
.add(new HistoryTruncator())
.add(new ModelCapabilityValidator());
```
### 4. 错误处理
```typescript
try {
const result = await pipeline.execute(context);
} catch (error) {
if (error instanceof ProcessorError) {
console.error(`Processor ${error.processorName} failed:`, error.message);
} else if (error instanceof PipelineError) {
console.error('Pipeline execution failed:', error.message);
}
}
```
## 配置管理
### 处理器配置
每个处理器可以接受特定的配置选项:
```typescript
interface ProcessorConfig {
// 通用配置
enabled?: boolean;
priority?: number;
// 处理器特定配置
[key: string]: any;
}
// 示例:历史注入器配置
interface HistoryInjectorConfig extends ProcessorConfig {
maxMessages?: number;
includeSystemMessages?: boolean;
preserveTools?: boolean;
}
```
### 管道配置
```typescript
interface PipelineConfig {
processors: Array<{
type: string;
config?: ProcessorConfig;
}>;
// 全局配置
abortOnError?: boolean;
timeout?: number;
}
// 从配置创建管道
const pipeline = createPipelineFromConfig({
processors: [
{ type: 'system-role', config: { enabled: true } },
{ type: 'history', config: { maxMessages: 20 } },
{ type: 'rag', config: { threshold: 0.7 } }
],
abortOnError: false,
timeout: 30000
});
```
## 最佳实践
### 1. 单一职责原则
每个处理器应该只负责一种特定的处理任务。
### 2. 可配置性
处理器应该通过配置参数来控制行为,而不是硬编码。
### 3. 错误处理
- 使用具体的错误类型
- 提供有意义的错误信息
- 考虑错误恢复策略
### 4. 性能优化
- 避免不必要的消息复制
- 使用流式处理处理大量数据
- 实现适当的缓存机制
### 5. 测试
- 为每个处理器编写单元测试
- 测试处理器组合的集成测试
- 边界条件和错误场景测试
## 扩展机制
### 1. 自定义处理器类型
```typescript
// 定义新的处理器类型
enum CustomProcessorType {
Analyzer = 'analyzer',
Enhancer = 'enhancer'
}
// 创建对应的基类
abstract class BaseAnalyzer extends BaseProcessor {
readonly type = CustomProcessorType.Analyzer;
// 分析器特定的方法
protected abstract analyze(messages: Message[]): AnalysisResult;
}
```
### 2. 处理器组合
```typescript
// 创建复合处理器
class CompositeProcessor extends BaseProcessor {
constructor(private processors: BaseProcessor[]) {
super();
}
protected async doProcess(context: ProcessorContext): Promise<ProcessorContext> {
let result = context;
for (const processor of this.processors) {
result = await processor.process(result);
}
return result;
}
}
```
### 3. 插件系统
```typescript
interface ProcessorPlugin {
name: string;
version: string;
processors: ProcessorDefinition[];
}
// 注册插件
registry.registerPlugin({
name: 'custom-processors',
version: '1.0.0',
processors: [
{ type: 'custom-injector', factory: () => new CustomInjector() },
{ type: 'custom-validator', factory: () => new CustomValidator() }
]
});
```
## 迁移指南
### 从当前架构迁移
1. **BaseProvider 迁移到 BaseInjector**
```typescript
// 旧代码
class MyProvider extends BaseProvider {
doProcess(context) {
// 实现
}
}
// 新代码
class MyInjector extends BaseInjector {
shouldInject(context) {
// 判断逻辑
}
buildContent(context) {
// 构建内容
}
}
```
2. **处理器分类**
- 将现有处理器按功能分类
- 继承对应的基类
- 实现必要的抽象方法
3. **配置迁移**
- 统一配置格式
- 支持向后兼容
- 提供迁移工具
## 总结
新的 Context Engine 架构提供了:
1. **清晰的分层结构**:基于功能的处理器分类
2. **灵活的扩展机制**:易于添加新的处理器类型
3. **强大的组合能力**:通过管道组合实现复杂功能
4. **完善的错误处理**:细粒度的错误类型和恢复策略
5. **优秀的可测试性**:模块化设计便于单元测试
这种设计使得 Context Engine 更加模块化、可维护和可扩展,能够更好地满足不同场景下的上下文处理需求。

View file

@ -0,0 +1,13 @@
{
"name": "@lobechat/conversation-flow",
"version": "0.0.1",
"description": "Scalable complex conversation flow rendering engine",
"main": "src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"@lobechat/types": "workspace:*"
}
}

View file

@ -0,0 +1,48 @@
import type { Message, ParseResult } from '../../types';
// Input fixtures
import assistantChainWithFollowupInput from './inputs/assistant-chain-with-followup.json';
import assistantWithToolsInput from './inputs/assistant-with-tools.json';
import { branch as branchInputs } from './inputs/branch';
import { compare as compareInputs } from './inputs/compare';
import complexScenarioInput from './inputs/complex-scenario.json';
import linearConversationInput from './inputs/linear-conversation.json';
// Output fixtures
import assistantChainWithFollowupOutput from './outputs/assistant-chain-with-followup.json';
import assistantWithToolsOutput from './outputs/assistant-with-tools.json';
import { branch as branchOutputs } from './outputs/branch';
import { compare as compareOutputs } from './outputs/compare';
import complexScenarioOutput from './outputs/complex-scenario.json';
import linearConversationOutput from './outputs/linear-conversation.json';
/**
* Serialized parse result type
*/
export interface SerializedParseResult {
contextTree: ParseResult['contextTree'];
flatList: ParseResult['flatList'];
messageMap: Record<string, Message>;
}
/**
* Test input fixtures - raw messages from database
*/
export const inputs = {
assistantChainWithFollowup: assistantChainWithFollowupInput as Message[],
assistantWithTools: assistantWithToolsInput as Message[],
branch: branchInputs,
compare: compareInputs,
complexScenario: complexScenarioInput as Message[],
linearConversation: linearConversationInput as Message[],
};
/**
* Test output fixtures - expected parse results
*/
export const outputs = {
assistantChainWithFollowup: assistantChainWithFollowupOutput as unknown as SerializedParseResult,
assistantWithTools: assistantWithToolsOutput as unknown as SerializedParseResult,
branch: branchOutputs,
compare: compareOutputs,
complexScenario: complexScenarioOutput as unknown as SerializedParseResult,
linearConversation: linearConversationOutput as unknown as SerializedParseResult,
};

View file

@ -0,0 +1,56 @@
[
{
"id": "msg-1",
"role": "user",
"content": "What's the weather?",
"createdAt": 1000,
"updatedAt": 1000,
"meta": {}
},
{
"id": "msg-2",
"role": "assistant",
"content": "Let me check the weather for you.",
"createdAt": 2000,
"updatedAt": 2000,
"parentId": "msg-1",
"meta": {},
"tools": [
{
"id": "tool-1",
"type": "default",
"apiName": "getWeather",
"identifier": "weather",
"arguments": "{\"location\":\"New York\"}"
}
]
},
{
"id": "tool-1",
"role": "tool",
"content": "{\"temperature\":\"72F\",\"condition\":\"sunny\"}",
"createdAt": 3000,
"updatedAt": 3000,
"parentId": "msg-2",
"meta": {},
"tool_call_id": "tool-1"
},
{
"id": "msg-3",
"role": "assistant",
"content": "The weather in New York is 72F and sunny.",
"createdAt": 4000,
"updatedAt": 4000,
"parentId": "tool-1",
"meta": {}
},
{
"id": "msg-4",
"role": "user",
"content": "Thanks! How about tomorrow?",
"createdAt": 5000,
"updatedAt": 5000,
"parentId": "msg-3",
"meta": {}
}
]

View file

@ -0,0 +1,144 @@
[
{
"id": "msg-101",
"role": "user",
"content": "What's the weather like in San Francisco and Beijing today?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-102",
"role": "assistant",
"content": "Let me check the current weather conditions in both cities for you.",
"parentId": "msg-101",
"tools": [
{
"id": "tool-sf",
"identifier": "weather",
"apiName": "getWeather",
"arguments": "{\"city\":\"San Francisco\"}",
"type": "default"
},
{
"id": "tool-bj",
"identifier": "weather",
"apiName": "getWeather",
"arguments": "{\"city\":\"Beijing\"}",
"type": "default"
}
],
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 28,
"totalOutputTokens": 45,
"totalTokens": 73,
"cost": 0.000219,
"tps": 48.5,
"ttft": 245,
"duration": 928,
"latency": 1173
}
},
{
"id": "msg-103",
"role": "tool",
"tool_call_id": "tool-sf",
"content": "{\"location\": \"San Francisco, CA\", \"temperature\": 18, \"conditions\": \"Partly Cloudy\", \"humidity\": 72, \"wind_speed\": 15}",
"pluginState": { "cached": false, "executionTime": 234 },
"parentId": "msg-102",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {}
},
{
"id": "msg-104",
"role": "tool",
"tool_call_id": "tool-bj",
"content": "{\"location\": \"Beijing, China\", \"temperature\": 12, \"conditions\": \"Clear\", \"humidity\": 45, \"wind_speed\": 8}",
"pluginState": { "cached": false, "executionTime": 189 },
"parentId": "msg-102",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {}
},
{
"id": "msg-105",
"role": "assistant",
"content": "Let me also check the forecast for tomorrow to give you a better comparison.",
"parentId": "msg-103",
"tools": [
{
"id": "tool-sf-forecast",
"identifier": "weather",
"apiName": "getForecast",
"arguments": "{\"city\":\"San Francisco\",\"days\":1}",
"type": "default"
},
{
"id": "tool-bj-forecast",
"identifier": "weather",
"apiName": "getForecast",
"arguments": "{\"city\":\"Beijing\",\"days\":1}",
"type": "default"
}
],
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 156,
"totalOutputTokens": 42,
"totalTokens": 198,
"cost": 0.000594,
"tps": 48,
"ttft": 178,
"duration": 875,
"latency": 1053
}
},
{
"id": "msg-106",
"role": "tool",
"tool_call_id": "tool-sf-forecast",
"content": "{\"location\": \"San Francisco, CA\", \"date\": \"2024-01-02\", \"high\": 20, \"low\": 14, \"conditions\": \"Sunny\"}",
"pluginState": { "cached": false, "executionTime": 198 },
"parentId": "msg-105",
"createdAt": 1704067206000,
"updatedAt": 1704067206000,
"meta": {}
},
{
"id": "msg-107",
"role": "tool",
"tool_call_id": "tool-bj-forecast",
"content": "{\"location\": \"Beijing, China\", \"date\": \"2024-01-02\", \"high\": 15, \"low\": 8, \"conditions\": \"Partly Cloudy\"}",
"pluginState": { "cached": false, "executionTime": 176 },
"parentId": "msg-105",
"createdAt": 1704067206100,
"updatedAt": 1704067206100,
"meta": {}
},
{
"id": "msg-108",
"role": "assistant",
"content": "Based on both current weather and tomorrow's forecast:\n\n**Today:**\n- San Francisco: 18°C, partly cloudy\n- Beijing: 12°C, clear\n\n**Tomorrow:**\n- San Francisco: High 20°C / Low 14°C, sunny\n- Beijing: High 15°C / Low 8°C, partly cloudy\n\nSan Francisco will have pleasant sunny weather tomorrow with a slight temperature increase. Beijing will see some clouds moving in but will still be cooler overall.",
"parentId": "msg-106",
"createdAt": 1704067208000,
"updatedAt": 1704067208000,
"meta": {},
"metadata": {
"totalInputTokens": 245,
"totalOutputTokens": 112,
"totalTokens": 357,
"cost": 0.001071,
"tps": 54,
"ttft": 165,
"duration": 2074,
"latency": 2239
}
}
]

View file

@ -0,0 +1,131 @@
[
{
"id": "msg-201",
"role": "user",
"content": "Write a haiku about programming",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
{
"id": "msg-202",
"role": "assistant",
"content": "Code flows like water,\nBugs hide in silent shadows,\nDebug brings the light.",
"parentId": "msg-201",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 28,
"totalTokens": 40,
"cost": 0.00012
}
},
{
"id": "msg-203",
"role": "assistant",
"content": "Functions compile,\nVariables dance in memory,\nAlgorithm sings.",
"parentId": "msg-201",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 24,
"totalTokens": 36,
"cost": 0.000108
}
},
{
"id": "msg-204",
"role": "assistant",
"content": "Lines of logic flow,\nErrors break the peaceful code,\nTests restore the calm.",
"parentId": "msg-201",
"createdAt": 1704067203200,
"updatedAt": 1704067203200,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 26,
"totalTokens": 38,
"cost": 0.000114
}
},
{
"id": "msg-205",
"role": "user",
"content": "I like the first one best!",
"parentId": "msg-202",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-206",
"role": "assistant",
"content": "Thank you! I'm glad you enjoyed it. The metaphor of light revealing hidden bugs is one of my favorites. Would you like me to write more haikus on different programming topics?",
"parentId": "msg-205",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 52,
"totalOutputTokens": 48,
"totalTokens": 100,
"cost": 0.0003
}
},
{
"id": "msg-207",
"role": "user",
"content": "Actually, I prefer the second one with the dancing variables.",
"parentId": "msg-203",
"createdAt": 1704067225000,
"updatedAt": 1704067225000,
"meta": {}
},
{
"id": "msg-208",
"role": "assistant",
"content": "I appreciate your feedback! The imagery of variables dancing in memory really captures the dynamic nature of programming. Would you like me to explore more haikus with that kind of vivid metaphor?",
"parentId": "msg-207",
"createdAt": 1704067227000,
"updatedAt": 1704067227000,
"meta": {},
"metadata": {
"totalInputTokens": 48,
"totalOutputTokens": 52,
"totalTokens": 100,
"cost": 0.0003
}
},
{
"id": "msg-209",
"role": "user",
"content": "Yes, please write one more about debugging!",
"parentId": "msg-206",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {}
},
{
"id": "msg-210",
"role": "assistant",
"content": "Console logs cascade,\nBreakpoints pause the racing code,\nTruth emerges clear.",
"parentId": "msg-209",
"createdAt": 1704067232000,
"updatedAt": 1704067232000,
"meta": {},
"metadata": {
"totalInputTokens": 78,
"totalOutputTokens": 24,
"totalTokens": 102,
"cost": 0.000306
}
}
]

View file

@ -0,0 +1,96 @@
[
{
"id": "msg-401",
"role": "user",
"content": "Can you help me write a function to calculate fibonacci?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-402",
"role": "assistant",
"content": "I'll help you write a fibonacci function.",
"parentId": "msg-401",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 15,
"totalOutputTokens": 12,
"totalTokens": 27,
"cost": 0.00008,
"activeBranchIndex": 1
}
},
{
"id": "msg-403",
"role": "assistant",
"content": "Here's a recursive implementation:\n\n```python\ndef fibonacci(n):\n if n <= 1:\n return n\n return fibonacci(n-1) + fibonacci(n-2)\n```",
"parentId": "msg-402",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 45,
"totalTokens": 75,
"cost": 0.00022
}
},
{
"id": "msg-404",
"role": "assistant",
"content": "Here's an iterative approach which is more efficient:\n\n```python\ndef fibonacci(n):\n if n <= 1:\n return n\n a, b = 0, 1\n for _ in range(2, n + 1):\n a, b = b, a + b\n return b\n```",
"parentId": "msg-402",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 52,
"totalTokens": 82,
"cost": 0.00025
}
},
{
"id": "msg-405",
"role": "assistant",
"content": "Let me show you a dynamic programming solution:\n\n```python\ndef fibonacci(n):\n if n <= 1:\n return n\n dp = [0, 1]\n for i in range(2, n + 1):\n dp.append(dp[i-1] + dp[i-2])\n return dp[n]\n```",
"parentId": "msg-402",
"createdAt": 1704067205200,
"updatedAt": 1704067205200,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 58,
"totalTokens": 88,
"cost": 0.00026
}
},
{
"id": "msg-406",
"role": "user",
"content": "The iterative approach looks good! Can you add some comments?",
"parentId": "msg-404",
"createdAt": 1704067215000,
"updatedAt": 1704067215000,
"meta": {}
},
{
"id": "msg-407",
"role": "assistant",
"content": "Sure! Here's the commented version:\n\n```python\ndef fibonacci(n):\n # Base cases\n if n <= 1:\n return n\n \n # Initialize first two fibonacci numbers\n a, b = 0, 1\n \n # Iterate from 2 to n\n for _ in range(2, n + 1):\n # Calculate next fibonacci number\n a, b = b, a + b\n \n return b\n```",
"parentId": "msg-406",
"createdAt": 1704067217000,
"updatedAt": 1704067217000,
"meta": {},
"metadata": {
"totalInputTokens": 95,
"totalOutputTokens": 78,
"totalTokens": 173,
"cost": 0.00052
}
}
]

View file

@ -0,0 +1,123 @@
[
{
"id": "msg-501",
"role": "user",
"content": "What's the difference between map and forEach in JavaScript?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-502",
"role": "assistant",
"content": "The main difference is that `map()` returns a new array with transformed elements, while `forEach()` just iterates and returns undefined. Map is better for transformations, forEach is better for side effects.",
"parentId": "msg-501",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 18,
"totalOutputTokens": 42,
"totalTokens": 60,
"cost": 0.00018,
"activeBranchIndex": 1
}
},
{
"id": "msg-503",
"role": "user",
"content": "Can you show me a practical example?",
"parentId": "msg-502",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
{
"id": "msg-504",
"role": "assistant",
"content": "Sure! Here's a practical example:\n\n```javascript\nconst numbers = [1, 2, 3, 4, 5];\n\n// Using map - creates new array\nconst doubled = numbers.map(n => n * 2);\nconsole.log(doubled); // [2, 4, 6, 8, 10]\n\n// Using forEach - just iterates\nnumbers.forEach(n => console.log(n * 2));\n// Prints: 2, 4, 6, 8, 10\n```",
"parentId": "msg-503",
"createdAt": 1704067212000,
"updatedAt": 1704067212000,
"meta": {},
"metadata": {
"totalInputTokens": 68,
"totalOutputTokens": 95,
"totalTokens": 163,
"cost": 0.00049
}
},
{
"id": "msg-505",
"role": "user",
"content": "Thanks! What about performance differences?",
"parentId": "msg-504",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-506",
"role": "assistant",
"content": "Performance-wise, forEach is slightly faster because it doesn't create a new array. However, the difference is negligible for most use cases. Choose based on functionality: use map when you need the transformed array, use forEach when you just need to iterate.",
"parentId": "msg-505",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 175,
"totalOutputTokens": 58,
"totalTokens": 233,
"cost": 0.0007
}
},
{
"id": "msg-507",
"role": "user",
"content": "Can you explain the technical implementation details?",
"parentId": "msg-502",
"createdAt": 1704067210100,
"updatedAt": 1704067210100,
"meta": {}
},
{
"id": "msg-508",
"role": "assistant",
"content": "Certainly! From a technical perspective:\n\n**map():**\n- Allocates a new array in memory\n- Iterates through each element\n- Applies the callback function\n- Stores the result in the new array\n- Returns the new array\n\n**forEach():**\n- Iterates through each element\n- Applies the callback function\n- Discards the return value\n- Returns undefined",
"parentId": "msg-507",
"createdAt": 1704067212100,
"updatedAt": 1704067212100,
"meta": {},
"metadata": {
"totalInputTokens": 68,
"totalOutputTokens": 82,
"totalTokens": 150,
"cost": 0.00045
}
},
{
"id": "msg-509",
"role": "user",
"content": "What about memory usage?",
"parentId": "msg-508",
"createdAt": 1704067220100,
"updatedAt": 1704067220100,
"meta": {}
},
{
"id": "msg-510",
"role": "assistant",
"content": "Great question! Map uses more memory because it creates a new array. If you have a large dataset (millions of items) and don't need the result array, forEach is more memory-efficient. However, if you're doing functional programming and need immutability, the memory overhead of map is usually worth it.",
"parentId": "msg-509",
"createdAt": 1704067222100,
"updatedAt": 1704067222100,
"meta": {},
"metadata": {
"totalInputTokens": 160,
"totalOutputTokens": 68,
"totalTokens": 228,
"cost": 0.00068
}
}
]

View file

@ -0,0 +1,128 @@
[
{
"id": "msg-201",
"role": "user",
"content": "Write a haiku about programming",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-202",
"role": "assistant",
"content": "Code flows like water,\nBugs hide in silent shadows,\nDebug brings the light.",
"parentId": "msg-201",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 28,
"totalTokens": 40,
"cost": 0.00012
}
},
{
"id": "msg-203",
"role": "assistant",
"content": "Functions compile,\nVariables dance in memory,\nAlgorithm sings.",
"parentId": "msg-201",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 24,
"totalTokens": 36,
"cost": 0.000108
}
},
{
"id": "msg-204",
"role": "assistant",
"content": "Lines of logic flow,\nErrors break the peaceful code,\nTests restore the calm.",
"parentId": "msg-201",
"createdAt": 1704067203200,
"updatedAt": 1704067203200,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 26,
"totalTokens": 38,
"cost": 0.000114
}
},
{
"id": "msg-205",
"role": "user",
"content": "I like the first one best!",
"parentId": "msg-202",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-206",
"role": "assistant",
"content": "Thank you! I'm glad you enjoyed it. The metaphor of light revealing hidden bugs is one of my favorites. Would you like me to write more haikus on different programming topics?",
"parentId": "msg-205",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 52,
"totalOutputTokens": 48,
"totalTokens": 100,
"cost": 0.0003
}
},
{
"id": "msg-207",
"role": "user",
"content": "Actually, I prefer the second one with the dancing variables.",
"parentId": "msg-203",
"createdAt": 1704067225000,
"updatedAt": 1704067225000,
"meta": {}
},
{
"id": "msg-208",
"role": "assistant",
"content": "I appreciate your feedback! The imagery of variables dancing in memory really captures the dynamic nature of programming. Would you like me to explore more haikus with that kind of vivid metaphor?",
"parentId": "msg-207",
"createdAt": 1704067227000,
"updatedAt": 1704067227000,
"meta": {},
"metadata": {
"totalInputTokens": 48,
"totalOutputTokens": 52,
"totalTokens": 100,
"cost": 0.0003
}
},
{
"id": "msg-209",
"role": "user",
"content": "Yes, please write one more about debugging!",
"parentId": "msg-206",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {}
},
{
"id": "msg-210",
"role": "assistant",
"content": "Console logs cascade,\nBreakpoints pause the racing code,\nTruth emerges clear.",
"parentId": "msg-209",
"createdAt": 1704067232000,
"updatedAt": 1704067232000,
"meta": {},
"metadata": {
"totalInputTokens": 78,
"totalOutputTokens": 24,
"totalTokens": 102,
"cost": 0.000306
}
}
]

View file

@ -0,0 +1,14 @@
import type { Message } from '../../../../types';
import activeIndex1 from './active-index-1.json';
import assistantBranch from './assistant-branch.json';
import assistantUserBranch from './assistant-user-branch.json';
import conversation from './conversation.json';
import nested from './nested.json';
export const branch = {
activeIndex1: activeIndex1 as Message[],
assistantBranch: assistantBranch as Message[],
assistantUserBranch: assistantUserBranch as Message[],
conversation: conversation as Message[],
nested: nested as Message[],
};

View file

@ -0,0 +1,179 @@
[
{
"id": "msg-301",
"role": "user",
"content": "What's your favorite programming paradigm?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-302",
"role": "assistant",
"content": "I appreciate functional programming for its elegance.",
"parentId": "msg-301",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 10,
"totalOutputTokens": 15,
"totalTokens": 25,
"cost": 0.00008
}
},
{
"id": "msg-303",
"role": "assistant",
"content": "Object-oriented programming offers great structure.",
"parentId": "msg-301",
"createdAt": 1704067202100,
"updatedAt": 1704067202100,
"meta": {},
"metadata": {
"totalInputTokens": 10,
"totalOutputTokens": 12,
"totalTokens": 22,
"cost": 0.00007
}
},
{
"id": "msg-304",
"role": "assistant",
"content": "Procedural programming is straightforward and efficient.",
"parentId": "msg-301",
"createdAt": 1704067202200,
"updatedAt": 1704067202200,
"meta": {},
"metadata": {
"totalInputTokens": 10,
"totalOutputTokens": 13,
"totalTokens": 23,
"cost": 0.000069
}
},
{
"id": "msg-305",
"role": "user",
"content": "Can you explain more about functional programming?",
"parentId": "msg-302",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
{
"id": "msg-306",
"role": "assistant",
"content": "Functional programming focuses on pure functions and immutability.",
"parentId": "msg-305",
"createdAt": 1704067212000,
"updatedAt": 1704067212000,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 18,
"totalTokens": 48,
"cost": 0.00014
}
},
{
"id": "msg-307",
"role": "assistant",
"content": "In functional programming, functions are first-class citizens and side effects are minimized.",
"parentId": "msg-305",
"createdAt": 1704067212100,
"updatedAt": 1704067212100,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 22,
"totalTokens": 52,
"cost": 0.00016
}
},
{
"id": "msg-310",
"role": "user",
"content": "What are some benefits of immutability?",
"parentId": "msg-307",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-311",
"role": "assistant",
"content": "Immutability prevents bugs from unexpected data changes.",
"parentId": "msg-310",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 55,
"totalOutputTokens": 16,
"totalTokens": 71,
"cost": 0.00021
}
},
{
"id": "msg-312",
"role": "assistant",
"content": "It makes code easier to reason about and test.",
"parentId": "msg-310",
"createdAt": 1704067222100,
"updatedAt": 1704067222100,
"meta": {},
"metadata": {
"totalInputTokens": 55,
"totalOutputTokens": 14,
"totalTokens": 69,
"cost": 0.0002
}
},
{
"id": "msg-314",
"role": "user",
"content": "Can you give me an example?",
"parentId": "msg-311",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
{
"id": "msg-315",
"role": "assistant",
"content": "Sure! In JavaScript, using `const` with arrays means you can't reassign the variable, but you can still modify the array with push/pop.",
"parentId": "msg-314",
"createdAt": 1704067232000,
"updatedAt": 1704067232000,
"meta": {},
"metadata": {
"totalInputTokens": 75,
"totalOutputTokens": 35,
"totalTokens": 110,
"cost": 0.00033
}
},
{
"id": "msg-316",
"role": "assistant",
"content": "Here's a better example: Instead of mutating an array with push(), functional programming uses methods like concat() or the spread operator to create new arrays.",
"parentId": "msg-314",
"createdAt": 1704067232100,
"updatedAt": 1704067232100,
"meta": {},
"metadata": {
"totalInputTokens": 75,
"totalOutputTokens": 42,
"totalTokens": 117,
"cost": 0.00035
}
}
]

View file

@ -0,0 +1,8 @@
import type { Message } from '../../../../types';
import simple from './simple.json';
import withTools from './with-tools.json';
export const compare = {
simple: simple as Message[],
withTools: withTools as Message[],
};

View file

@ -0,0 +1,85 @@
[
{
"id": "msg-601",
"role": "user",
"content": "Should I use REST or GraphQL for my API?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"compare": true
}
},
{
"id": "msg-602",
"role": "assistant",
"content": "**REST is a great choice:**\n\nREST is simple, well-understood, and has excellent caching support. It's perfect for:\n\n- CRUD operations\n- Public APIs\n- Mobile apps with limited bandwidth\n- Teams familiar with HTTP conventions\n\n**Advantages:**\n- HTTP caching works out of the box\n- Stateless architecture\n- Wide tooling support\n- Easy to learn\n\n**Trade-offs:**\n- Over-fetching/under-fetching data\n- Multiple endpoints to maintain\n- Versioning can be complex",
"parentId": "msg-601",
"model": "gpt-4",
"provider": "openai",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 145,
"totalTokens": 157,
"cost": 0.00471,
"tps": 42.0,
"ttft": 387,
"duration": 3452,
"latency": 3839
}
},
{
"id": "msg-603",
"role": "assistant",
"content": "**GraphQL offers more flexibility:**\n\nGraphQL provides a powerful query language that lets clients request exactly what they need. It's ideal for:\n\n- Complex data relationships\n- Multiple client types (web, mobile, desktop)\n- Rapidly evolving requirements\n- Real-time applications\n\n**Advantages:**\n- Single endpoint\n- No over-fetching\n- Strong typing with schema\n- Powerful developer tools\n\n**Trade-offs:**\n- Steeper learning curve\n- More complex server setup\n- Caching requires extra effort\n- Can be overkill for simple APIs",
"parentId": "msg-601",
"model": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 158,
"totalTokens": 170,
"cost": 0.00085,
"tps": 51.0,
"ttft": 298,
"duration": 3098,
"latency": 3396,
"activeColumn": true
}
},
{
"id": "msg-604",
"role": "user",
"content": "What about performance differences?",
"parentId": "msg-603",
"createdAt": 1704067215000,
"updatedAt": 1704067215000,
"meta": {}
},
{
"id": "msg-605",
"role": "assistant",
"content": "Performance-wise, both can be highly performant when implemented correctly:\n\n**REST Performance:**\n- Leverages HTTP caching (CDNs, browser cache)\n- Simpler server logic = faster response times\n- Can over-fetch data, wasting bandwidth\n\n**GraphQL Performance:**\n- Eliminates over-fetching, reducing payload size\n- Single request vs. multiple REST calls\n- Requires query complexity analysis to prevent abuse\n- DataLoader pattern helps with N+1 queries\n\n**Recommendation:** For your use case, if you're building a simple CRUD API, REST will likely perform better with less effort. If you need flexible data fetching across multiple clients, GraphQL's efficiency gains will outweigh the setup complexity.",
"parentId": "msg-604",
"createdAt": 1704067217000,
"updatedAt": 1704067217000,
"meta": {},
"metadata": {
"totalInputTokens": 182,
"totalOutputTokens": 167,
"totalTokens": 349,
"cost": 0.001745,
"tps": 48.0,
"ttft": 345,
"duration": 3479,
"latency": 3824
}
}
]

View file

@ -0,0 +1,169 @@
[
{
"id": "msg-301",
"role": "user",
"content": "Compare the performance of bubble sort vs quicksort",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"compare": true
}
},
{
"id": "msg-302",
"role": "assistant",
"content": "**Bubble Sort Analysis:**\n\n```python\ndef bubble_sort(arr):\n n = len(arr)\n for i in range(n):\n for j in range(0, n-i-1):\n if arr[j] > arr[j+1]:\n arr[j], arr[j+1] = arr[j+1], arr[j]\n return arr\n```\n\n**Time Complexity:**\n- Best Case: O(n)\n- Average Case: O(n²)\n- Worst Case: O(n²)\n\n**Space Complexity:** O(1)\n\n**Pros:**\n- Simple to implement\n- No extra memory needed\n- Stable sorting algorithm\n\n**Cons:**\n- Very slow for large datasets\n- Not practical for real-world use",
"parentId": "msg-301",
"model": "gpt-4",
"provider": "openai",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 198,
"totalTokens": 222,
"cost": 0.00666,
"tps": 45.0,
"ttft": 456,
"duration": 4400,
"latency": 4856
}
},
{
"id": "msg-303",
"role": "assistant",
"content": "**QuickSort Analysis:**\n\n```python\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[len(arr) // 2]\n left = [x for x in arr if x < pivot]\n middle = [x for x in arr if x == pivot]\n right = [x for x in arr if x > pivot]\n return quicksort(left) + middle + quicksort(right)\n```\n\n**Time Complexity:**\n- Best Case: O(n log n)\n- Average Case: O(n log n)\n- Worst Case: O(n²)\n\n**Space Complexity:** O(log n)\n\n**Pros:**\n- Very fast in practice\n- Good cache locality\n- Industry standard\n\n**Cons:**\n- Not stable by default\n- Worst case is rare but possible\n- Uses recursion",
"parentId": "msg-301",
"model": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 215,
"totalTokens": 239,
"cost": 0.001195,
"tps": 58.0,
"ttft": 312,
"duration": 3707,
"latency": 4019,
"activeColumn": true
}
},
{
"id": "msg-304",
"role": "assistant",
"content": "Let me run a benchmark test to compare these two sorting algorithms with real data.",
"parentId": "msg-301",
"model": "gpt-4",
"provider": "openai",
"tools": [
{
"id": "tool-benchmark-bubble",
"identifier": "code-runner",
"apiName": "runBenchmark",
"arguments": "{\"algorithm\":\"bubble_sort\",\"size\":1000}",
"type": "default"
},
{
"id": "tool-benchmark-quick",
"identifier": "code-runner",
"apiName": "runBenchmark",
"arguments": "{\"algorithm\":\"quicksort\",\"size\":1000}",
"type": "default"
}
],
"createdAt": 1704067205200,
"updatedAt": 1704067205200,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 38,
"totalTokens": 62,
"cost": 0.000186,
"tps": 52.0,
"ttft": 198,
"duration": 730,
"latency": 928
}
},
{
"id": "msg-305",
"role": "tool",
"tool_call_id": "tool-benchmark-bubble",
"content": "{\"algorithm\":\"bubble_sort\",\"arraySize\":1000,\"executionTime\":\"2.34s\",\"comparisons\":499500,\"swaps\":250000}",
"pluginState": {
"cached": false,
"executionTime": 2340
},
"parentId": "msg-304",
"createdAt": 1704067207540,
"updatedAt": 1704067207540,
"meta": {}
},
{
"id": "msg-306",
"role": "tool",
"tool_call_id": "tool-benchmark-quick",
"content": "{\"algorithm\":\"quicksort\",\"arraySize\":1000,\"executionTime\":\"0.012s\",\"comparisons\":9976,\"recursionDepth\":12}",
"pluginState": {
"cached": false,
"executionTime": 12
},
"parentId": "msg-304",
"createdAt": 1704067207552,
"updatedAt": 1704067207552,
"meta": {}
},
{
"id": "msg-307",
"role": "assistant",
"content": "**Benchmark Results (Array Size: 1000):**\n\n**Bubble Sort:**\n- Execution Time: 2.34 seconds\n- Comparisons: 499,500\n- Swaps: 250,000\n\n**QuickSort:**\n- Execution Time: 0.012 seconds\n- Comparisons: 9,976\n- Recursion Depth: 12\n\n**Conclusion:** QuickSort is approximately **195x faster** than Bubble Sort for an array of 1000 elements. This demonstrates why QuickSort is the industry standard for general-purpose sorting.",
"parentId": "msg-306",
"createdAt": 1704067208000,
"updatedAt": 1704067208000,
"meta": {},
"metadata": {
"totalInputTokens": 156,
"totalOutputTokens": 134,
"totalTokens": 290,
"cost": 0.00087,
"tps": 48.0,
"ttft": 234,
"duration": 2791,
"latency": 3025
}
},
{
"id": "msg-308",
"role": "user",
"content": "Great analysis! Can you explain why QuickSort has better cache locality?",
"parentId": "msg-303",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-309",
"role": "assistant",
"content": "Excellent question! QuickSort has better cache locality because:\n\n1. **Sequential Access**: QuickSort processes array elements sequentially during partitioning, which means the CPU can prefetch nearby memory locations into cache.\n\n2. **In-place Partitioning**: Elements are rearranged within the same array, keeping data in the same memory region.\n\n3. **Smaller Working Set**: As QuickSort divides the array, it works on smaller subarrays that fit better in CPU cache.\n\n4. **Cache Lines**: Modern CPUs load data in cache lines (typically 64 bytes). QuickSort's sequential access pattern means when you access one element, nearby elements are already cached.\n\nIn contrast, some sorting algorithms like merge sort need to copy data to auxiliary arrays, causing more cache misses.",
"parentId": "msg-308",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 268,
"totalOutputTokens": 176,
"totalTokens": 444,
"cost": 0.00222,
"tps": 52.0,
"ttft": 312,
"duration": 3384,
"latency": 3696
}
}
]

View file

@ -0,0 +1,107 @@
[
{
"id": "msg-501",
"role": "user",
"content": "Help me debug this Python code that's throwing an error",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-502",
"role": "assistant",
"content": "I'll help you debug the code. Let me first check the syntax and run some analysis.",
"parentId": "msg-501",
"tools": [
{
"id": "tool-lint",
"identifier": "code-analyzer",
"apiName": "analyzePython",
"arguments": "{\"action\":\"lint\"}",
"type": "default"
},
{
"id": "tool-test",
"identifier": "code-analyzer",
"apiName": "runTests",
"arguments": "{\"action\":\"test\"}",
"type": "default"
}
],
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"usage": {
"totalInputTokens": 32,
"totalOutputTokens": 48,
"totalTokens": 80,
"cost": 0.00024
},
"performance": {
"tps": 45.0
}
},
{
"id": "msg-503",
"role": "tool",
"tool_call_id": "tool-lint",
"content": "{\"status\":\"success\",\"issues\":[{\"line\":15,\"message\":\"undefined variable 'result'\"}]}",
"pluginState": { "cached": false },
"parentId": "msg-502",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {}
},
{
"id": "msg-504",
"role": "tool",
"tool_call_id": "tool-test",
"content": "{\"status\":\"failed\",\"error\":\"NameError: name 'result' is not defined\"}",
"pluginState": { "cached": false },
"pluginError": { "type": "NameError", "message": "name 'result' is not defined" },
"parentId": "msg-502",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {}
},
{
"id": "msg-505",
"role": "user",
"content": "Here's the code:\n```python\ndef calculate():\n x = 5\n y = 10\n print(result)\n```",
"parentId": "msg-504",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
{
"id": "msg-506",
"role": "assistant",
"content": "I found the issue! On line 15, you're trying to use `result` but it's never defined. Here's the fix:\n\n```python\ndef calculate():\n x = 5\n y = 10\n result = x + y # Define result before using it\n print(result)\n```",
"parentId": "msg-505",
"createdAt": 1704067213000,
"updatedAt": 1704067213000,
"meta": {},
"usage": {
"totalInputTokens": 156,
"totalOutputTokens": 89,
"totalTokens": 245,
"cost": 0.000735
}
},
{
"id": "msg-507",
"role": "assistant",
"content": "Alternatively, you could return the value instead:\n\n```python\ndef calculate():\n x = 5\n y = 10\n return x + y\n\nresult = calculate()\nprint(result)\n```",
"parentId": "msg-505",
"createdAt": 1704067213100,
"updatedAt": 1704067213100,
"meta": {},
"usage": {
"totalInputTokens": 156,
"totalOutputTokens": 72,
"totalTokens": 228,
"cost": 0.000684
}
}
]

View file

@ -0,0 +1,14 @@
import type { Message } from '../../../types';
import assistantWithTools from './assistant-with-tools.json';
import { branch } from './branch';
import { compare } from './compare';
import complexScenario from './complex-scenario.json';
import linearConversation from './linear-conversation.json';
export const inputs = {
assistantWithTools: assistantWithTools as Message[],
branch,
compare,
complexScenario: complexScenario as Message[],
linearConversation: linearConversation as Message[],
};

View file

@ -0,0 +1,59 @@
[
{
"id": "msg-001",
"role": "user",
"content": "Can you help me write a Python function to check if a number is prime?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-002",
"role": "assistant",
"content": "I'd be happy to help you write a function to check if a number is prime. Here's a simple and efficient implementation:\n\n```python\ndef is_prime(n):\n if n < 2:\n return False\n if n == 2:\n return True\n if n % 2 == 0:\n return False\n \n for i in range(3, int(n**0.5) + 1, 2):\n if n % i == 0:\n return False\n \n return True\n```\n\nThis function works by checking divisibility up to the square root of the number.",
"parentId": "msg-001",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"usage": {
"totalInputTokens": 24,
"totalOutputTokens": 156,
"totalTokens": 180,
"cost": 0.00054
},
"performance": {
"tps": 52.0,
"ttft": 234,
"duration": 3000,
"latency": 3234
}
},
{
"id": "msg-003",
"role": "user",
"content": "Thanks! Can you add some test cases?",
"parentId": "msg-002",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
{
"id": "msg-004",
"role": "assistant",
"content": "Sure! Here are some test cases:\n\n```python\nprint(is_prime(2)) # True\nprint(is_prime(17)) # True\nprint(is_prime(100)) # False\nprint(is_prime(1)) # False\nprint(is_prime(-5)) # False\n```",
"parentId": "msg-003",
"createdAt": 1704067213000,
"updatedAt": 1704067213000,
"meta": {},
"usage": {
"totalInputTokens": 180,
"totalOutputTokens": 89,
"totalTokens": 269,
"cost": 0.000807
},
"performance": {
"tps": 44.5
}
}
]

View file

@ -0,0 +1,135 @@
{
"contextTree": [
{
"id": "msg-1",
"type": "message"
},
{
"children": [
{
"id": "msg-2",
"type": "message",
"tools": ["tool-1"]
},
{
"id": "msg-3",
"type": "message"
}
],
"id": "msg-2",
"type": "assistantGroup"
},
{
"id": "msg-4",
"type": "message"
}
],
"flatList": [
{
"id": "msg-1",
"role": "user",
"content": "What's the weather?",
"createdAt": 1000,
"updatedAt": 1000,
"meta": {}
},
{
"id": "msg-2",
"role": "assistantGroup",
"content": "",
"createdAt": 2000,
"updatedAt": 2000,
"parentId": "msg-1",
"meta": {},
"children": [
{
"content": "Let me check the weather for you.",
"id": "msg-2",
"tools": [
{
"id": "tool-1",
"type": "default",
"apiName": "getWeather",
"identifier": "weather",
"arguments": "{\"location\":\"New York\"}",
"result": {
"content": "{\"temperature\":\"72F\",\"condition\":\"sunny\"}",
"id": "tool-1"
},
"result_msg_id": "tool-1"
}
]
},
{
"content": "The weather in New York is 72F and sunny.",
"id": "msg-3"
}
]
},
{
"id": "msg-4",
"role": "user",
"content": "Thanks! How about tomorrow?",
"createdAt": 5000,
"updatedAt": 5000,
"parentId": "msg-3",
"meta": {}
}
],
"messageMap": {
"msg-1": {
"id": "msg-1",
"role": "user",
"content": "What's the weather?",
"createdAt": 1000,
"updatedAt": 1000,
"meta": {}
},
"msg-2": {
"id": "msg-2",
"role": "assistant",
"content": "Let me check the weather for you.",
"createdAt": 2000,
"updatedAt": 2000,
"parentId": "msg-1",
"meta": {},
"tools": [
{
"id": "tool-1",
"type": "default",
"apiName": "getWeather",
"identifier": "weather",
"arguments": "{\"location\":\"New York\"}"
}
]
},
"tool-1": {
"id": "tool-1",
"role": "tool",
"content": "{\"temperature\":\"72F\",\"condition\":\"sunny\"}",
"createdAt": 3000,
"updatedAt": 3000,
"parentId": "msg-2",
"meta": {},
"tool_call_id": "tool-1"
},
"msg-3": {
"id": "msg-3",
"role": "assistant",
"content": "The weather in New York is 72F and sunny.",
"createdAt": 4000,
"updatedAt": 4000,
"parentId": "tool-1",
"meta": {}
},
"msg-4": {
"id": "msg-4",
"role": "user",
"content": "Thanks! How about tomorrow?",
"createdAt": 5000,
"updatedAt": 5000,
"parentId": "msg-3",
"meta": {}
}
}
}

View file

@ -0,0 +1,340 @@
{
"contextTree": [
{
"id": "msg-101",
"type": "message"
},
{
"children": [
{
"id": "msg-102",
"type": "message",
"tools": [
"msg-103",
"msg-104"
]
},
{
"id": "msg-105",
"type": "message",
"tools": [
"msg-106",
"msg-107"
]
},
{
"id": "msg-108",
"type": "message"
}
],
"id": "msg-102",
"type": "assistantGroup"
}
],
"flatList": [
{
"id": "msg-101",
"role": "user",
"content": "What's the weather like in San Francisco and Beijing today?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-102",
"role": "assistantGroup",
"content": "",
"parentId": "msg-101",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"children": [
{
"content": "Let me check the current weather conditions in both cities for you.",
"id": "msg-102",
"performance": {
"duration": 928,
"latency": 1173,
"tps": 48.5,
"ttft": 245
},
"tools": [
{
"id": "tool-sf",
"identifier": "weather",
"apiName": "getWeather",
"arguments": "{\"city\":\"San Francisco\"}",
"type": "default",
"result": {
"content": "{\"location\": \"San Francisco, CA\", \"temperature\": 18, \"conditions\": \"Partly Cloudy\", \"humidity\": 72, \"wind_speed\": 15}",
"id": "msg-103",
"state": {
"cached": false,
"executionTime": 234
}
},
"result_msg_id": "msg-103"
},
{
"id": "tool-bj",
"identifier": "weather",
"apiName": "getWeather",
"arguments": "{\"city\":\"Beijing\"}",
"type": "default",
"result": {
"content": "{\"location\": \"Beijing, China\", \"temperature\": 12, \"conditions\": \"Clear\", \"humidity\": 45, \"wind_speed\": 8}",
"id": "msg-104",
"state": {
"cached": false,
"executionTime": 189
}
},
"result_msg_id": "msg-104"
}
],
"usage": {
"cost": 0.000219,
"totalInputTokens": 28,
"totalOutputTokens": 45,
"totalTokens": 73
}
},
{
"content": "Let me also check the forecast for tomorrow to give you a better comparison.",
"id": "msg-105",
"performance": {
"duration": 875,
"latency": 1053,
"tps": 48,
"ttft": 178
},
"tools": [
{
"id": "tool-sf-forecast",
"identifier": "weather",
"apiName": "getForecast",
"arguments": "{\"city\":\"San Francisco\",\"days\":1}",
"type": "default",
"result": {
"content": "{\"location\": \"San Francisco, CA\", \"date\": \"2024-01-02\", \"high\": 20, \"low\": 14, \"conditions\": \"Sunny\"}",
"id": "msg-106",
"state": {
"cached": false,
"executionTime": 198
}
},
"result_msg_id": "msg-106"
},
{
"id": "tool-bj-forecast",
"identifier": "weather",
"apiName": "getForecast",
"arguments": "{\"city\":\"Beijing\",\"days\":1}",
"type": "default",
"result": {
"content": "{\"location\": \"Beijing, China\", \"date\": \"2024-01-02\", \"high\": 15, \"low\": 8, \"conditions\": \"Partly Cloudy\"}",
"id": "msg-107",
"state": {
"cached": false,
"executionTime": 176
}
},
"result_msg_id": "msg-107"
}
],
"usage": {
"cost": 0.000594,
"totalInputTokens": 156,
"totalOutputTokens": 42,
"totalTokens": 198
}
},
{
"content": "Based on both current weather and tomorrow's forecast:\n\n**Today:**\n- San Francisco: 18°C, partly cloudy\n- Beijing: 12°C, clear\n\n**Tomorrow:**\n- San Francisco: High 20°C / Low 14°C, sunny\n- Beijing: High 15°C / Low 8°C, partly cloudy\n\nSan Francisco will have pleasant sunny weather tomorrow with a slight temperature increase. Beijing will see some clouds moving in but will still be cooler overall.",
"id": "msg-108",
"performance": {
"duration": 2074,
"latency": 2239,
"tps": 54,
"ttft": 165
},
"usage": {
"cost": 0.001071,
"totalInputTokens": 245,
"totalOutputTokens": 112,
"totalTokens": 357
}
}
],
"performance": {
"ttft": 245,
"duration": 3877,
"latency": 4465,
"tps": 50.166666666666664
},
"usage": {
"totalInputTokens": 429,
"totalOutputTokens": 199,
"totalTokens": 628,
"cost": 0.0018839999999999998
}
}
],
"messageMap": {
"msg-101": {
"id": "msg-101",
"role": "user",
"content": "What's the weather like in San Francisco and Beijing today?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
"msg-102": {
"id": "msg-102",
"role": "assistant",
"content": "Let me check the current weather conditions in both cities for you.",
"parentId": "msg-101",
"tools": [
{
"id": "tool-sf",
"identifier": "weather",
"apiName": "getWeather",
"arguments": "{\"city\":\"San Francisco\"}",
"type": "default"
},
{
"id": "tool-bj",
"identifier": "weather",
"apiName": "getWeather",
"arguments": "{\"city\":\"Beijing\"}",
"type": "default"
}
],
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 28,
"totalOutputTokens": 45,
"totalTokens": 73,
"cost": 0.000219,
"tps": 48.5,
"ttft": 245,
"duration": 928,
"latency": 1173
}
},
"msg-103": {
"id": "msg-103",
"role": "tool",
"tool_call_id": "tool-sf",
"content": "{\"location\": \"San Francisco, CA\", \"temperature\": 18, \"conditions\": \"Partly Cloudy\", \"humidity\": 72, \"wind_speed\": 15}",
"pluginState": {
"cached": false,
"executionTime": 234
},
"parentId": "msg-102",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {}
},
"msg-104": {
"id": "msg-104",
"role": "tool",
"tool_call_id": "tool-bj",
"content": "{\"location\": \"Beijing, China\", \"temperature\": 12, \"conditions\": \"Clear\", \"humidity\": 45, \"wind_speed\": 8}",
"pluginState": {
"cached": false,
"executionTime": 189
},
"parentId": "msg-102",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {}
},
"msg-105": {
"id": "msg-105",
"role": "assistant",
"content": "Let me also check the forecast for tomorrow to give you a better comparison.",
"parentId": "msg-103",
"tools": [
{
"id": "tool-sf-forecast",
"identifier": "weather",
"apiName": "getForecast",
"arguments": "{\"city\":\"San Francisco\",\"days\":1}",
"type": "default"
},
{
"id": "tool-bj-forecast",
"identifier": "weather",
"apiName": "getForecast",
"arguments": "{\"city\":\"Beijing\",\"days\":1}",
"type": "default"
}
],
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 156,
"totalOutputTokens": 42,
"totalTokens": 198,
"cost": 0.000594,
"tps": 48,
"ttft": 178,
"duration": 875,
"latency": 1053
}
},
"msg-106": {
"id": "msg-106",
"role": "tool",
"tool_call_id": "tool-sf-forecast",
"content": "{\"location\": \"San Francisco, CA\", \"date\": \"2024-01-02\", \"high\": 20, \"low\": 14, \"conditions\": \"Sunny\"}",
"pluginState": {
"cached": false,
"executionTime": 198
},
"parentId": "msg-105",
"createdAt": 1704067206000,
"updatedAt": 1704067206000,
"meta": {}
},
"msg-107": {
"id": "msg-107",
"role": "tool",
"tool_call_id": "tool-bj-forecast",
"content": "{\"location\": \"Beijing, China\", \"date\": \"2024-01-02\", \"high\": 15, \"low\": 8, \"conditions\": \"Partly Cloudy\"}",
"pluginState": {
"cached": false,
"executionTime": 176
},
"parentId": "msg-105",
"createdAt": 1704067206100,
"updatedAt": 1704067206100,
"meta": {}
},
"msg-108": {
"id": "msg-108",
"role": "assistant",
"content": "Based on both current weather and tomorrow's forecast:\n\n**Today:**\n- San Francisco: 18°C, partly cloudy\n- Beijing: 12°C, clear\n\n**Tomorrow:**\n- San Francisco: High 20°C / Low 14°C, sunny\n- Beijing: High 15°C / Low 8°C, partly cloudy\n\nSan Francisco will have pleasant sunny weather tomorrow with a slight temperature increase. Beijing will see some clouds moving in but will still be cooler overall.",
"parentId": "msg-106",
"createdAt": 1704067208000,
"updatedAt": 1704067208000,
"meta": {},
"metadata": {
"totalInputTokens": 245,
"totalOutputTokens": 112,
"totalTokens": 357,
"cost": 0.001071,
"tps": 54,
"ttft": 165,
"duration": 2074,
"latency": 2239
}
}
}
}

View file

@ -0,0 +1,242 @@
{
"contextTree": [
{
"id": "msg-201",
"type": "message"
},
{
"activeBranchIndex": 1,
"branches": [
[
{
"id": "msg-202",
"type": "message"
},
{
"id": "msg-205",
"type": "message"
},
{
"id": "msg-206",
"type": "message"
},
{
"id": "msg-209",
"type": "message"
},
{
"id": "msg-210",
"type": "message"
}
],
[
{
"id": "msg-203",
"type": "message"
},
{
"id": "msg-207",
"type": "message"
},
{
"id": "msg-208",
"type": "message"
}
],
[
{
"id": "msg-204",
"type": "message"
}
]
],
"id": "branch-msg-201-0",
"parentMessageId": "msg-201",
"type": "branch"
}
],
"flatList": [
{
"id": "msg-201",
"role": "user",
"content": "Write a haiku about programming",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
{
"id": "msg-203",
"role": "assistant",
"content": "Functions compile,\nVariables dance in memory,\nAlgorithm sings.",
"parentId": "msg-201",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 24,
"totalTokens": 36,
"cost": 0.000108
}
},
{
"id": "msg-207",
"role": "user",
"content": "Actually, I prefer the second one with the dancing variables.",
"parentId": "msg-203",
"createdAt": 1704067225000,
"updatedAt": 1704067225000,
"meta": {}
},
{
"id": "msg-208",
"role": "assistant",
"content": "I appreciate your feedback! The imagery of variables dancing in memory really captures the dynamic nature of programming. Would you like me to explore more haikus with that kind of vivid metaphor?",
"parentId": "msg-207",
"createdAt": 1704067227000,
"updatedAt": 1704067227000,
"meta": {},
"metadata": {
"totalInputTokens": 48,
"totalOutputTokens": 52,
"totalTokens": 100,
"cost": 0.0003
}
}
],
"messageMap": {
"msg-201": {
"id": "msg-201",
"role": "user",
"content": "Write a haiku about programming",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
"msg-202": {
"id": "msg-202",
"role": "assistant",
"content": "Code flows like water,\nBugs hide in silent shadows,\nDebug brings the light.",
"parentId": "msg-201",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 28,
"totalTokens": 40,
"cost": 0.00012
}
},
"msg-203": {
"id": "msg-203",
"role": "assistant",
"content": "Functions compile,\nVariables dance in memory,\nAlgorithm sings.",
"parentId": "msg-201",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 24,
"totalTokens": 36,
"cost": 0.000108
}
},
"msg-204": {
"id": "msg-204",
"role": "assistant",
"content": "Lines of logic flow,\nErrors break the peaceful code,\nTests restore the calm.",
"parentId": "msg-201",
"createdAt": 1704067203200,
"updatedAt": 1704067203200,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 26,
"totalTokens": 38,
"cost": 0.000114
}
},
"msg-205": {
"id": "msg-205",
"role": "user",
"content": "I like the first one best!",
"parentId": "msg-202",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
"msg-206": {
"id": "msg-206",
"role": "assistant",
"content": "Thank you! I'm glad you enjoyed it. The metaphor of light revealing hidden bugs is one of my favorites. Would you like me to write more haikus on different programming topics?",
"parentId": "msg-205",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 52,
"totalOutputTokens": 48,
"totalTokens": 100,
"cost": 0.0003
}
},
"msg-207": {
"id": "msg-207",
"role": "user",
"content": "Actually, I prefer the second one with the dancing variables.",
"parentId": "msg-203",
"createdAt": 1704067225000,
"updatedAt": 1704067225000,
"meta": {}
},
"msg-208": {
"id": "msg-208",
"role": "assistant",
"content": "I appreciate your feedback! The imagery of variables dancing in memory really captures the dynamic nature of programming. Would you like me to explore more haikus with that kind of vivid metaphor?",
"parentId": "msg-207",
"createdAt": 1704067227000,
"updatedAt": 1704067227000,
"meta": {},
"metadata": {
"totalInputTokens": 48,
"totalOutputTokens": 52,
"totalTokens": 100,
"cost": 0.0003
}
},
"msg-209": {
"id": "msg-209",
"role": "user",
"content": "Yes, please write one more about debugging!",
"parentId": "msg-206",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {}
},
"msg-210": {
"id": "msg-210",
"role": "assistant",
"content": "Console logs cascade,\nBreakpoints pause the racing code,\nTruth emerges clear.",
"parentId": "msg-209",
"createdAt": 1704067232000,
"updatedAt": 1704067232000,
"meta": {},
"metadata": {
"totalInputTokens": 78,
"totalOutputTokens": 24,
"totalTokens": 102,
"cost": 0.000306
}
}
}
}

View file

@ -0,0 +1,208 @@
{
"contextTree": [
{
"id": "msg-401",
"type": "message"
},
{
"id": "msg-402",
"type": "message"
},
{
"activeBranchIndex": 1,
"branches": [
[
{
"id": "msg-403",
"type": "message"
}
],
[
{
"id": "msg-404",
"type": "message"
},
{
"id": "msg-406",
"type": "message"
},
{
"id": "msg-407",
"type": "message"
}
],
[
{
"id": "msg-405",
"type": "message"
}
]
],
"id": "branch-msg-402-0",
"parentMessageId": "msg-402",
"type": "branch"
}
],
"flatList": [
{
"id": "msg-401",
"role": "user",
"content": "Can you help me write a function to calculate fibonacci?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-402",
"role": "assistant",
"content": "I'll help you write a fibonacci function.",
"parentId": "msg-401",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 15,
"totalOutputTokens": 12,
"totalTokens": 27,
"cost": 0.00008,
"activeBranchIndex": 1
}
},
{
"id": "msg-404",
"role": "assistant",
"content": "Here's an iterative approach which is more efficient:\n\n```python\ndef fibonacci(n):\n if n <= 1:\n return n\n a, b = 0, 1\n for _ in range(2, n + 1):\n a, b = b, a + b\n return b\n```",
"parentId": "msg-402",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 52,
"totalTokens": 82,
"cost": 0.00025
}
},
{
"id": "msg-406",
"role": "user",
"content": "The iterative approach looks good! Can you add some comments?",
"parentId": "msg-404",
"createdAt": 1704067215000,
"updatedAt": 1704067215000,
"meta": {}
},
{
"id": "msg-407",
"role": "assistant",
"content": "Sure! Here's the commented version:\n\n```python\ndef fibonacci(n):\n # Base cases\n if n <= 1:\n return n\n \n # Initialize first two fibonacci numbers\n a, b = 0, 1\n \n # Iterate from 2 to n\n for _ in range(2, n + 1):\n # Calculate next fibonacci number\n a, b = b, a + b\n \n return b\n```",
"parentId": "msg-406",
"createdAt": 1704067217000,
"updatedAt": 1704067217000,
"meta": {},
"metadata": {
"totalInputTokens": 95,
"totalOutputTokens": 78,
"totalTokens": 173,
"cost": 0.00052
}
}
],
"messageMap": {
"msg-401": {
"id": "msg-401",
"role": "user",
"content": "Can you help me write a function to calculate fibonacci?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
"msg-402": {
"id": "msg-402",
"role": "assistant",
"content": "I'll help you write a fibonacci function.",
"parentId": "msg-401",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 15,
"totalOutputTokens": 12,
"totalTokens": 27,
"cost": 0.00008,
"activeBranchIndex": 1
}
},
"msg-403": {
"id": "msg-403",
"role": "assistant",
"content": "Here's a recursive implementation:\n\n```python\ndef fibonacci(n):\n if n <= 1:\n return n\n return fibonacci(n-1) + fibonacci(n-2)\n```",
"parentId": "msg-402",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 45,
"totalTokens": 75,
"cost": 0.00022
}
},
"msg-404": {
"id": "msg-404",
"role": "assistant",
"content": "Here's an iterative approach which is more efficient:\n\n```python\ndef fibonacci(n):\n if n <= 1:\n return n\n a, b = 0, 1\n for _ in range(2, n + 1):\n a, b = b, a + b\n return b\n```",
"parentId": "msg-402",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 52,
"totalTokens": 82,
"cost": 0.00025
}
},
"msg-405": {
"id": "msg-405",
"role": "assistant",
"content": "Let me show you a dynamic programming solution:\n\n```python\ndef fibonacci(n):\n if n <= 1:\n return n\n dp = [0, 1]\n for i in range(2, n + 1):\n dp.append(dp[i-1] + dp[i-2])\n return dp[n]\n```",
"parentId": "msg-402",
"createdAt": 1704067205200,
"updatedAt": 1704067205200,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 58,
"totalTokens": 88,
"cost": 0.00026
}
},
"msg-406": {
"id": "msg-406",
"role": "user",
"content": "The iterative approach looks good! Can you add some comments?",
"parentId": "msg-404",
"createdAt": 1704067215000,
"updatedAt": 1704067215000,
"meta": {}
},
"msg-407": {
"id": "msg-407",
"role": "assistant",
"content": "Sure! Here's the commented version:\n\n```python\ndef fibonacci(n):\n # Base cases\n if n <= 1:\n return n\n \n # Initialize first two fibonacci numbers\n a, b = 0, 1\n \n # Iterate from 2 to n\n for _ in range(2, n + 1):\n # Calculate next fibonacci number\n a, b = b, a + b\n \n return b\n```",
"parentId": "msg-406",
"createdAt": 1704067217000,
"updatedAt": 1704067217000,
"meta": {},
"metadata": {
"totalInputTokens": 95,
"totalOutputTokens": 78,
"totalTokens": 173,
"cost": 0.00052
}
}
}
}

View file

@ -0,0 +1,254 @@
{
"contextTree": [
{
"id": "msg-501",
"type": "message"
},
{
"id": "msg-502",
"type": "message"
},
{
"activeBranchIndex": 1,
"branches": [
[
{
"id": "msg-503",
"type": "message"
},
{
"id": "msg-504",
"type": "message"
},
{
"id": "msg-505",
"type": "message"
},
{
"id": "msg-506",
"type": "message"
}
],
[
{
"id": "msg-507",
"type": "message"
},
{
"id": "msg-508",
"type": "message"
},
{
"id": "msg-509",
"type": "message"
},
{
"id": "msg-510",
"type": "message"
}
]
],
"id": "branch-msg-502-0",
"parentMessageId": "msg-502",
"type": "branch"
}
],
"flatList": [
{
"id": "msg-501",
"role": "user",
"content": "What's the difference between map and forEach in JavaScript?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-502",
"role": "assistant",
"content": "The main difference is that `map()` returns a new array with transformed elements, while `forEach()` just iterates and returns undefined. Map is better for transformations, forEach is better for side effects.",
"parentId": "msg-501",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 18,
"totalOutputTokens": 42,
"totalTokens": 60,
"cost": 0.00018,
"activeBranchIndex": 1
}
},
{
"id": "msg-507",
"role": "user",
"content": "Can you explain the technical implementation details?",
"parentId": "msg-502",
"createdAt": 1704067210100,
"updatedAt": 1704067210100,
"meta": {}
},
{
"id": "msg-508",
"role": "assistant",
"content": "Certainly! From a technical perspective:\n\n**map():**\n- Allocates a new array in memory\n- Iterates through each element\n- Applies the callback function\n- Stores the result in the new array\n- Returns the new array\n\n**forEach():**\n- Iterates through each element\n- Applies the callback function\n- Discards the return value\n- Returns undefined",
"parentId": "msg-507",
"createdAt": 1704067212100,
"updatedAt": 1704067212100,
"meta": {},
"metadata": {
"totalInputTokens": 68,
"totalOutputTokens": 82,
"totalTokens": 150,
"cost": 0.00045
}
},
{
"id": "msg-509",
"role": "user",
"content": "What about memory usage?",
"parentId": "msg-508",
"createdAt": 1704067220100,
"updatedAt": 1704067220100,
"meta": {}
},
{
"id": "msg-510",
"role": "assistant",
"content": "Great question! Map uses more memory because it creates a new array. If you have a large dataset (millions of items) and don't need the result array, forEach is more memory-efficient. However, if you're doing functional programming and need immutability, the memory overhead of map is usually worth it.",
"parentId": "msg-509",
"createdAt": 1704067222100,
"updatedAt": 1704067222100,
"meta": {},
"metadata": {
"totalInputTokens": 160,
"totalOutputTokens": 68,
"totalTokens": 228,
"cost": 0.00068
}
}
],
"messageMap": {
"msg-501": {
"id": "msg-501",
"role": "user",
"content": "What's the difference between map and forEach in JavaScript?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
"msg-502": {
"id": "msg-502",
"role": "assistant",
"content": "The main difference is that `map()` returns a new array with transformed elements, while `forEach()` just iterates and returns undefined. Map is better for transformations, forEach is better for side effects.",
"parentId": "msg-501",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 18,
"totalOutputTokens": 42,
"totalTokens": 60,
"cost": 0.00018,
"activeBranchIndex": 1
}
},
"msg-503": {
"id": "msg-503",
"role": "user",
"content": "Can you show me a practical example?",
"parentId": "msg-502",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
"msg-504": {
"id": "msg-504",
"role": "assistant",
"content": "Sure! Here's a practical example:\n\n```javascript\nconst numbers = [1, 2, 3, 4, 5];\n\n// Using map - creates new array\nconst doubled = numbers.map(n => n * 2);\nconsole.log(doubled); // [2, 4, 6, 8, 10]\n\n// Using forEach - just iterates\nnumbers.forEach(n => console.log(n * 2));\n// Prints: 2, 4, 6, 8, 10\n```",
"parentId": "msg-503",
"createdAt": 1704067212000,
"updatedAt": 1704067212000,
"meta": {},
"metadata": {
"totalInputTokens": 68,
"totalOutputTokens": 95,
"totalTokens": 163,
"cost": 0.00049
}
},
"msg-505": {
"id": "msg-505",
"role": "user",
"content": "Thanks! What about performance differences?",
"parentId": "msg-504",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
"msg-506": {
"id": "msg-506",
"role": "assistant",
"content": "Performance-wise, forEach is slightly faster because it doesn't create a new array. However, the difference is negligible for most use cases. Choose based on functionality: use map when you need the transformed array, use forEach when you just need to iterate.",
"parentId": "msg-505",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 175,
"totalOutputTokens": 58,
"totalTokens": 233,
"cost": 0.0007
}
},
"msg-507": {
"id": "msg-507",
"role": "user",
"content": "Can you explain the technical implementation details?",
"parentId": "msg-502",
"createdAt": 1704067210100,
"updatedAt": 1704067210100,
"meta": {}
},
"msg-508": {
"id": "msg-508",
"role": "assistant",
"content": "Certainly! From a technical perspective:\n\n**map():**\n- Allocates a new array in memory\n- Iterates through each element\n- Applies the callback function\n- Stores the result in the new array\n- Returns the new array\n\n**forEach():**\n- Iterates through each element\n- Applies the callback function\n- Discards the return value\n- Returns undefined",
"parentId": "msg-507",
"createdAt": 1704067212100,
"updatedAt": 1704067212100,
"meta": {},
"metadata": {
"totalInputTokens": 68,
"totalOutputTokens": 82,
"totalTokens": 150,
"cost": 0.00045
}
},
"msg-509": {
"id": "msg-509",
"role": "user",
"content": "What about memory usage?",
"parentId": "msg-508",
"createdAt": 1704067220100,
"updatedAt": 1704067220100,
"meta": {}
},
"msg-510": {
"id": "msg-510",
"role": "assistant",
"content": "Great question! Map uses more memory because it creates a new array. If you have a large dataset (millions of items) and don't need the result array, forEach is more memory-efficient. However, if you're doing functional programming and need immutability, the memory overhead of map is usually worth it.",
"parentId": "msg-509",
"createdAt": 1704067222100,
"updatedAt": 1704067222100,
"meta": {},
"metadata": {
"totalInputTokens": 160,
"totalOutputTokens": 68,
"totalTokens": 228,
"cost": 0.00068
}
}
}
}

View file

@ -0,0 +1,260 @@
{
"contextTree": [
{
"id": "msg-201",
"type": "message"
},
{
"activeBranchIndex": 0,
"branches": [
[
{
"id": "msg-202",
"type": "message"
},
{
"id": "msg-205",
"type": "message"
},
{
"id": "msg-206",
"type": "message"
},
{
"id": "msg-209",
"type": "message"
},
{
"id": "msg-210",
"type": "message"
}
],
[
{
"id": "msg-203",
"type": "message"
},
{
"id": "msg-207",
"type": "message"
},
{
"id": "msg-208",
"type": "message"
}
],
[
{
"id": "msg-204",
"type": "message"
}
]
],
"id": "branch-msg-201-0",
"parentMessageId": "msg-201",
"type": "branch"
}
],
"flatList": [
{
"id": "msg-201",
"role": "user",
"content": "Write a haiku about programming",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-202",
"role": "assistant",
"content": "Code flows like water,\nBugs hide in silent shadows,\nDebug brings the light.",
"parentId": "msg-201",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 28,
"totalTokens": 40,
"cost": 0.00012
}
},
{
"id": "msg-205",
"role": "user",
"content": "I like the first one best!",
"parentId": "msg-202",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-206",
"role": "assistant",
"content": "Thank you! I'm glad you enjoyed it. The metaphor of light revealing hidden bugs is one of my favorites. Would you like me to write more haikus on different programming topics?",
"parentId": "msg-205",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 52,
"totalOutputTokens": 48,
"totalTokens": 100,
"cost": 0.0003
}
},
{
"id": "msg-209",
"role": "user",
"content": "Yes, please write one more about debugging!",
"parentId": "msg-206",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {}
},
{
"id": "msg-210",
"role": "assistant",
"content": "Console logs cascade,\nBreakpoints pause the racing code,\nTruth emerges clear.",
"parentId": "msg-209",
"createdAt": 1704067232000,
"updatedAt": 1704067232000,
"meta": {},
"metadata": {
"totalInputTokens": 78,
"totalOutputTokens": 24,
"totalTokens": 102,
"cost": 0.000306
}
}
],
"messageMap": {
"msg-201": {
"id": "msg-201",
"role": "user",
"content": "Write a haiku about programming",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
"msg-202": {
"id": "msg-202",
"role": "assistant",
"content": "Code flows like water,\nBugs hide in silent shadows,\nDebug brings the light.",
"parentId": "msg-201",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 28,
"totalTokens": 40,
"cost": 0.00012
}
},
"msg-203": {
"id": "msg-203",
"role": "assistant",
"content": "Functions compile,\nVariables dance in memory,\nAlgorithm sings.",
"parentId": "msg-201",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 24,
"totalTokens": 36,
"cost": 0.000108
}
},
"msg-204": {
"id": "msg-204",
"role": "assistant",
"content": "Lines of logic flow,\nErrors break the peaceful code,\nTests restore the calm.",
"parentId": "msg-201",
"createdAt": 1704067203200,
"updatedAt": 1704067203200,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 26,
"totalTokens": 38,
"cost": 0.000114
}
},
"msg-205": {
"id": "msg-205",
"role": "user",
"content": "I like the first one best!",
"parentId": "msg-202",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
"msg-206": {
"id": "msg-206",
"role": "assistant",
"content": "Thank you! I'm glad you enjoyed it. The metaphor of light revealing hidden bugs is one of my favorites. Would you like me to write more haikus on different programming topics?",
"parentId": "msg-205",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 52,
"totalOutputTokens": 48,
"totalTokens": 100,
"cost": 0.0003
}
},
"msg-207": {
"id": "msg-207",
"role": "user",
"content": "Actually, I prefer the second one with the dancing variables.",
"parentId": "msg-203",
"createdAt": 1704067225000,
"updatedAt": 1704067225000,
"meta": {}
},
"msg-208": {
"id": "msg-208",
"role": "assistant",
"content": "I appreciate your feedback! The imagery of variables dancing in memory really captures the dynamic nature of programming. Would you like me to explore more haikus with that kind of vivid metaphor?",
"parentId": "msg-207",
"createdAt": 1704067227000,
"updatedAt": 1704067227000,
"meta": {},
"metadata": {
"totalInputTokens": 48,
"totalOutputTokens": 52,
"totalTokens": 100,
"cost": 0.0003
}
},
"msg-209": {
"id": "msg-209",
"role": "user",
"content": "Yes, please write one more about debugging!",
"parentId": "msg-206",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {}
},
"msg-210": {
"id": "msg-210",
"role": "assistant",
"content": "Console logs cascade,\nBreakpoints pause the racing code,\nTruth emerges clear.",
"parentId": "msg-209",
"createdAt": 1704067232000,
"updatedAt": 1704067232000,
"meta": {},
"metadata": {
"totalInputTokens": 78,
"totalOutputTokens": 24,
"totalTokens": 102,
"cost": 0.000306
}
}
}
}

View file

@ -0,0 +1,14 @@
import type { SerializedParseResult } from '../..';
import activeIndex1 from './active-index-1.json';
import assistantBranch from './assistant-branch.json';
import assistantUserBranch from './assistant-user-branch.json';
import conversation from './conversation.json';
import nested from './nested.json';
export const branch = {
activeIndex1: activeIndex1 as unknown as SerializedParseResult,
assistantBranch: assistantBranch as unknown as SerializedParseResult,
assistantUserBranch: assistantUserBranch as unknown as SerializedParseResult,
conversation: conversation as unknown as SerializedParseResult,
nested: nested as unknown as SerializedParseResult,
};

View file

@ -0,0 +1,389 @@
{
"contextTree": [
{
"id": "msg-301",
"type": "message"
},
{
"activeBranchIndex": 0,
"branches": [
[
{
"id": "msg-302",
"type": "message"
},
{
"id": "msg-305",
"type": "message"
},
{
"activeBranchIndex": 1,
"branches": [
[
{
"id": "msg-306",
"type": "message"
}
],
[
{
"id": "msg-307",
"type": "message"
},
{
"id": "msg-310",
"type": "message"
},
{
"activeBranchIndex": 0,
"branches": [
[
{
"id": "msg-311",
"type": "message"
},
{
"id": "msg-314",
"type": "message"
},
{
"activeBranchIndex": 1,
"branches": [
[
{
"id": "msg-315",
"type": "message"
}
],
[
{
"id": "msg-316",
"type": "message"
}
]
],
"id": "branch-msg-314-0",
"parentMessageId": "msg-314",
"type": "branch"
}
],
[
{
"id": "msg-312",
"type": "message"
}
]
],
"id": "branch-msg-310-1",
"parentMessageId": "msg-310",
"type": "branch"
}
]
],
"id": "branch-msg-305-2",
"parentMessageId": "msg-305",
"type": "branch"
}
],
[
{
"id": "msg-303",
"type": "message"
}
],
[
{
"id": "msg-304",
"type": "message"
}
]
],
"id": "branch-msg-301-3",
"parentMessageId": "msg-301",
"type": "branch"
}
],
"flatList": [
{
"id": "msg-301",
"role": "user",
"content": "What's your favorite programming paradigm?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-302",
"role": "assistant",
"content": "I appreciate functional programming for its elegance.",
"parentId": "msg-301",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 10,
"totalOutputTokens": 15,
"totalTokens": 25,
"cost": 0.00008
}
},
{
"id": "msg-305",
"role": "user",
"content": "Can you explain more about functional programming?",
"parentId": "msg-302",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
{
"id": "msg-307",
"role": "assistant",
"content": "In functional programming, functions are first-class citizens and side effects are minimized.",
"parentId": "msg-305",
"createdAt": 1704067212100,
"updatedAt": 1704067212100,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 22,
"totalTokens": 52,
"cost": 0.00016
}
},
{
"id": "msg-310",
"role": "user",
"content": "What are some benefits of immutability?",
"parentId": "msg-307",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-311",
"role": "assistant",
"content": "Immutability prevents bugs from unexpected data changes.",
"parentId": "msg-310",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 55,
"totalOutputTokens": 16,
"totalTokens": 71,
"cost": 0.00021
}
},
{
"id": "msg-314",
"role": "user",
"content": "Can you give me an example?",
"parentId": "msg-311",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
{
"id": "msg-316",
"role": "assistant",
"content": "Here's a better example: Instead of mutating an array with push(), functional programming uses methods like concat() or the spread operator to create new arrays.",
"parentId": "msg-314",
"createdAt": 1704067232100,
"updatedAt": 1704067232100,
"meta": {},
"metadata": {
"totalInputTokens": 75,
"totalOutputTokens": 42,
"totalTokens": 117,
"cost": 0.00035
}
}
],
"messageMap": {
"msg-301": {
"id": "msg-301",
"role": "user",
"content": "What's your favorite programming paradigm?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
"msg-302": {
"id": "msg-302",
"role": "assistant",
"content": "I appreciate functional programming for its elegance.",
"parentId": "msg-301",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"metadata": {
"totalInputTokens": 10,
"totalOutputTokens": 15,
"totalTokens": 25,
"cost": 0.00008
}
},
"msg-303": {
"id": "msg-303",
"role": "assistant",
"content": "Object-oriented programming offers great structure.",
"parentId": "msg-301",
"createdAt": 1704067202100,
"updatedAt": 1704067202100,
"meta": {},
"metadata": {
"totalInputTokens": 10,
"totalOutputTokens": 12,
"totalTokens": 22,
"cost": 0.00007
}
},
"msg-304": {
"id": "msg-304",
"role": "assistant",
"content": "Procedural programming is straightforward and efficient.",
"parentId": "msg-301",
"createdAt": 1704067202200,
"updatedAt": 1704067202200,
"meta": {},
"metadata": {
"totalInputTokens": 10,
"totalOutputTokens": 13,
"totalTokens": 23,
"cost": 0.000069
}
},
"msg-305": {
"id": "msg-305",
"role": "user",
"content": "Can you explain more about functional programming?",
"parentId": "msg-302",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
"msg-306": {
"id": "msg-306",
"role": "assistant",
"content": "Functional programming focuses on pure functions and immutability.",
"parentId": "msg-305",
"createdAt": 1704067212000,
"updatedAt": 1704067212000,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 18,
"totalTokens": 48,
"cost": 0.00014
}
},
"msg-307": {
"id": "msg-307",
"role": "assistant",
"content": "In functional programming, functions are first-class citizens and side effects are minimized.",
"parentId": "msg-305",
"createdAt": 1704067212100,
"updatedAt": 1704067212100,
"meta": {},
"metadata": {
"totalInputTokens": 30,
"totalOutputTokens": 22,
"totalTokens": 52,
"cost": 0.00016
}
},
"msg-310": {
"id": "msg-310",
"role": "user",
"content": "What are some benefits of immutability?",
"parentId": "msg-307",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
"msg-311": {
"id": "msg-311",
"role": "assistant",
"content": "Immutability prevents bugs from unexpected data changes.",
"parentId": "msg-310",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 55,
"totalOutputTokens": 16,
"totalTokens": 71,
"cost": 0.00021
}
},
"msg-312": {
"id": "msg-312",
"role": "assistant",
"content": "It makes code easier to reason about and test.",
"parentId": "msg-310",
"createdAt": 1704067222100,
"updatedAt": 1704067222100,
"meta": {},
"metadata": {
"totalInputTokens": 55,
"totalOutputTokens": 14,
"totalTokens": 69,
"cost": 0.0002
}
},
"msg-314": {
"id": "msg-314",
"role": "user",
"content": "Can you give me an example?",
"parentId": "msg-311",
"createdAt": 1704067230000,
"updatedAt": 1704067230000,
"meta": {},
"metadata": {
"activeBranchIndex": 1
}
},
"msg-315": {
"id": "msg-315",
"role": "assistant",
"content": "Sure! In JavaScript, using `const` with arrays means you can't reassign the variable, but you can still modify the array with push/pop.",
"parentId": "msg-314",
"createdAt": 1704067232000,
"updatedAt": 1704067232000,
"meta": {},
"metadata": {
"totalInputTokens": 75,
"totalOutputTokens": 35,
"totalTokens": 110,
"cost": 0.00033
}
},
"msg-316": {
"id": "msg-316",
"role": "assistant",
"content": "Here's a better example: Instead of mutating an array with push(), functional programming uses methods like concat() or the spread operator to create new arrays.",
"parentId": "msg-314",
"createdAt": 1704067232100,
"updatedAt": 1704067232100,
"meta": {},
"metadata": {
"totalInputTokens": 75,
"totalOutputTokens": 42,
"totalTokens": 117,
"cost": 0.00035
}
}
}
}

View file

@ -0,0 +1,8 @@
import type { SerializedParseResult } from '../..';
import simple from './simple.json';
import withTools from './with-tools.json';
export const compare = {
simple: simple as unknown as SerializedParseResult,
withTools: withTools as unknown as SerializedParseResult,
};

View file

@ -0,0 +1,224 @@
{
"contextTree": [
{
"id": "msg-601",
"type": "message"
},
{
"activeColumnId": "msg-603",
"columns": [
[
{
"id": "msg-602",
"type": "message"
}
],
[
{
"id": "msg-603",
"type": "message"
}
]
],
"id": "compare-msg-601-msg-602-msg-603",
"messageId": "msg-601",
"type": "compare"
},
{
"id": "msg-604",
"type": "message"
},
{
"id": "msg-605",
"type": "message"
}
],
"flatList": [
{
"id": "msg-601",
"role": "user",
"content": "Should I use REST or GraphQL for my API?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"compare": true
}
},
{
"activeColumnId": "msg-603",
"columns": [
[
{
"id": "msg-602",
"role": "assistant",
"content": "**REST is a great choice:**\n\nREST is simple, well-understood, and has excellent caching support. It's perfect for:\n\n- CRUD operations\n- Public APIs\n- Mobile apps with limited bandwidth\n- Teams familiar with HTTP conventions\n\n**Advantages:**\n- HTTP caching works out of the box\n- Stateless architecture\n- Wide tooling support\n- Easy to learn\n\n**Trade-offs:**\n- Over-fetching/under-fetching data\n- Multiple endpoints to maintain\n- Versioning can be complex",
"parentId": "msg-601",
"model": "gpt-4",
"provider": "openai",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 145,
"totalTokens": 157,
"cost": 0.00471,
"tps": 42,
"ttft": 387,
"duration": 3452,
"latency": 3839
}
}
],
[
{
"id": "msg-603",
"role": "assistant",
"content": "**GraphQL offers more flexibility:**\n\nGraphQL provides a powerful query language that lets clients request exactly what they need. It's ideal for:\n\n- Complex data relationships\n- Multiple client types (web, mobile, desktop)\n- Rapidly evolving requirements\n- Real-time applications\n\n**Advantages:**\n- Single endpoint\n- No over-fetching\n- Strong typing with schema\n- Powerful developer tools\n\n**Trade-offs:**\n- Steeper learning curve\n- More complex server setup\n- Caching requires extra effort\n- Can be overkill for simple APIs",
"parentId": "msg-601",
"model": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 158,
"totalTokens": 170,
"cost": 0.00085,
"tps": 51,
"ttft": 298,
"duration": 3098,
"latency": 3396,
"activeColumn": true
}
}
]
],
"content": "",
"createdAt": 1704067205000,
"extra": {
"parentMessageId": "msg-601"
},
"id": "compare-msg-601-msg-602-msg-603",
"meta": {},
"role": "compare",
"updatedAt": 1704067205100
},
{
"id": "msg-604",
"role": "user",
"content": "What about performance differences?",
"parentId": "msg-603",
"createdAt": 1704067215000,
"updatedAt": 1704067215000,
"meta": {}
},
{
"id": "msg-605",
"role": "assistant",
"content": "Performance-wise, both can be highly performant when implemented correctly:\n\n**REST Performance:**\n- Leverages HTTP caching (CDNs, browser cache)\n- Simpler server logic = faster response times\n- Can over-fetch data, wasting bandwidth\n\n**GraphQL Performance:**\n- Eliminates over-fetching, reducing payload size\n- Single request vs. multiple REST calls\n- Requires query complexity analysis to prevent abuse\n- DataLoader pattern helps with N+1 queries\n\n**Recommendation:** For your use case, if you're building a simple CRUD API, REST will likely perform better with less effort. If you need flexible data fetching across multiple clients, GraphQL's efficiency gains will outweigh the setup complexity.",
"parentId": "msg-604",
"createdAt": 1704067217000,
"updatedAt": 1704067217000,
"meta": {},
"metadata": {
"totalInputTokens": 182,
"totalOutputTokens": 167,
"totalTokens": 349,
"cost": 0.001745,
"tps": 48,
"ttft": 345,
"duration": 3479,
"latency": 3824
}
}
],
"messageMap": {
"msg-601": {
"id": "msg-601",
"role": "user",
"content": "Should I use REST or GraphQL for my API?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"compare": true
}
},
"msg-602": {
"id": "msg-602",
"role": "assistant",
"content": "**REST is a great choice:**\n\nREST is simple, well-understood, and has excellent caching support. It's perfect for:\n\n- CRUD operations\n- Public APIs\n- Mobile apps with limited bandwidth\n- Teams familiar with HTTP conventions\n\n**Advantages:**\n- HTTP caching works out of the box\n- Stateless architecture\n- Wide tooling support\n- Easy to learn\n\n**Trade-offs:**\n- Over-fetching/under-fetching data\n- Multiple endpoints to maintain\n- Versioning can be complex",
"parentId": "msg-601",
"model": "gpt-4",
"provider": "openai",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 145,
"totalTokens": 157,
"cost": 0.00471,
"tps": 42,
"ttft": 387,
"duration": 3452,
"latency": 3839
}
},
"msg-603": {
"id": "msg-603",
"role": "assistant",
"content": "**GraphQL offers more flexibility:**\n\nGraphQL provides a powerful query language that lets clients request exactly what they need. It's ideal for:\n\n- Complex data relationships\n- Multiple client types (web, mobile, desktop)\n- Rapidly evolving requirements\n- Real-time applications\n\n**Advantages:**\n- Single endpoint\n- No over-fetching\n- Strong typing with schema\n- Powerful developer tools\n\n**Trade-offs:**\n- Steeper learning curve\n- More complex server setup\n- Caching requires extra effort\n- Can be overkill for simple APIs",
"parentId": "msg-601",
"model": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 12,
"totalOutputTokens": 158,
"totalTokens": 170,
"cost": 0.00085,
"tps": 51,
"ttft": 298,
"duration": 3098,
"latency": 3396,
"activeColumn": true
}
},
"msg-604": {
"id": "msg-604",
"role": "user",
"content": "What about performance differences?",
"parentId": "msg-603",
"createdAt": 1704067215000,
"updatedAt": 1704067215000,
"meta": {}
},
"msg-605": {
"id": "msg-605",
"role": "assistant",
"content": "Performance-wise, both can be highly performant when implemented correctly:\n\n**REST Performance:**\n- Leverages HTTP caching (CDNs, browser cache)\n- Simpler server logic = faster response times\n- Can over-fetch data, wasting bandwidth\n\n**GraphQL Performance:**\n- Eliminates over-fetching, reducing payload size\n- Single request vs. multiple REST calls\n- Requires query complexity analysis to prevent abuse\n- DataLoader pattern helps with N+1 queries\n\n**Recommendation:** For your use case, if you're building a simple CRUD API, REST will likely perform better with less effort. If you need flexible data fetching across multiple clients, GraphQL's efficiency gains will outweigh the setup complexity.",
"parentId": "msg-604",
"createdAt": 1704067217000,
"updatedAt": 1704067217000,
"meta": {},
"metadata": {
"totalInputTokens": 182,
"totalOutputTokens": 167,
"totalTokens": 349,
"cost": 0.001745,
"tps": 48,
"ttft": 345,
"duration": 3479,
"latency": 3824
}
}
}
}

View file

@ -0,0 +1,418 @@
{
"contextTree": [
{
"id": "msg-301",
"type": "message"
},
{
"activeColumnId": "msg-303",
"columns": [
[
{
"id": "msg-302",
"type": "message"
}
],
[
{
"id": "msg-303",
"type": "message"
}
],
[
{
"children": [
{
"id": "msg-304",
"type": "message",
"tools": ["msg-305", "msg-306"]
},
{
"id": "msg-307",
"type": "message"
}
],
"id": "msg-304",
"type": "assistantGroup"
}
]
],
"id": "compare-msg-301-msg-302-msg-303-msg-304",
"messageId": "msg-301",
"type": "compare"
},
{
"id": "msg-308",
"type": "message"
},
{
"id": "msg-309",
"type": "message"
}
],
"flatList": [
{
"id": "msg-301",
"role": "user",
"content": "Compare the performance of bubble sort vs quicksort",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"compare": true
}
},
{
"activeColumnId": "msg-303",
"columns": [
[
{
"id": "msg-302",
"role": "assistant",
"content": "**Bubble Sort Analysis:**\n\n```python\ndef bubble_sort(arr):\n n = len(arr)\n for i in range(n):\n for j in range(0, n-i-1):\n if arr[j] > arr[j+1]:\n arr[j], arr[j+1] = arr[j+1], arr[j]\n return arr\n```\n\n**Time Complexity:**\n- Best Case: O(n)\n- Average Case: O(n²)\n- Worst Case: O(n²)\n\n**Space Complexity:** O(1)\n\n**Pros:**\n- Simple to implement\n- No extra memory needed\n- Stable sorting algorithm\n\n**Cons:**\n- Very slow for large datasets\n- Not practical for real-world use",
"parentId": "msg-301",
"model": "gpt-4",
"provider": "openai",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 198,
"totalTokens": 222,
"cost": 0.00666,
"tps": 45,
"ttft": 456,
"duration": 4400,
"latency": 4856
}
}
],
[
{
"id": "msg-303",
"role": "assistant",
"content": "**QuickSort Analysis:**\n\n```python\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[len(arr) // 2]\n left = [x for x in arr if x < pivot]\n middle = [x for x in arr if x == pivot]\n right = [x for x in arr if x > pivot]\n return quicksort(left) + middle + quicksort(right)\n```\n\n**Time Complexity:**\n- Best Case: O(n log n)\n- Average Case: O(n log n)\n- Worst Case: O(n²)\n\n**Space Complexity:** O(log n)\n\n**Pros:**\n- Very fast in practice\n- Good cache locality\n- Industry standard\n\n**Cons:**\n- Not stable by default\n- Worst case is rare but possible\n- Uses recursion",
"parentId": "msg-301",
"model": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 215,
"totalTokens": 239,
"cost": 0.001195,
"tps": 58,
"ttft": 312,
"duration": 3707,
"latency": 4019,
"activeColumn": true
}
}
],
[
{
"id": "msg-304",
"role": "assistantGroup",
"content": "",
"parentId": "msg-301",
"model": "gpt-4",
"provider": "openai",
"createdAt": 1704067205200,
"updatedAt": 1704067205200,
"meta": {},
"children": [
{
"content": "Let me run a benchmark test to compare these two sorting algorithms with real data.",
"id": "msg-304",
"performance": {
"duration": 730,
"latency": 928,
"tps": 52,
"ttft": 198
},
"tools": [
{
"id": "tool-benchmark-bubble",
"identifier": "code-runner",
"apiName": "runBenchmark",
"arguments": "{\"algorithm\":\"bubble_sort\",\"size\":1000}",
"type": "default",
"result": {
"content": "{\"algorithm\":\"bubble_sort\",\"arraySize\":1000,\"executionTime\":\"2.34s\",\"comparisons\":499500,\"swaps\":250000}",
"id": "msg-305",
"state": {
"cached": false,
"executionTime": 2340
}
},
"result_msg_id": "msg-305"
},
{
"id": "tool-benchmark-quick",
"identifier": "code-runner",
"apiName": "runBenchmark",
"arguments": "{\"algorithm\":\"quicksort\",\"size\":1000}",
"type": "default",
"result": {
"content": "{\"algorithm\":\"quicksort\",\"arraySize\":1000,\"executionTime\":\"0.012s\",\"comparisons\":9976,\"recursionDepth\":12}",
"id": "msg-306",
"state": {
"cached": false,
"executionTime": 12
}
},
"result_msg_id": "msg-306"
}
],
"usage": {
"cost": 0.000186,
"totalInputTokens": 24,
"totalOutputTokens": 38,
"totalTokens": 62
}
},
{
"content": "**Benchmark Results (Array Size: 1000):**\n\n**Bubble Sort:**\n- Execution Time: 2.34 seconds\n- Comparisons: 499,500\n- Swaps: 250,000\n\n**QuickSort:**\n- Execution Time: 0.012 seconds\n- Comparisons: 9,976\n- Recursion Depth: 12\n\n**Conclusion:** QuickSort is approximately **195x faster** than Bubble Sort for an array of 1000 elements. This demonstrates why QuickSort is the industry standard for general-purpose sorting.",
"id": "msg-307",
"performance": {
"duration": 2791,
"latency": 3025,
"tps": 48,
"ttft": 234
},
"usage": {
"cost": 0.00087,
"totalInputTokens": 156,
"totalOutputTokens": 134,
"totalTokens": 290
}
}
],
"performance": {
"ttft": 198,
"duration": 3521,
"latency": 3953,
"tps": 50
},
"usage": {
"totalInputTokens": 180,
"totalOutputTokens": 172,
"totalTokens": 352,
"cost": 0.001056
}
}
]
],
"content": "",
"createdAt": 1704067205000,
"extra": {
"parentMessageId": "msg-301"
},
"id": "compare-msg-301-msg-302-msg-303-msg-304",
"meta": {},
"role": "compare",
"updatedAt": 1704067205200
},
{
"id": "msg-308",
"role": "user",
"content": "Great analysis! Can you explain why QuickSort has better cache locality?",
"parentId": "msg-303",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
{
"id": "msg-309",
"role": "assistant",
"content": "Excellent question! QuickSort has better cache locality because:\n\n1. **Sequential Access**: QuickSort processes array elements sequentially during partitioning, which means the CPU can prefetch nearby memory locations into cache.\n\n2. **In-place Partitioning**: Elements are rearranged within the same array, keeping data in the same memory region.\n\n3. **Smaller Working Set**: As QuickSort divides the array, it works on smaller subarrays that fit better in CPU cache.\n\n4. **Cache Lines**: Modern CPUs load data in cache lines (typically 64 bytes). QuickSort's sequential access pattern means when you access one element, nearby elements are already cached.\n\nIn contrast, some sorting algorithms like merge sort need to copy data to auxiliary arrays, causing more cache misses.",
"parentId": "msg-308",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 268,
"totalOutputTokens": 176,
"totalTokens": 444,
"cost": 0.00222,
"tps": 52,
"ttft": 312,
"duration": 3384,
"latency": 3696
}
}
],
"messageMap": {
"msg-301": {
"id": "msg-301",
"role": "user",
"content": "Compare the performance of bubble sort vs quicksort",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {},
"metadata": {
"compare": true
}
},
"msg-302": {
"id": "msg-302",
"role": "assistant",
"content": "**Bubble Sort Analysis:**\n\n```python\ndef bubble_sort(arr):\n n = len(arr)\n for i in range(n):\n for j in range(0, n-i-1):\n if arr[j] > arr[j+1]:\n arr[j], arr[j+1] = arr[j+1], arr[j]\n return arr\n```\n\n**Time Complexity:**\n- Best Case: O(n)\n- Average Case: O(n²)\n- Worst Case: O(n²)\n\n**Space Complexity:** O(1)\n\n**Pros:**\n- Simple to implement\n- No extra memory needed\n- Stable sorting algorithm\n\n**Cons:**\n- Very slow for large datasets\n- Not practical for real-world use",
"parentId": "msg-301",
"model": "gpt-4",
"provider": "openai",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 198,
"totalTokens": 222,
"cost": 0.00666,
"tps": 45,
"ttft": 456,
"duration": 4400,
"latency": 4856
}
},
"msg-303": {
"id": "msg-303",
"role": "assistant",
"content": "**QuickSort Analysis:**\n\n```python\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[len(arr) // 2]\n left = [x for x in arr if x < pivot]\n middle = [x for x in arr if x == pivot]\n right = [x for x in arr if x > pivot]\n return quicksort(left) + middle + quicksort(right)\n```\n\n**Time Complexity:**\n- Best Case: O(n log n)\n- Average Case: O(n log n)\n- Worst Case: O(n²)\n\n**Space Complexity:** O(log n)\n\n**Pros:**\n- Very fast in practice\n- Good cache locality\n- Industry standard\n\n**Cons:**\n- Not stable by default\n- Worst case is rare but possible\n- Uses recursion",
"parentId": "msg-301",
"model": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"createdAt": 1704067205100,
"updatedAt": 1704067205100,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 215,
"totalTokens": 239,
"cost": 0.001195,
"tps": 58,
"ttft": 312,
"duration": 3707,
"latency": 4019,
"activeColumn": true
}
},
"msg-304": {
"id": "msg-304",
"role": "assistant",
"content": "Let me run a benchmark test to compare these two sorting algorithms with real data.",
"parentId": "msg-301",
"model": "gpt-4",
"provider": "openai",
"tools": [
{
"id": "tool-benchmark-bubble",
"identifier": "code-runner",
"apiName": "runBenchmark",
"arguments": "{\"algorithm\":\"bubble_sort\",\"size\":1000}",
"type": "default"
},
{
"id": "tool-benchmark-quick",
"identifier": "code-runner",
"apiName": "runBenchmark",
"arguments": "{\"algorithm\":\"quicksort\",\"size\":1000}",
"type": "default"
}
],
"createdAt": 1704067205200,
"updatedAt": 1704067205200,
"meta": {},
"metadata": {
"totalInputTokens": 24,
"totalOutputTokens": 38,
"totalTokens": 62,
"cost": 0.000186,
"tps": 52,
"ttft": 198,
"duration": 730,
"latency": 928
}
},
"msg-305": {
"id": "msg-305",
"role": "tool",
"tool_call_id": "tool-benchmark-bubble",
"content": "{\"algorithm\":\"bubble_sort\",\"arraySize\":1000,\"executionTime\":\"2.34s\",\"comparisons\":499500,\"swaps\":250000}",
"pluginState": {
"cached": false,
"executionTime": 2340
},
"parentId": "msg-304",
"createdAt": 1704067207540,
"updatedAt": 1704067207540,
"meta": {}
},
"msg-306": {
"id": "msg-306",
"role": "tool",
"tool_call_id": "tool-benchmark-quick",
"content": "{\"algorithm\":\"quicksort\",\"arraySize\":1000,\"executionTime\":\"0.012s\",\"comparisons\":9976,\"recursionDepth\":12}",
"pluginState": {
"cached": false,
"executionTime": 12
},
"parentId": "msg-304",
"createdAt": 1704067207552,
"updatedAt": 1704067207552,
"meta": {}
},
"msg-307": {
"id": "msg-307",
"role": "assistant",
"content": "**Benchmark Results (Array Size: 1000):**\n\n**Bubble Sort:**\n- Execution Time: 2.34 seconds\n- Comparisons: 499,500\n- Swaps: 250,000\n\n**QuickSort:**\n- Execution Time: 0.012 seconds\n- Comparisons: 9,976\n- Recursion Depth: 12\n\n**Conclusion:** QuickSort is approximately **195x faster** than Bubble Sort for an array of 1000 elements. This demonstrates why QuickSort is the industry standard for general-purpose sorting.",
"parentId": "msg-306",
"createdAt": 1704067208000,
"updatedAt": 1704067208000,
"meta": {},
"metadata": {
"totalInputTokens": 156,
"totalOutputTokens": 134,
"totalTokens": 290,
"cost": 0.00087,
"tps": 48,
"ttft": 234,
"duration": 2791,
"latency": 3025
}
},
"msg-308": {
"id": "msg-308",
"role": "user",
"content": "Great analysis! Can you explain why QuickSort has better cache locality?",
"parentId": "msg-303",
"createdAt": 1704067220000,
"updatedAt": 1704067220000,
"meta": {}
},
"msg-309": {
"id": "msg-309",
"role": "assistant",
"content": "Excellent question! QuickSort has better cache locality because:\n\n1. **Sequential Access**: QuickSort processes array elements sequentially during partitioning, which means the CPU can prefetch nearby memory locations into cache.\n\n2. **In-place Partitioning**: Elements are rearranged within the same array, keeping data in the same memory region.\n\n3. **Smaller Working Set**: As QuickSort divides the array, it works on smaller subarrays that fit better in CPU cache.\n\n4. **Cache Lines**: Modern CPUs load data in cache lines (typically 64 bytes). QuickSort's sequential access pattern means when you access one element, nearby elements are already cached.\n\nIn contrast, some sorting algorithms like merge sort need to copy data to auxiliary arrays, causing more cache misses.",
"parentId": "msg-308",
"createdAt": 1704067222000,
"updatedAt": 1704067222000,
"meta": {},
"metadata": {
"totalInputTokens": 268,
"totalOutputTokens": 176,
"totalTokens": 444,
"cost": 0.00222,
"tps": 52,
"ttft": 312,
"duration": 3384,
"latency": 3696
}
}
}
}

View file

@ -0,0 +1,239 @@
{
"contextTree": [
{
"id": "msg-501",
"type": "message"
},
{
"children": [
{
"id": "msg-502",
"type": "message",
"tools": ["msg-503", "msg-504"]
}
],
"id": "msg-502",
"type": "assistantGroup"
},
{
"id": "msg-505",
"type": "message"
},
{
"activeBranchIndex": 0,
"branches": [
[
{
"id": "msg-506",
"type": "message"
}
],
[
{
"id": "msg-507",
"type": "message"
}
]
],
"id": "branch-msg-505-0",
"parentMessageId": "msg-505",
"type": "branch"
}
],
"flatList": [
{
"id": "msg-501",
"role": "user",
"content": "Help me debug this Python code that's throwing an error",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-502",
"role": "assistantGroup",
"content": "",
"parentId": "msg-501",
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"children": [
{
"content": "I'll help you debug the code. Let me first check the syntax and run some analysis.",
"id": "msg-502",
"tools": [
{
"id": "tool-lint",
"identifier": "code-analyzer",
"apiName": "analyzePython",
"arguments": "{\"action\":\"lint\"}",
"type": "default",
"result": {
"content": "{\"status\":\"success\",\"issues\":[{\"line\":15,\"message\":\"undefined variable 'result'\"}]}",
"id": "msg-503",
"state": {
"cached": false
}
},
"result_msg_id": "msg-503"
},
{
"id": "tool-test",
"identifier": "code-analyzer",
"apiName": "runTests",
"arguments": "{\"action\":\"test\"}",
"type": "default",
"result": {
"content": "{\"status\":\"failed\",\"error\":\"NameError: name 'result' is not defined\"}",
"id": "msg-504",
"state": {
"cached": false
}
},
"result_msg_id": "msg-504"
}
]
}
]
},
{
"id": "msg-505",
"role": "user",
"content": "Here's the code:\n```python\ndef calculate():\n x = 5\n y = 10\n print(result)\n```",
"parentId": "msg-504",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
{
"id": "msg-506",
"role": "assistant",
"content": "I found the issue! On line 15, you're trying to use `result` but it's never defined. Here's the fix:\n\n```python\ndef calculate():\n x = 5\n y = 10\n result = x + y # Define result before using it\n print(result)\n```",
"parentId": "msg-505",
"createdAt": 1704067213000,
"updatedAt": 1704067213000,
"meta": {},
"usage": {
"totalInputTokens": 156,
"totalOutputTokens": 89,
"totalTokens": 245,
"cost": 0.000735
}
}
],
"messageMap": {
"msg-501": {
"id": "msg-501",
"role": "user",
"content": "Help me debug this Python code that's throwing an error",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
"msg-502": {
"id": "msg-502",
"role": "assistant",
"content": "I'll help you debug the code. Let me first check the syntax and run some analysis.",
"parentId": "msg-501",
"tools": [
{
"id": "tool-lint",
"identifier": "code-analyzer",
"apiName": "analyzePython",
"arguments": "{\"action\":\"lint\"}",
"type": "default"
},
{
"id": "tool-test",
"identifier": "code-analyzer",
"apiName": "runTests",
"arguments": "{\"action\":\"test\"}",
"type": "default"
}
],
"createdAt": 1704067202000,
"updatedAt": 1704067202000,
"meta": {},
"usage": {
"totalInputTokens": 32,
"totalOutputTokens": 48,
"totalTokens": 80,
"cost": 0.00024
},
"performance": {
"tps": 45
}
},
"msg-503": {
"id": "msg-503",
"role": "tool",
"tool_call_id": "tool-lint",
"content": "{\"status\":\"success\",\"issues\":[{\"line\":15,\"message\":\"undefined variable 'result'\"}]}",
"pluginState": {
"cached": false
},
"parentId": "msg-502",
"createdAt": 1704067203000,
"updatedAt": 1704067203000,
"meta": {}
},
"msg-504": {
"id": "msg-504",
"role": "tool",
"tool_call_id": "tool-test",
"content": "{\"status\":\"failed\",\"error\":\"NameError: name 'result' is not defined\"}",
"pluginState": {
"cached": false
},
"pluginError": {
"type": "NameError",
"message": "name 'result' is not defined"
},
"parentId": "msg-502",
"createdAt": 1704067203100,
"updatedAt": 1704067203100,
"meta": {}
},
"msg-505": {
"id": "msg-505",
"role": "user",
"content": "Here's the code:\n```python\ndef calculate():\n x = 5\n y = 10\n print(result)\n```",
"parentId": "msg-504",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
"msg-506": {
"id": "msg-506",
"role": "assistant",
"content": "I found the issue! On line 15, you're trying to use `result` but it's never defined. Here's the fix:\n\n```python\ndef calculate():\n x = 5\n y = 10\n result = x + y # Define result before using it\n print(result)\n```",
"parentId": "msg-505",
"createdAt": 1704067213000,
"updatedAt": 1704067213000,
"meta": {},
"usage": {
"totalInputTokens": 156,
"totalOutputTokens": 89,
"totalTokens": 245,
"cost": 0.000735
}
},
"msg-507": {
"id": "msg-507",
"role": "assistant",
"content": "Alternatively, you could return the value instead:\n\n```python\ndef calculate():\n x = 5\n y = 10\n return x + y\n\nresult = calculate()\nprint(result)\n```",
"parentId": "msg-505",
"createdAt": 1704067213100,
"updatedAt": 1704067213100,
"meta": {},
"usage": {
"totalInputTokens": 156,
"totalOutputTokens": 72,
"totalTokens": 228,
"cost": 0.000684
}
}
}
}

View file

@ -0,0 +1,138 @@
{
"contextTree": [
{
"id": "msg-001",
"type": "message"
},
{
"id": "msg-002",
"type": "message"
},
{
"id": "msg-003",
"type": "message"
},
{
"id": "msg-004",
"type": "message"
}
],
"flatList": [
{
"id": "msg-001",
"role": "user",
"content": "Can you help me write a Python function to check if a number is prime?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
{
"id": "msg-002",
"role": "assistant",
"content": "I'd be happy to help you write a function to check if a number is prime. Here's a simple and efficient implementation:\n\n```python\ndef is_prime(n):\n if n < 2:\n return False\n if n == 2:\n return True\n if n % 2 == 0:\n return False\n \n for i in range(3, int(n**0.5) + 1, 2):\n if n % i == 0:\n return False\n \n return True\n```\n\nThis function works by checking divisibility up to the square root of the number.",
"parentId": "msg-001",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"usage": {
"totalInputTokens": 24,
"totalOutputTokens": 156,
"totalTokens": 180,
"cost": 0.00054
},
"performance": {
"tps": 52,
"ttft": 234,
"duration": 3000,
"latency": 3234
}
},
{
"id": "msg-003",
"role": "user",
"content": "Thanks! Can you add some test cases?",
"parentId": "msg-002",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
{
"id": "msg-004",
"role": "assistant",
"content": "Sure! Here are some test cases:\n\n```python\nprint(is_prime(2)) # True\nprint(is_prime(17)) # True\nprint(is_prime(100)) # False\nprint(is_prime(1)) # False\nprint(is_prime(-5)) # False\n```",
"parentId": "msg-003",
"createdAt": 1704067213000,
"updatedAt": 1704067213000,
"meta": {},
"usage": {
"totalInputTokens": 180,
"totalOutputTokens": 89,
"totalTokens": 269,
"cost": 0.000807
},
"performance": {
"tps": 44.5
}
}
],
"messageMap": {
"msg-001": {
"id": "msg-001",
"role": "user",
"content": "Can you help me write a Python function to check if a number is prime?",
"parentId": null,
"createdAt": 1704067200000,
"updatedAt": 1704067200000,
"meta": {}
},
"msg-002": {
"id": "msg-002",
"role": "assistant",
"content": "I'd be happy to help you write a function to check if a number is prime. Here's a simple and efficient implementation:\n\n```python\ndef is_prime(n):\n if n < 2:\n return False\n if n == 2:\n return True\n if n % 2 == 0:\n return False\n \n for i in range(3, int(n**0.5) + 1, 2):\n if n % i == 0:\n return False\n \n return True\n```\n\nThis function works by checking divisibility up to the square root of the number.",
"parentId": "msg-001",
"createdAt": 1704067205000,
"updatedAt": 1704067205000,
"meta": {},
"usage": {
"totalInputTokens": 24,
"totalOutputTokens": 156,
"totalTokens": 180,
"cost": 0.00054
},
"performance": {
"tps": 52,
"ttft": 234,
"duration": 3000,
"latency": 3234
}
},
"msg-003": {
"id": "msg-003",
"role": "user",
"content": "Thanks! Can you add some test cases?",
"parentId": "msg-002",
"createdAt": 1704067210000,
"updatedAt": 1704067210000,
"meta": {}
},
"msg-004": {
"id": "msg-004",
"role": "assistant",
"content": "Sure! Here are some test cases:\n\n```python\nprint(is_prime(2)) # True\nprint(is_prime(17)) # True\nprint(is_prime(100)) # False\nprint(is_prime(1)) # False\nprint(is_prime(-5)) # False\n```",
"parentId": "msg-003",
"createdAt": 1704067213000,
"updatedAt": 1704067213000,
"meta": {},
"usage": {
"totalInputTokens": 180,
"totalOutputTokens": 89,
"totalTokens": 269,
"cost": 0.000807
},
"performance": {
"tps": 44.5
}
}
}
}

View file

@ -0,0 +1,97 @@
import { describe, expect, it } from 'vitest';
import { parse } from '../parse';
import { inputs, outputs } from './fixtures';
function serializeParseResult(result: ReturnType<typeof parse>) {
return {
contextTree: result.contextTree,
flatList: result.flatList,
messageMap: result.messageMap,
};
}
describe('parse', () => {
describe('Basic Conversations', () => {
it('should parse linear conversation correctly', () => {
const result = parse(inputs.linearConversation);
expect(serializeParseResult(result)).toEqual(outputs.linearConversation);
});
});
describe('Tool Usage', () => {
it('should parse assistant with tools correctly', () => {
const result = parse(inputs.assistantWithTools);
expect(serializeParseResult(result)).toEqual(outputs.assistantWithTools);
});
it('should include follow-up messages after assistant chain', () => {
const result = parse(inputs.assistantChainWithFollowup);
// The critical assertion: flatList should contain all 5 messages
// msg-1 (user) + assistantGroup (msg-2+msg-3+tool-1) + msg-4 (user follow-up)
expect(result.flatList).toHaveLength(3);
expect(result.flatList[0].id).toBe('msg-1');
expect(result.flatList[1].role).toBe('assistantGroup');
expect(result.flatList[2].id).toBe('msg-4'); // This is the critical one that might be missing
expect(serializeParseResult(result)).toEqual(outputs.assistantChainWithFollowup);
});
});
describe('Branching', () => {
it('should parse branched conversation correctly', () => {
const result = parse(inputs.branch.conversation);
expect(serializeParseResult(result)).toEqual(outputs.branch.conversation);
});
it('should respect activeBranchIndex when specified', () => {
const result = parse(inputs.branch.activeIndex1);
expect(serializeParseResult(result)).toEqual(outputs.branch.activeIndex1);
});
it('should handle assistant message with branches', () => {
const result = parse(inputs.branch.assistantBranch);
expect(serializeParseResult(result)).toEqual(outputs.branch.assistantBranch);
});
it('should handle assistant with user branches', () => {
const result = parse(inputs.branch.assistantUserBranch);
expect(serializeParseResult(result)).toEqual(outputs.branch.assistantUserBranch);
});
it('should handle deeply nested branches (4 levels)', () => {
const result = parse(inputs.branch.nested);
expect(serializeParseResult(result)).toEqual(outputs.branch.nested);
});
});
describe('Compare Mode', () => {
it('should parse simple compare mode correctly', () => {
const result = parse(inputs.compare.simple);
expect(serializeParseResult(result)).toEqual(outputs.compare.simple);
});
it('should parse compare mode with tools correctly', () => {
const result = parse(inputs.compare.withTools);
expect(serializeParseResult(result)).toEqual(outputs.compare.withTools);
});
});
describe('Complex Scenarios', () => {
it('should handle complex mixed scenarios correctly', () => {
const result = parse(inputs.complexScenario);
expect(serializeParseResult(result)).toEqual(outputs.complexScenario);
});
});
});

View file

@ -0,0 +1,17 @@
// Main parse function
export { parse } from './parse';
// Context Tree Types - for navigation and context understanding
export type {
AssistantGroupNode,
BranchNode,
CompareNode,
ContextNode,
MessageNode,
} from './types';
// Flat Message List Types - for virtual list rendering
export type { BranchMetadata, FlatMessage, FlatMessageExtra, FlatMessageRole } from './types';
// Shared Types
export type { HelperMaps, IdNode, Message, MessageGroupMetadata, ParseResult } from './types';

View file

@ -0,0 +1,58 @@
import type { HelperMaps, Message, MessageGroupMetadata } from './types';
/**
* Phase 1: Indexing
* Builds helper maps for efficient querying during parsing
*
* @param messages - Flat array of messages from backend
* @param messageGroups - Optional array of message group metadata
* @returns Helper maps for efficient access
*/
export function buildHelperMaps(
messages: Message[],
messageGroups?: MessageGroupMetadata[],
): HelperMaps {
const messageMap = new Map<string, Message>();
const childrenMap = new Map<string | null, string[]>();
const threadMap = new Map<string, Message[]>();
const messageGroupMap = new Map<string, MessageGroupMetadata>();
// Single pass through messages to build all maps
for (const message of messages) {
// 1. Build messageMap for O(1) access
messageMap.set(message.id, message);
// 2. Build childrenMap for parent -> children lookup
const parentId = message.parentId ?? null;
const siblings = childrenMap.get(parentId);
if (siblings) {
siblings.push(message.id);
} else {
childrenMap.set(parentId, [message.id]);
}
// 3. Build threadMap for thread aggregation
if (message.threadId) {
const threadMessages = threadMap.get(message.threadId);
if (threadMessages) {
threadMessages.push(message);
} else {
threadMap.set(message.threadId, [message]);
}
}
}
// 4. Build messageGroupMap from provided metadata
if (messageGroups) {
for (const group of messageGroups) {
messageGroupMap.set(group.id, group);
}
}
return {
childrenMap,
messageGroupMap,
messageMap,
threadMap,
};
}

View file

@ -0,0 +1,53 @@
import { buildHelperMaps } from './indexing';
import { buildIdTree } from './structuring';
import { Transformer } from './transformation';
import type { Message, MessageGroupMetadata, ParseResult } from './types';
/**
* Main parse function - the brain of the conversation flow engine
*
* Converts a flat array of messages into:
* 1. messageMap - for O(1) message access
* 2. displayTree - semantic tree structure for navigation
* 3. flatList - flattened array optimized for virtual list rendering
*
* Uses a three-phase parsing strategy:
* 1. Indexing - build helper maps for efficient querying
* 2. Structuring - convert flat data to tree structure
* 3. Transformation - apply business logic to create semantic nodes and flat list
*
* @param messages - Flat array of messages from backend
* @param messageGroups - Optional array of message group metadata for compare/manual grouping
* @returns ParseResult containing messageMap, displayTree, and flatList
*/
export function parse(messages: Message[], messageGroups?: MessageGroupMetadata[]): ParseResult {
// Phase 1: Indexing
// Build helper maps for O(1) access patterns
const helperMaps = buildHelperMaps(messages, messageGroups);
// Phase 2: Structuring
// Convert flat parent-child relationships to tree structure
// Separates main flow from threaded conversations
const idTree = buildIdTree(helperMaps);
// Phase 3: Transformation
// Apply priority-based pattern matching to create semantic display nodes
const transformer = new Transformer(helperMaps);
const contextTree = transformer.transformAll(idTree);
// Phase 3b: Generate flatList for virtual list rendering
// Implements RFC priority-based pattern matching
const flatList = transformer.flatten(messages);
// Convert messageMap from Map to plain object for serialization
const messageMapObj: Record<string, Message> = {};
helperMaps.messageMap.forEach((message, id) => {
messageMapObj[id] = message;
});
return {
contextTree,
flatList,
messageMap: messageMapObj,
};
}

View file

@ -0,0 +1,38 @@
import type { HelperMaps, IdNode } from './types';
/**
* Phase 2: Structuring
* Converts flat parent-child relationships into a tree structure
* Separates main flow from threaded conversations
*
* @param helperMaps - Maps built in indexing phase
* @returns Root nodes of the main conversation flow (idTree)
*/
export function buildIdTree(helperMaps: HelperMaps): IdNode[] {
const { childrenMap, messageMap } = helperMaps;
// Build tree recursively starting from root messages (parentId = null)
const buildTree = (messageId: string): IdNode => {
const childIds = childrenMap.get(messageId) ?? [];
// Filter children to only include those in main flow (not in threads)
const mainFlowChildIds = childIds.filter((childId) => {
const child = messageMap.get(childId);
return child && !child.threadId;
});
return {
children: mainFlowChildIds.map((childId) => buildTree(childId)),
id: messageId,
};
};
// Get root message IDs (messages with parentId = null and no threadId)
const rootIds = childrenMap.get(null) ?? [];
const mainFlowRootIds = rootIds.filter((id) => {
const msg = messageMap.get(id);
return msg && !msg.threadId;
});
return mainFlowRootIds.map((rootId) => buildTree(rootId));
}

View file

@ -0,0 +1,66 @@
import type { IdNode, Message } from '../types';
/**
* BranchResolver - Handles branch resolution logic
*
* Determines which branch should be active based on:
* 1. metadata.activeBranchIndex (explicit index)
* 2. Inferred from which branch has children
* 3. Default to first branch
*/
export class BranchResolver {
/**
* Get active branch ID from IdNode structure (used in contextTree building)
*/
getActiveBranchId(message: Message, idNode: IdNode): string {
// Priority 1: Try to get from metadata.activeBranchIndex (index-based)
const activeBranchIndex = (message.metadata as any)?.activeBranchIndex;
if (
typeof activeBranchIndex === 'number' &&
activeBranchIndex >= 0 &&
activeBranchIndex < idNode.children.length
) {
return idNode.children[activeBranchIndex].id;
}
// Priority 2: Infer from which branch has children
for (const child of idNode.children) {
if (child.children.length > 0) {
return child.id;
}
}
// Default to first branch
return idNode.children[0].id;
}
/**
* Get active branch ID from flat list (used in flatList building)
*/
getActiveBranchIdFromMetadata(
message: Message,
childIds: string[],
childrenMap: Map<string | null, string[]>,
): string {
// Priority 1: Try to get from metadata.activeBranchIndex (index-based)
const activeBranchIndex = (message.metadata as any)?.activeBranchIndex;
if (
typeof activeBranchIndex === 'number' &&
activeBranchIndex >= 0 &&
activeBranchIndex < childIds.length
) {
return childIds[activeBranchIndex];
}
// Priority 2: Infer from which child has descendants
for (const childId of childIds) {
const descendants = childrenMap.get(childId);
if (descendants && descendants.length > 0) {
return childId;
}
}
// Default to first child
return childIds[0];
}
}

View file

@ -0,0 +1,292 @@
import type {
AssistantGroupNode,
BranchNode,
CompareNode,
ContextNode,
IdNode,
Message,
MessageGroupMetadata,
MessageNode,
} from '../types';
import type { BranchResolver } from './BranchResolver';
import type { MessageCollector } from './MessageCollector';
/**
* ContextTreeBuilder - Transforms IdNode tree into ContextNode tree
*
* Handles:
* 1. Tree traversal with priority-based node type detection
* 2. Creating different types of ContextNodes (Message, Branch, Compare, AssistantGroup)
* 3. Linear array output of the tree structure
*/
export class ContextTreeBuilder {
constructor(
private messageMap: Map<string, Message>,
private messageGroupMap: Map<string, MessageGroupMetadata>,
private branchResolver: BranchResolver,
private messageCollector: MessageCollector,
private generateNodeId: (prefix: string, messageId: string) => string,
) {}
/**
* Transform all root nodes to contextTree
* Returns a linear array of context nodes
*/
transformAll(idNodes: IdNode[]): ContextNode[] {
const contextTree: ContextNode[] = [];
for (const idNode of idNodes) {
this.transformToLinear(idNode, contextTree);
}
return contextTree;
}
/**
* Transform a single IdNode and append to contextTree array
*/
private transformToLinear(idNode: IdNode, contextTree: ContextNode[]): void {
const message = this.messageMap.get(idNode.id);
if (!message) return;
// Priority 1: Compare mode from user message metadata
if (this.isCompareMode(message) && idNode.children.length > 1) {
// Add user message node
const messageNode = this.createMessageNode(message);
contextTree.push(messageNode);
// Create compare node with children
const compareNode = this.createCompareNodeFromChildren(message, idNode);
contextTree.push(compareNode);
// Continue with active column's children (if any)
if (compareNode.activeColumnId) {
// Find the active column's IdNode in the children
const activeColumnIdNode = idNode.children.find(
(child) => child.id === compareNode.activeColumnId,
);
if (activeColumnIdNode && activeColumnIdNode.children.length > 0) {
this.transformToLinear(activeColumnIdNode.children[0], contextTree);
}
}
return;
}
// Priority 2: Compare mode (from messageGroup metadata)
const messageGroup = message.groupId ? this.messageGroupMap.get(message.groupId) : undefined;
if (messageGroup && messageGroup.mode === 'compare') {
// Create compare node
const compareNode = this.createCompareNode(messageGroup, message);
contextTree.push(compareNode);
// Continue with active column's children (if any)
if (compareNode.activeColumnId) {
// Find the active column's IdNode in the children
const activeColumnIdNode = idNode.children.find(
(child) => child.id === compareNode.activeColumnId,
);
if (activeColumnIdNode && activeColumnIdNode.children.length > 0) {
this.transformToLinear(activeColumnIdNode.children[0], contextTree);
}
}
return;
}
// Priority 3: AssistantGroup (assistant + tools)
if (this.isAssistantGroupNode(message, idNode)) {
const assistantGroupNode = this.createAssistantGroupNode(message, idNode);
contextTree.push(assistantGroupNode);
// Find the next message after tools
const nextMessage = this.messageCollector.findNextAfterTools(message, idNode);
if (nextMessage) {
this.transformToLinear(nextMessage, contextTree);
}
return;
}
// Priority 4: Branch (multiple children)
if (idNode.children.length > 1) {
// Add current message node
const messageNode = this.createMessageNode(message);
contextTree.push(messageNode);
// Create branch node
const branchNode = this.createBranchNode(message, idNode);
contextTree.push(branchNode);
// Don't continue after branch - branch is an end point
return;
}
// Priority 5: Regular message
const messageNode = this.createMessageNode(message);
contextTree.push(messageNode);
// Continue with single child
if (idNode.children.length === 1) {
this.transformToLinear(idNode.children[0], contextTree);
}
}
/**
* Check if message has compare mode in metadata
*/
private isCompareMode(message: Message): boolean {
return (message.metadata as any)?.compare === true;
}
/**
* Check if this is Assistant + Tools pattern
*/
private isAssistantGroupNode(message: Message, idNode: IdNode): boolean {
if (message.role !== 'assistant') return false;
return (
idNode.children.length > 0 &&
idNode.children.every((child) => {
const childMsg = this.messageMap.get(child.id);
return childMsg?.role === 'tool';
})
);
}
/**
* Create MessageNode (leaf node)
* Uses the message's own id directly
*/
private createMessageNode(message: Message): MessageNode {
return {
id: message.id,
type: 'message',
};
}
/**
* Create AssistantGroupNode
* Collects all assistant messages in the sequence (with or without tools)
*/
private createAssistantGroupNode(message: Message, idNode: IdNode): AssistantGroupNode {
const children: ContextNode[] = [];
// Recursively collect all assistant messages in this group
this.messageCollector.collectAssistantGroupMessages(message, idNode, children);
return {
children,
id: message.id,
type: 'assistantGroup',
};
}
/**
* Create BranchNode
*/
private createBranchNode(message: Message, idNode: IdNode): BranchNode {
const activeBranchId = this.branchResolver.getActiveBranchId(message, idNode);
const activeBranchIndex = idNode.children.findIndex((child) => child.id === activeBranchId);
// Each branch is a tree starting from that child
const branches = idNode.children.map((child) => {
const branchTree: ContextNode[] = [];
this.transformToLinear(child, branchTree);
return branchTree;
});
return {
activeBranchIndex: activeBranchIndex >= 0 ? activeBranchIndex : 0,
branches,
id: this.generateNodeId('branch', message.id),
parentMessageId: message.id,
type: 'branch',
};
}
/**
* Create CompareNode from children messages
*/
private createCompareNodeFromChildren(message: Message, idNode: IdNode): CompareNode {
// Find active column ID from children metadata
let activeColumnId: string | undefined;
// Each child is a column - need to recursively process to handle AssistantGroup
const columns = idNode.children.map((child) => {
const childMessage = this.messageMap.get(child.id);
if (!childMessage) {
return [
{
id: child.id,
type: 'message',
} as MessageNode,
];
}
// Check if this message is marked as active column
if ((childMessage.metadata as any)?.activeColumn === true) {
activeColumnId = child.id;
}
// Check if this column should be an AssistantGroup
if (this.isAssistantGroupNode(childMessage, child)) {
const assistantGroupNode = this.createAssistantGroupNode(childMessage, child);
return [assistantGroupNode];
}
// Otherwise, just a simple MessageNode
return [
{
id: child.id,
type: 'message',
} as MessageNode,
];
});
// Generate ID by joining parent message id and all column message ids
const columnIds = idNode.children.map((child) => child.id).join('-');
const compareId = `compare-${message.id}-${columnIds}`;
return {
activeColumnId,
columns,
id: compareId,
messageId: message.id,
type: 'compare',
};
}
/**
* Create CompareNode from message group
*/
private createCompareNode(group: MessageGroupMetadata, message: Message): CompareNode {
// Collect all messages in this group
const groupMessages: Message[] = [];
for (const msg of this.messageMap.values()) {
if (msg.groupId === group.id) {
groupMessages.push(msg);
}
}
// Find active column ID from group messages metadata
const activeColumnId = groupMessages.find(
(msg) => (msg.metadata as any)?.activeColumn === true,
)?.id;
// Each column is a message tree
const columns = groupMessages.map((msg) => {
const messageNode: MessageNode = {
id: msg.id,
type: 'message',
};
return [messageNode];
});
return {
activeColumnId,
columns,
id: this.generateNodeId('compare', group.id),
messageId: group.parentMessageId || message.id,
type: 'compare',
};
}
}

View file

@ -0,0 +1,421 @@
import type { AssistantContentBlock, ChatToolPayloadWithResult } from '@lobechat/types';
import type { Message, MessageGroupMetadata } from '../types';
import type { BranchResolver } from './BranchResolver';
import type { MessageCollector } from './MessageCollector';
import type { MessageTransformer } from './MessageTransformer';
/**
* FlatListBuilder - Builds flat message list following the active path
*
* Handles:
* 1. Recursive traversal following active branches
* 2. Creating virtual messages for Compare and AssistantGroup
* 3. Processing different message types with priority
*/
export class FlatListBuilder {
constructor(
private messageMap: Map<string, Message>,
private messageGroupMap: Map<string, MessageGroupMetadata>,
private childrenMap: Map<string | null, string[]>,
private branchResolver: BranchResolver,
private messageCollector: MessageCollector,
private messageTransformer: MessageTransformer,
) {}
/**
* Generate flatList from messages array
* Only includes messages in the active path
*/
flatten(messages: Message[]): Message[] {
const flatList: Message[] = [];
const processedIds = new Set<string>();
// Build the active path by traversing from root
this.buildFlatListRecursive(null, flatList, processedIds, messages);
return flatList;
}
/**
* Recursively build flatList following the active path
*/
private buildFlatListRecursive(
parentId: string | null,
flatList: Message[],
processedIds: Set<string>,
allMessages: Message[],
): void {
const children = this.childrenMap.get(parentId) ?? [];
for (const childId of children) {
if (processedIds.has(childId)) continue;
const message = this.messageMap.get(childId);
if (!message) continue;
// Priority 1: Compare message group
const messageGroup = message.groupId ? this.messageGroupMap.get(message.groupId) : undefined;
if (messageGroup && messageGroup.mode === 'compare' && !processedIds.has(messageGroup.id)) {
const groupMembers = this.messageCollector.collectGroupMembers(
message.groupId!,
allMessages,
);
const compareMessage = this.createCompareMessage(messageGroup, groupMembers);
flatList.push(compareMessage);
groupMembers.forEach((m) => processedIds.add(m.id));
processedIds.add(messageGroup.id);
// Continue with active column's children (if any)
if ((compareMessage as any).activeColumnId) {
this.buildFlatListRecursive(
(compareMessage as any).activeColumnId,
flatList,
processedIds,
allMessages,
);
}
continue;
}
// Priority 2: AssistantGroup (assistant + tools)
if (message.role === 'assistant' && message.tools && message.tools.length > 0) {
// Collect the entire assistant group chain
const assistantChain: Message[] = [];
const allToolMessages: Message[] = [];
this.messageCollector.collectAssistantChain(
message,
allMessages,
assistantChain,
allToolMessages,
processedIds,
);
// Create assistantGroup virtual message
const groupMessage = this.createAssistantGroupMessage(
assistantChain[0],
assistantChain,
allToolMessages,
);
flatList.push(groupMessage);
// Mark all as processed
assistantChain.forEach((m) => processedIds.add(m.id));
allToolMessages.forEach((m) => processedIds.add(m.id));
// Continue after the assistant chain
// Priority 1: If last assistant has non-tool children, continue from it
// Priority 2: Otherwise continue from tools (for cases where user replies to tool)
const lastAssistant = assistantChain.at(-1);
const toolIds = new Set(allToolMessages.map((t) => t.id));
const lastAssistantNonToolChildren = lastAssistant
? this.childrenMap.get(lastAssistant.id)?.filter((childId) => !toolIds.has(childId))
: undefined;
if (
lastAssistantNonToolChildren &&
lastAssistantNonToolChildren.length > 0 &&
lastAssistant
) {
// Follow-up messages exist after the last assistant (not tools)
this.buildFlatListRecursive(lastAssistant.id, flatList, processedIds, allMessages);
} else {
// No non-tool children of last assistant, check tools for children
for (const toolMsg of allToolMessages) {
this.buildFlatListRecursive(toolMsg.id, flatList, processedIds, allMessages);
}
}
continue;
}
// Priority 3a: Compare mode from user message metadata
const childMessages = this.childrenMap.get(message.id) ?? [];
if (this.isCompareMode(message) && childMessages.length > 1) {
// Add user message
flatList.push(message);
processedIds.add(message.id);
// Create compare virtual message with proper handling of AssistantGroups
const compareMessage = this.createCompareMessageFromChildIds(
message,
childMessages,
allMessages,
processedIds,
);
flatList.push(compareMessage);
// Continue with active column's children (if any)
if ((compareMessage as any).activeColumnId) {
this.buildFlatListRecursive(
(compareMessage as any).activeColumnId,
flatList,
processedIds,
allMessages,
);
}
continue;
}
// Priority 3b: User message with branches
if (message.role === 'user' && childMessages.length > 1) {
const activeBranchId = this.branchResolver.getActiveBranchIdFromMetadata(
message,
childMessages,
this.childrenMap,
);
const userWithBranches = this.createUserMessageWithBranches(message);
flatList.push(userWithBranches);
processedIds.add(message.id);
// Continue with active branch and process its message
const activeBranchMsg = this.messageMap.get(activeBranchId);
if (activeBranchMsg) {
flatList.push(activeBranchMsg);
processedIds.add(activeBranchId);
// Continue with active branch's children
this.buildFlatListRecursive(activeBranchId, flatList, processedIds, allMessages);
}
continue;
}
// Priority 3c: Assistant message with branches
if (message.role === 'assistant' && childMessages.length > 1) {
const activeBranchId = this.branchResolver.getActiveBranchIdFromMetadata(
message,
childMessages,
this.childrenMap,
);
// Add the assistant message itself
flatList.push(message);
processedIds.add(message.id);
// Continue with active branch and process its message
const activeBranchMsg = this.messageMap.get(activeBranchId);
if (activeBranchMsg) {
flatList.push(activeBranchMsg);
processedIds.add(activeBranchId);
// Continue with active branch's children
this.buildFlatListRecursive(activeBranchId, flatList, processedIds, allMessages);
}
continue;
}
// Priority 4: Regular message
flatList.push(message);
processedIds.add(message.id);
// Continue with children
this.buildFlatListRecursive(message.id, flatList, processedIds, allMessages);
}
}
/**
* Check if message has compare mode in metadata
*/
private isCompareMode(message: Message): boolean {
return (message.metadata as any)?.compare === true;
}
/**
* Create compare virtual message from child IDs with AssistantGroup support
*/
private createCompareMessageFromChildIds(
parentMessage: Message,
childIds: string[],
allMessages: Message[],
processedIds: Set<string>,
): Message {
const columns: Message[][] = [];
const columnFirstIds: string[] = [];
let activeColumnId: string | undefined;
// Process each child (column)
for (const childId of childIds) {
const childMessage = this.messageMap.get(childId);
if (!childMessage) continue;
columnFirstIds.push(childId);
// Check if this message is marked as active column
if ((childMessage.metadata as any)?.activeColumn === true) {
activeColumnId = childId;
}
// Check if this child is an AssistantGroup
if (
childMessage.role === 'assistant' &&
childMessage.tools &&
childMessage.tools.length > 0
) {
// Collect the entire assistant group chain for this column
const assistantChain: Message[] = [];
const allToolMessages: Message[] = [];
const columnProcessedIds = new Set<string>();
this.messageCollector.collectAssistantChain(
childMessage,
allMessages,
assistantChain,
allToolMessages,
columnProcessedIds,
);
// Create assistantGroup virtual message for this column
const groupMessage = this.createAssistantGroupMessage(
assistantChain[0],
assistantChain,
allToolMessages,
);
columns.push([groupMessage]);
// Mark all as processed
assistantChain.forEach((m) => processedIds.add(m.id));
allToolMessages.forEach((m) => processedIds.add(m.id));
} else {
// Regular message (not an AssistantGroup)
columns.push([childMessage]);
processedIds.add(childId);
}
}
// Generate ID with all column first message IDs
const columnIdsStr = columnFirstIds.join('-');
const compareId = `compare-${parentMessage.id}-${columnIdsStr}`;
// Calculate timestamps from first column's messages
const firstColumnMessages = childIds.map((id) => this.messageMap.get(id)).filter(Boolean);
const createdAt =
firstColumnMessages.length > 0
? Math.min(...firstColumnMessages.map((m) => m!.createdAt))
: parentMessage.createdAt;
const updatedAt =
firstColumnMessages.length > 0
? Math.max(...firstColumnMessages.map((m) => m!.updatedAt))
: parentMessage.updatedAt;
return {
activeColumnId,
columns: columns as any,
content: '',
createdAt,
extra: {
parentMessageId: parentMessage.id,
},
id: compareId,
meta: parentMessage.meta || {},
role: 'compare' as any,
updatedAt,
} as Message;
}
/**
* Create compare virtual message (for group-based compare)
*/
private createCompareMessage(group: MessageGroupMetadata, members: Message[]): Message {
// Find active column ID from members metadata
const activeColumnId = members.find((msg) => (msg.metadata as any)?.activeColumn === true)?.id;
// columns contain full Message objects
const columns: Message[][] = members.map((msg) => [msg]);
return {
activeColumnId,
columns: columns as any,
content: '',
createdAt: Math.min(...members.map((m) => m.createdAt)),
extra: {
groupMode: group.mode,
parentMessageId: group.parentMessageId,
},
id: group.id,
meta: members[0]?.meta || {},
role: 'compare' as any,
updatedAt: Math.max(...members.map((m) => m.updatedAt)),
} as Message;
}
/**
* Create assistant group virtual message from entire chain
*/
private createAssistantGroupMessage(
firstAssistant: Message,
assistantChain: Message[],
allToolMessages: Message[],
): Message {
const children: AssistantContentBlock[] = [];
// Create tool map for lookup
const toolMap = new Map<string, Message>();
allToolMessages.forEach((tm) => {
if (tm.tool_call_id) {
toolMap.set(tm.tool_call_id, tm);
}
});
// Process each assistant in the chain
for (const assistant of assistantChain) {
// Build toolsWithResults for this assistant
const toolsWithResults: ChatToolPayloadWithResult[] =
assistant.tools?.map((tool) => {
const toolMsg = toolMap.get(tool.id);
if (toolMsg) {
return {
...tool,
result: {
content: toolMsg.content || '',
error: toolMsg.error,
id: toolMsg.id,
state: toolMsg.pluginState,
},
result_msg_id: toolMsg.id,
};
}
return tool;
}) || [];
const { usage: msgUsage, performance: msgPerformance } =
this.messageTransformer.splitMetadata(assistant.metadata);
children.push({
content: assistant.content || '',
error: assistant.error,
id: assistant.id,
imageList:
assistant.imageList && assistant.imageList.length > 0 ? assistant.imageList : undefined,
performance: msgPerformance,
reasoning: assistant.reasoning || undefined,
tools: toolsWithResults.length > 0 ? toolsWithResults : undefined,
usage: msgUsage,
});
}
const aggregated = this.messageTransformer.aggregateMetadata(children);
return {
...firstAssistant,
children,
content: '',
imageList: undefined,
metadata: undefined,
performance: aggregated.performance,
reasoning: undefined,
role: 'assistantGroup' as any,
tools: undefined,
usage: aggregated.usage,
};
}
/**
* Create user message with branch metadata
*/
private createUserMessageWithBranches(user: Message): Message {
// Just return the original user message with its metadata.activeBranchId
// No need to add extra.branches
return { ...user };
}
}

View file

@ -0,0 +1,166 @@
import type { ContextNode, IdNode, Message, MessageNode } from '../types';
/**
* MessageCollector - Handles collection of related messages
*
* Provides utilities for:
* 1. Collecting messages in a group
* 2. Collecting tool messages
* 3. Collecting assistant chains
* 4. Finding next messages in sequences
*/
export class MessageCollector {
constructor(
private messageMap: Map<string, Message>,
private childrenMap: Map<string | null, string[]>,
) {}
/**
* Collect all messages belonging to a message group
*/
collectGroupMembers(groupId: string, messages: Message[]): Message[] {
return messages.filter((m) => m.groupId === groupId);
}
/**
* Collect tool messages related to an assistant message
*/
collectToolMessages(assistant: Message, messages: Message[]): Message[] {
const toolCallIds = new Set(assistant.tools?.map((t) => t.id) || []);
return messages.filter(
(m) => m.role === 'tool' && m.tool_call_id && toolCallIds.has(m.tool_call_id),
);
}
/**
* Recursively collect the entire assistant chain
* (assistant -> tools -> assistant -> tools -> ...)
*/
collectAssistantChain(
currentAssistant: Message,
allMessages: Message[],
assistantChain: Message[],
allToolMessages: Message[],
processedIds: Set<string>,
): void {
if (processedIds.has(currentAssistant.id)) return;
// Add current assistant to chain
assistantChain.push(currentAssistant);
// Collect its tool messages
const toolMessages = this.collectToolMessages(currentAssistant, allMessages);
allToolMessages.push(...toolMessages);
// Find next assistant after tools
for (const toolMsg of toolMessages) {
const nextMessages = allMessages.filter((m) => m.parentId === toolMsg.id);
for (const nextMsg of nextMessages) {
if (nextMsg.role === 'assistant' && nextMsg.tools && nextMsg.tools.length > 0) {
// Continue the chain
this.collectAssistantChain(
nextMsg,
allMessages,
assistantChain,
allToolMessages,
processedIds,
);
return;
} else if (nextMsg.role === 'assistant') {
// Final assistant without tools
assistantChain.push(nextMsg);
return;
}
}
}
}
/**
* Recursively collect assistant messages for an AssistantGroup (contextTree version)
*/
collectAssistantGroupMessages(
message: Message,
idNode: IdNode,
children: ContextNode[],
): void {
// Get tool message IDs if this assistant has tools
const toolIds = idNode.children
.filter((child) => {
const childMsg = this.messageMap.get(child.id);
return childMsg?.role === 'tool';
})
.map((child) => child.id);
// Add current assistant message node
const messageNode: MessageNode = {
id: message.id,
type: 'message',
};
if (toolIds.length > 0) {
messageNode.tools = toolIds;
}
children.push(messageNode);
// Find next assistant message after tools
for (const toolNode of idNode.children) {
const toolMsg = this.messageMap.get(toolNode.id);
if (toolMsg?.role !== 'tool') continue;
// Check if tool has an assistant child
if (toolNode.children.length > 0) {
const nextChild = toolNode.children[0];
const nextMsg = this.messageMap.get(nextChild.id);
if (nextMsg?.role === 'assistant') {
// Recursively collect this assistant and its descendants
this.collectAssistantGroupMessages(nextMsg, nextChild, children);
return; // Only follow one path
}
}
}
}
/**
* Find next message after tools in an assistant group
*/
findNextAfterTools(assistantMsg: Message, idNode: IdNode): IdNode | null {
// Recursively find the last message in the assistant group
const lastNode = this.findLastNodeInAssistantGroup(idNode);
if (lastNode && lastNode.children.length > 0) {
return lastNode.children[0];
}
return null;
}
/**
* Find the last node in an AssistantGroup sequence
*/
findLastNodeInAssistantGroup(idNode: IdNode): IdNode | null {
// Check if has tool children
const toolChildren = idNode.children.filter((child) => {
const childMsg = this.messageMap.get(child.id);
return childMsg?.role === 'tool';
});
if (toolChildren.length === 0) {
return idNode;
}
// Check if any tool has an assistant child
for (const toolNode of toolChildren) {
if (toolNode.children.length > 0) {
const nextChild = toolNode.children[0];
const nextMsg = this.messageMap.get(nextChild.id);
if (nextMsg?.role === 'assistant') {
// Continue following the assistant chain
return this.findLastNodeInAssistantGroup(nextChild);
}
}
}
// No more assistant messages, return the last tool node
return toolChildren.at(-1) ?? null;
}
}

View file

@ -0,0 +1,177 @@
import type { AssistantContentBlock, ModelPerformance, ModelUsage } from '@lobechat/types';
import type { Message } from '../types';
/**
* MessageTransformer - Handles message transformation utilities
*
* Provides utilities for:
* 1. Converting Message to AssistantContentBlock
* 2. Splitting metadata into usage and performance
* 3. Aggregating metadata from multiple messages
*/
export class MessageTransformer {
/**
* Convert a Message to AssistantContentBlock
*/
messageToContentBlock(message: Message): AssistantContentBlock {
const { usage, performance } = this.splitMetadata(message.metadata);
return {
content: message.content || '',
error: message.error,
id: message.id,
imageList: message.imageList,
performance,
reasoning: message.reasoning || undefined,
tools: message.tools as any,
usage,
};
}
/**
* Split metadata into usage and performance objects
*/
splitMetadata(metadata?: any): {
performance?: ModelPerformance;
usage?: ModelUsage;
} {
if (!metadata) return {};
const usage: ModelUsage = {};
const performance: ModelPerformance = {};
const usageFields = [
'acceptedPredictionTokens',
'cost',
'inputAudioTokens',
'inputCacheMissTokens',
'inputCachedTokens',
'inputCitationTokens',
'inputImageTokens',
'inputTextTokens',
'inputWriteCacheTokens',
'outputAudioTokens',
'outputImageTokens',
'outputReasoningTokens',
'outputTextTokens',
'rejectedPredictionTokens',
'totalInputTokens',
'totalOutputTokens',
'totalTokens',
] as const;
let hasUsage = false;
usageFields.forEach((field) => {
if (metadata[field] !== undefined) {
(usage as any)[field] = metadata[field];
hasUsage = true;
}
});
const performanceFields = ['duration', 'latency', 'tps', 'ttft'] as const;
let hasPerformance = false;
performanceFields.forEach((field) => {
if (metadata[field] !== undefined) {
(performance as any)[field] = metadata[field];
hasPerformance = true;
}
});
return {
performance: hasPerformance ? performance : undefined,
usage: hasUsage ? usage : undefined,
};
}
/**
* Aggregate metadata from multiple children
* - Sums token counts and costs
* - Takes first ttft
* - Averages tps
* - Sums duration and latency
*/
aggregateMetadata(children: AssistantContentBlock[]): {
performance?: ModelPerformance;
usage?: ModelUsage;
} {
const usage: ModelUsage = {};
const performance: ModelPerformance = {};
let hasUsageData = false;
let hasPerformanceData = false;
let tpsSum = 0;
let tpsCount = 0;
children.forEach((child) => {
if (child.usage) {
const tokenFields = [
'acceptedPredictionTokens',
'inputAudioTokens',
'inputCacheMissTokens',
'inputCachedTokens',
'inputCitationTokens',
'inputImageTokens',
'inputTextTokens',
'inputWriteCacheTokens',
'outputAudioTokens',
'outputImageTokens',
'outputReasoningTokens',
'outputTextTokens',
'rejectedPredictionTokens',
'totalInputTokens',
'totalOutputTokens',
'totalTokens',
] as const;
tokenFields.forEach((field) => {
if (typeof child.usage![field] === 'number') {
(usage as any)[field] = ((usage as any)[field] || 0) + child.usage![field]!;
hasUsageData = true;
}
});
if (typeof child.usage.cost === 'number') {
usage.cost = (usage.cost || 0) + child.usage.cost;
hasUsageData = true;
}
}
if (child.performance) {
// Take first ttft (time to first token)
if (child.performance.ttft !== undefined && performance.ttft === undefined) {
performance.ttft = child.performance.ttft;
hasPerformanceData = true;
}
// Average tps (tokens per second)
if (typeof child.performance.tps === 'number') {
tpsSum += child.performance.tps;
tpsCount += 1;
hasPerformanceData = true;
}
// Sum duration
if (child.performance.duration !== undefined) {
performance.duration = (performance.duration || 0) + child.performance.duration;
hasPerformanceData = true;
}
// Sum latency
if (child.performance.latency !== undefined) {
performance.latency = (performance.latency || 0) + child.performance.latency;
hasPerformanceData = true;
}
}
});
// Calculate average tps
if (tpsCount > 0) {
performance.tps = tpsSum / tpsCount;
}
return {
performance: hasPerformanceData ? performance : undefined,
usage: hasUsageData ? usage : undefined,
};
}
}

View file

@ -0,0 +1,151 @@
import { describe, expect, it } from 'vitest';
import type { IdNode, Message } from '../../types';
import { BranchResolver } from '../BranchResolver';
describe('BranchResolver', () => {
const resolver = new BranchResolver();
describe('getActiveBranchId', () => {
it('should return branch by activeBranchIndex from metadata when present', () => {
const message: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { activeBranchIndex: 1 },
role: 'user',
updatedAt: 0,
};
const idNode: IdNode = {
children: [
{ children: [], id: 'msg-2' },
{ children: [], id: 'msg-3' },
],
id: 'msg-1',
};
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-3');
});
it('should infer active branch from which branch has children', () => {
const message: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
};
const idNode: IdNode = {
children: [
{ children: [], id: 'msg-2' },
{ children: [{ children: [], id: 'msg-4' }], id: 'msg-3' },
],
id: 'msg-1',
};
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-3');
});
it('should default to first branch when no hints available', () => {
const message: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
};
const idNode: IdNode = {
children: [
{ children: [], id: 'msg-2' },
{ children: [], id: 'msg-3' },
],
id: 'msg-1',
};
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-2');
});
it('should ignore invalid activeBranchIndex', () => {
const message: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { activeBranchIndex: 5 }, // out of bounds
role: 'user',
updatedAt: 0,
};
const idNode: IdNode = {
children: [
{ children: [], id: 'msg-2' },
{ children: [], id: 'msg-3' },
],
id: 'msg-1',
};
// Should default to first branch
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-2');
});
});
describe('getActiveBranchIdFromMetadata', () => {
it('should return branch by activeBranchIndex from metadata', () => {
const message: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { activeBranchIndex: 1 },
role: 'user',
updatedAt: 0,
};
const childIds = ['msg-2', 'msg-3'];
const childrenMap = new Map<string | null, string[]>();
expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-3');
});
it('should infer active branch from which child has descendants', () => {
const message: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
};
const childIds = ['msg-2', 'msg-3'];
const childrenMap = new Map<string | null, string[]>([
['msg-2', []],
['msg-3', ['msg-4']],
]);
expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-3');
});
it('should default to first child when no hints available', () => {
const message: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
};
const childIds = ['msg-2', 'msg-3'];
const childrenMap = new Map<string | null, string[]>();
expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-2');
});
});
});

View file

@ -0,0 +1,385 @@
import { describe, expect, it } from 'vitest';
import type { IdNode, Message, MessageGroupMetadata } from '../../types';
import { BranchResolver } from '../BranchResolver';
import { ContextTreeBuilder } from '../ContextTreeBuilder';
import { MessageCollector } from '../MessageCollector';
describe('ContextTreeBuilder', () => {
const createBuilder = (
messageMap: Map<string, Message>,
messageGroupMap: Map<string, MessageGroupMetadata> = new Map(),
) => {
const childrenMap = new Map<string | null, string[]>();
const branchResolver = new BranchResolver();
const messageCollector = new MessageCollector(messageMap, childrenMap);
let nodeIdCounter = 0;
const generateNodeId = (prefix: string, messageId: string) =>
`${prefix}-${messageId}-${nodeIdCounter++}`;
return new ContextTreeBuilder(
messageMap,
messageGroupMap,
branchResolver,
messageCollector,
generateNodeId,
);
};
describe('transformAll', () => {
it('should transform regular message nodes', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'Hello',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
},
],
[
'msg-2',
{
content: 'Hi',
createdAt: 0,
id: 'msg-2',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
]);
const builder = createBuilder(messageMap);
const idNodes: IdNode[] = [
{
children: [{ children: [], id: 'msg-2' }],
id: 'msg-1',
},
];
const result = builder.transformAll(idNodes);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
expect(result[1]).toEqual({ id: 'msg-2', type: 'message' });
});
it('should create branch node for multiple children', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'Hello',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { activeBranchIndex: 0 },
role: 'user',
updatedAt: 0,
},
],
[
'msg-2',
{
content: 'Response 1',
createdAt: 0,
id: 'msg-2',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
[
'msg-3',
{
content: 'Response 2',
createdAt: 0,
id: 'msg-3',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
]);
const builder = createBuilder(messageMap);
const idNodes: IdNode[] = [
{
children: [
{ children: [], id: 'msg-2' },
{ children: [], id: 'msg-3' },
],
id: 'msg-1',
},
];
const result = builder.transformAll(idNodes);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
expect(result[1]).toMatchObject({
activeBranchIndex: 0,
branches: [[{ id: 'msg-2', type: 'message' }], [{ id: 'msg-3', type: 'message' }]],
parentMessageId: 'msg-1',
type: 'branch',
});
});
it('should create assistant group node for assistant with tools', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'Assistant with tools',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'assistant',
tools: [
{
apiName: 'test',
arguments: '{}',
id: 'tool-1',
identifier: 'test',
type: 'default',
},
],
updatedAt: 0,
},
],
[
'tool-1',
{
content: 'Tool result',
createdAt: 0,
id: 'tool-1',
meta: {},
role: 'tool',
updatedAt: 0,
},
],
]);
const builder = createBuilder(messageMap);
const idNodes: IdNode[] = [
{
children: [{ children: [], id: 'tool-1' }],
id: 'msg-1',
},
];
const result = builder.transformAll(idNodes);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
children: [{ id: 'msg-1', tools: ['tool-1'], type: 'message' }],
id: 'msg-1',
type: 'assistantGroup',
});
});
it('should create compare node from message group', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'Compare 1',
createdAt: 0,
groupId: 'group-1',
id: 'msg-1',
meta: {},
metadata: { activeColumn: true },
role: 'assistant',
updatedAt: 0,
},
],
[
'msg-2',
{
content: 'Compare 2',
createdAt: 0,
groupId: 'group-1',
id: 'msg-2',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
]);
const messageGroupMap = new Map<string, MessageGroupMetadata>([
['group-1', { id: 'group-1', mode: 'compare' }],
]);
const builder = createBuilder(messageMap, messageGroupMap);
const idNodes: IdNode[] = [{ children: [], id: 'msg-1' }];
const result = builder.transformAll(idNodes);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
activeColumnId: 'msg-1',
columns: [[{ id: 'msg-1', type: 'message' }], [{ id: 'msg-2', type: 'message' }]],
messageId: 'msg-1',
type: 'compare',
});
});
it('should create compare node from user message metadata', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'User message',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { compare: true },
role: 'user',
updatedAt: 0,
},
],
[
'msg-2',
{
content: 'Assistant 1',
createdAt: 0,
id: 'msg-2',
meta: {},
metadata: { activeColumn: true },
role: 'assistant',
updatedAt: 0,
},
],
[
'msg-3',
{
content: 'Assistant 2',
createdAt: 0,
id: 'msg-3',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
]);
const builder = createBuilder(messageMap);
const idNodes: IdNode[] = [
{
children: [
{ children: [], id: 'msg-2' },
{ children: [], id: 'msg-3' },
],
id: 'msg-1',
},
];
const result = builder.transformAll(idNodes);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
expect(result[1]).toMatchObject({
activeColumnId: 'msg-2',
columns: [[{ id: 'msg-2', type: 'message' }], [{ id: 'msg-3', type: 'message' }]],
id: 'compare-msg-1-msg-2-msg-3',
messageId: 'msg-1',
type: 'compare',
});
});
it('should handle empty node list', () => {
const messageMap = new Map<string, Message>();
const builder = createBuilder(messageMap);
const result = builder.transformAll([]);
expect(result).toHaveLength(0);
});
it('should handle missing message in map', () => {
const messageMap = new Map<string, Message>();
const builder = createBuilder(messageMap);
const idNodes: IdNode[] = [{ children: [], id: 'missing' }];
const result = builder.transformAll(idNodes);
expect(result).toHaveLength(0);
});
it('should continue with active column children in compare mode', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'User',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { compare: true },
role: 'user',
updatedAt: 0,
},
],
[
'msg-2',
{
content: 'Assistant 1',
createdAt: 0,
id: 'msg-2',
meta: {},
metadata: { activeColumn: true },
role: 'assistant',
updatedAt: 0,
},
],
[
'msg-3',
{
content: 'Assistant 2',
createdAt: 0,
id: 'msg-3',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
[
'msg-4',
{
content: 'Follow-up',
createdAt: 0,
id: 'msg-4',
meta: {},
role: 'user',
updatedAt: 0,
},
],
]);
const builder = createBuilder(messageMap);
const idNodes: IdNode[] = [
{
children: [
{ children: [{ children: [], id: 'msg-4' }], id: 'msg-2' },
{ children: [], id: 'msg-3' },
],
id: 'msg-1',
},
];
const result = builder.transformAll(idNodes);
expect(result).toHaveLength(3);
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
expect(result[1]).toMatchObject({
type: 'compare',
});
expect(result[2]).toEqual({ id: 'msg-4', type: 'message' });
});
});
});

View file

@ -0,0 +1,511 @@
import { describe, expect, it } from 'vitest';
import type { Message, MessageGroupMetadata } from '../../types';
import { BranchResolver } from '../BranchResolver';
import { FlatListBuilder } from '../FlatListBuilder';
import { MessageCollector } from '../MessageCollector';
import { MessageTransformer } from '../MessageTransformer';
describe('FlatListBuilder', () => {
const createBuilder = (
messages: Message[],
messageGroupMap: Map<string, MessageGroupMetadata> = new Map(),
) => {
const messageMap = new Map<string, Message>();
const childrenMap = new Map<string | null, string[]>();
// Build maps
messages.forEach((msg) => {
messageMap.set(msg.id, msg);
const parentId = msg.parentId || null;
if (!childrenMap.has(parentId)) {
childrenMap.set(parentId, []);
}
childrenMap.get(parentId)!.push(msg.id);
});
const branchResolver = new BranchResolver();
const messageCollector = new MessageCollector(messageMap, childrenMap);
const messageTransformer = new MessageTransformer();
return new FlatListBuilder(
messageMap,
messageGroupMap,
childrenMap,
branchResolver,
messageCollector,
messageTransformer,
);
};
describe('flatten', () => {
it('should flatten simple message chain', () => {
const messages: Message[] = [
{
content: 'Hello',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
},
{
content: 'Hi',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('msg-1');
expect(result[1].id).toBe('msg-2');
});
it('should create assistant group virtual message', () => {
const messages: Message[] = [
{
content: 'Request',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
},
{
content: 'Using tool',
createdAt: 0,
id: 'msg-2',
meta: {},
metadata: { totalInputTokens: 10, totalOutputTokens: 20 },
parentId: 'msg-1',
role: 'assistant',
tools: [
{ apiName: 'test', arguments: '{}', id: 'tool-1', identifier: 'test', type: 'default' },
],
updatedAt: 0,
},
{
content: 'Tool result',
createdAt: 0,
id: 'tool-1',
meta: {},
parentId: 'msg-2',
role: 'tool',
tool_call_id: 'tool-1',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('msg-1');
expect(result[1].role).toBe('assistantGroup');
expect(result[1].children).toHaveLength(1);
expect(result[1].usage).toBeDefined();
});
it('should handle user message with branches', () => {
const messages: Message[] = [
{
content: 'User',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { activeBranchIndex: 0 },
role: 'user',
updatedAt: 0,
},
{
content: 'Branch 1',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
{
content: 'Branch 2',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('msg-1');
expect(result[1].id).toBe('msg-2'); // active branch
});
it('should handle assistant message with branches', () => {
const messages: Message[] = [
{
content: 'User',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
},
{
content: 'Assistant',
createdAt: 0,
id: 'msg-2',
meta: {},
metadata: { activeBranchIndex: 1 },
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
{
content: 'Branch 1',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'msg-2',
role: 'user',
updatedAt: 0,
},
{
content: 'Branch 2',
createdAt: 0,
id: 'msg-4',
meta: {},
parentId: 'msg-2',
role: 'user',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
expect(result).toHaveLength(3);
expect(result[0].id).toBe('msg-1');
expect(result[1].id).toBe('msg-2');
expect(result[2].id).toBe('msg-4'); // active branch (index 1)
});
it('should create compare message from message group', () => {
const messages: Message[] = [
{
content: 'Compare 1',
createdAt: 0,
groupId: 'group-1',
id: 'msg-1',
meta: {},
metadata: { activeColumn: true },
role: 'assistant',
updatedAt: 0,
},
{
content: 'Compare 2',
createdAt: 0,
groupId: 'group-1',
id: 'msg-2',
meta: {},
role: 'assistant',
updatedAt: 0,
},
];
const messageGroupMap = new Map<string, MessageGroupMetadata>([
['group-1', { id: 'group-1', mode: 'compare' }],
]);
const builder = createBuilder(messages, messageGroupMap);
const result = builder.flatten(messages);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('group-1');
expect(result[0].role).toBe('compare');
expect((result[0] as any).columns).toHaveLength(2);
expect((result[0] as any).activeColumnId).toBe('msg-1');
});
it('should create compare message from user metadata', () => {
const messages: Message[] = [
{
content: 'User',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { compare: true },
role: 'user',
updatedAt: 0,
},
{
content: 'Assistant 1',
createdAt: 0,
id: 'msg-2',
meta: {},
metadata: { activeColumn: true },
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
{
content: 'Assistant 2',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('msg-1');
expect(result[1].role).toBe('compare');
expect((result[1] as any).activeColumnId).toBe('msg-2');
});
it('should handle empty messages array', () => {
const builder = createBuilder([]);
const result = builder.flatten([]);
expect(result).toHaveLength(0);
});
it('should follow active branch correctly', () => {
const messages: Message[] = [
{
content: 'User',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { activeBranchIndex: 0 },
role: 'user',
updatedAt: 0,
},
{
content: 'Branch 1',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
{
content: 'Branch 2',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
{
content: 'Follow-up',
createdAt: 0,
id: 'msg-4',
meta: {},
parentId: 'msg-2',
role: 'user',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
expect(result).toHaveLength(3);
expect(result[0].id).toBe('msg-1');
expect(result[1].id).toBe('msg-2');
expect(result[2].id).toBe('msg-4');
});
it('should handle assistant group in compare columns', () => {
const messages: Message[] = [
{
content: 'User',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: { compare: true },
role: 'user',
updatedAt: 0,
},
{
content: 'Assistant 1',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'msg-1',
role: 'assistant',
tools: [
{ apiName: 'test', arguments: '{}', id: 'tool-1', identifier: 'test', type: 'default' },
],
updatedAt: 0,
},
{
content: 'Tool result',
createdAt: 0,
id: 'tool-1',
meta: {},
parentId: 'msg-2',
role: 'tool',
tool_call_id: 'tool-1',
updatedAt: 0,
},
{
content: 'Assistant 2',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'msg-1',
role: 'assistant',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('msg-1');
expect(result[1].role).toBe('compare');
const columns = (result[1] as any).columns;
expect(columns).toHaveLength(2);
// First column should be an assistant group
expect(columns[0][0].role).toBe('assistantGroup');
// Second column should be a regular message
expect(columns[1][0].id).toBe('msg-3');
});
it('should include follow-up messages after assistant chain', () => {
const messages: Message[] = [
{
content: 'User request',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
},
{
content: 'Using tool',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'msg-1',
role: 'assistant',
tools: [
{ apiName: 'test', arguments: '{}', id: 'tool-1', identifier: 'test', type: 'default' },
],
updatedAt: 0,
},
{
content: 'Tool result',
createdAt: 0,
id: 'tool-1',
meta: {},
parentId: 'msg-2',
role: 'tool',
tool_call_id: 'tool-1',
updatedAt: 0,
},
{
content: 'Response based on tool',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'tool-1',
role: 'assistant',
updatedAt: 0,
},
{
content: 'User follow-up',
createdAt: 0,
id: 'msg-4',
meta: {},
parentId: 'msg-3',
role: 'user',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
// Critical: msg-4 should be included
expect(result).toHaveLength(3);
expect(result[0].id).toBe('msg-1');
expect(result[1].role).toBe('assistantGroup');
expect(result[2].id).toBe('msg-4');
});
it('should handle user reply to tool message', () => {
const messages: Message[] = [
{
content: 'User request',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'user',
updatedAt: 0,
},
{
content: 'Using tool',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'msg-1',
role: 'assistant',
tools: [
{ apiName: 'test', arguments: '{}', id: 'tool-1', identifier: 'test', type: 'default' },
],
updatedAt: 0,
},
{
content: 'Tool result',
createdAt: 0,
id: 'tool-1',
meta: {},
parentId: 'msg-2',
role: 'tool',
tool_call_id: 'tool-1',
updatedAt: 0,
},
{
content: 'User reply to tool',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'tool-1',
role: 'user',
updatedAt: 0,
},
];
const builder = createBuilder(messages);
const result = builder.flatten(messages);
// msg-3 should be included even though it's a reply to tool
expect(result).toHaveLength(3);
expect(result[0].id).toBe('msg-1');
expect(result[1].role).toBe('assistantGroup');
expect(result[2].id).toBe('msg-3');
});
});
});

View file

@ -0,0 +1,220 @@
import { describe, expect, it } from 'vitest';
import type { IdNode, Message } from '../../types';
import { MessageCollector } from '../MessageCollector';
describe('MessageCollector', () => {
describe('collectGroupMembers', () => {
it('should collect messages with matching groupId', () => {
const messageMap = new Map<string, Message>();
const childrenMap = new Map<string | null, string[]>();
const collector = new MessageCollector(messageMap, childrenMap);
const messages: Message[] = [
{
content: '1',
createdAt: 0,
groupId: 'group-1',
id: 'msg-1',
meta: {},
role: 'assistant',
updatedAt: 0,
},
{
content: '2',
createdAt: 0,
groupId: 'group-1',
id: 'msg-2',
meta: {},
role: 'assistant',
updatedAt: 0,
},
{
content: '3',
createdAt: 0,
groupId: 'group-2',
id: 'msg-3',
meta: {},
role: 'assistant',
updatedAt: 0,
},
];
const result = collector.collectGroupMembers('group-1', messages);
expect(result).toHaveLength(2);
expect(result.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
});
});
describe('collectToolMessages', () => {
it('should collect tool messages matching assistant tool call IDs', () => {
const messageMap = new Map<string, Message>();
const childrenMap = new Map<string | null, string[]>();
const collector = new MessageCollector(messageMap, childrenMap);
const assistant: Message = {
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'assistant',
tools: [
{ apiName: 'tool1', arguments: '{}', id: 'tool-1', identifier: 'test', type: 'default' },
{ apiName: 'tool2', arguments: '{}', id: 'tool-2', identifier: 'test', type: 'default' },
],
updatedAt: 0,
};
const messages: Message[] = [
{
content: 'result1',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'msg-1',
role: 'tool',
tool_call_id: 'tool-1',
updatedAt: 0,
},
{
content: 'result2',
createdAt: 0,
id: 'msg-3',
meta: {},
parentId: 'msg-1',
role: 'tool',
tool_call_id: 'tool-2',
updatedAt: 0,
},
{
content: 'other',
createdAt: 0,
id: 'msg-4',
meta: {},
parentId: 'msg-1',
role: 'tool',
tool_call_id: 'tool-3',
updatedAt: 0,
},
];
const result = collector.collectToolMessages(assistant, messages);
expect(result).toHaveLength(2);
expect(result.map((m) => m.id)).toEqual(['msg-2', 'msg-3']);
});
});
describe('findLastNodeInAssistantGroup', () => {
it('should return the node itself if no tool children', () => {
const messageMap = new Map<string, Message>();
const childrenMap = new Map<string | null, string[]>();
const collector = new MessageCollector(messageMap, childrenMap);
const idNode: IdNode = {
children: [],
id: 'msg-1',
};
const result = collector.findLastNodeInAssistantGroup(idNode);
expect(result).toEqual(idNode);
});
it('should return last tool node if no assistant children', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'test',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
[
'tool-1',
{
content: 'result',
createdAt: 0,
id: 'tool-1',
meta: {},
parentId: 'msg-1',
role: 'tool',
updatedAt: 0,
},
],
]);
const childrenMap = new Map<string | null, string[]>();
const collector = new MessageCollector(messageMap, childrenMap);
const idNode: IdNode = {
children: [{ children: [], id: 'tool-1' }],
id: 'msg-1',
};
const result = collector.findLastNodeInAssistantGroup(idNode);
expect(result?.id).toBe('tool-1');
});
it('should follow assistant chain recursively', () => {
const messageMap = new Map<string, Message>([
[
'msg-1',
{
content: 'test1',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'assistant',
updatedAt: 0,
},
],
[
'tool-1',
{
content: 'result1',
createdAt: 0,
id: 'tool-1',
meta: {},
parentId: 'msg-1',
role: 'tool',
updatedAt: 0,
},
],
[
'msg-2',
{
content: 'test2',
createdAt: 0,
id: 'msg-2',
meta: {},
parentId: 'tool-1',
role: 'assistant',
updatedAt: 0,
},
],
]);
const childrenMap = new Map<string | null, string[]>();
const collector = new MessageCollector(messageMap, childrenMap);
const idNode: IdNode = {
children: [
{
children: [{ children: [], id: 'msg-2' }],
id: 'tool-1',
},
],
id: 'msg-1',
};
const result = collector.findLastNodeInAssistantGroup(idNode);
expect(result?.id).toBe('msg-2');
});
});
});

View file

@ -0,0 +1,287 @@
import { describe, expect, it } from 'vitest';
import type { AssistantContentBlock } from '@/types/index';
import type { Message } from '../../types';
import { MessageTransformer } from '../MessageTransformer';
describe('MessageTransformer', () => {
const transformer = new MessageTransformer();
describe('messageToContentBlock', () => {
it('should convert message to content block', () => {
const message: Message = {
content: 'Hello',
createdAt: 0,
id: 'msg-1',
meta: {},
metadata: {
cost: 0.001,
duration: 1000,
totalInputTokens: 10,
totalOutputTokens: 20,
totalTokens: 30,
tps: 20,
},
role: 'assistant',
updatedAt: 0,
};
const result = transformer.messageToContentBlock(message);
expect(result).toEqual({
content: 'Hello',
error: undefined,
id: 'msg-1',
imageList: undefined,
performance: {
duration: 1000,
tps: 20,
},
reasoning: undefined,
tools: undefined,
usage: {
cost: 0.001,
totalInputTokens: 10,
totalOutputTokens: 20,
totalTokens: 30,
},
});
});
it('should handle message without metadata', () => {
const message: Message = {
content: 'Hello',
createdAt: 0,
id: 'msg-1',
meta: {},
role: 'assistant',
updatedAt: 0,
};
const result = transformer.messageToContentBlock(message);
expect(result.usage).toBeUndefined();
expect(result.performance).toBeUndefined();
});
});
describe('splitMetadata', () => {
it('should split metadata into usage and performance', () => {
const metadata = {
cost: 0.001,
duration: 1000,
latency: 1100,
totalInputTokens: 10,
totalOutputTokens: 20,
totalTokens: 30,
tps: 20,
ttft: 100,
};
const result = transformer.splitMetadata(metadata);
expect(result.usage).toEqual({
cost: 0.001,
totalInputTokens: 10,
totalOutputTokens: 20,
totalTokens: 30,
});
expect(result.performance).toEqual({
duration: 1000,
latency: 1100,
tps: 20,
ttft: 100,
});
});
it('should handle undefined metadata', () => {
const result = transformer.splitMetadata(undefined);
expect(result).toEqual({});
});
it('should handle empty metadata', () => {
const result = transformer.splitMetadata({});
expect(result).toEqual({
performance: undefined,
usage: undefined,
});
});
it('should handle partial metadata', () => {
const result = transformer.splitMetadata({
cost: 0.001,
duration: 1000,
});
expect(result.usage).toEqual({
cost: 0.001,
});
expect(result.performance).toEqual({
duration: 1000,
});
});
});
describe('aggregateMetadata', () => {
it('should aggregate usage and performance from multiple children', () => {
const children: AssistantContentBlock[] = [
{
content: 'First',
id: 'msg-1',
performance: {
duration: 1000,
latency: 1100,
tps: 20,
ttft: 100,
},
usage: {
cost: 0.001,
totalInputTokens: 10,
totalOutputTokens: 20,
totalTokens: 30,
},
},
{
content: 'Second',
id: 'msg-2',
performance: {
duration: 2000,
latency: 2100,
tps: 30,
},
usage: {
cost: 0.002,
totalInputTokens: 15,
totalOutputTokens: 25,
totalTokens: 40,
},
},
];
const result = transformer.aggregateMetadata(children);
expect(result.usage).toEqual({
cost: 0.003,
totalInputTokens: 25,
totalOutputTokens: 45,
totalTokens: 70,
});
expect(result.performance).toEqual({
duration: 3000,
latency: 3200,
tps: 25, // average of 20 and 30
ttft: 100, // first value
});
});
it('should handle empty children array', () => {
const result = transformer.aggregateMetadata([]);
expect(result).toEqual({
performance: undefined,
usage: undefined,
});
});
it('should handle children without metadata', () => {
const children: AssistantContentBlock[] = [
{
content: 'First',
id: 'msg-1',
},
{
content: 'Second',
id: 'msg-2',
},
];
const result = transformer.aggregateMetadata(children);
expect(result).toEqual({
performance: undefined,
usage: undefined,
});
});
it('should handle mixed children (some with metadata, some without)', () => {
const children: AssistantContentBlock[] = [
{
content: 'First',
id: 'msg-1',
usage: {
cost: 0.001,
totalTokens: 30,
},
},
{
content: 'Second',
id: 'msg-2',
},
{
content: 'Third',
id: 'msg-3',
usage: {
cost: 0.002,
totalTokens: 40,
},
},
];
const result = transformer.aggregateMetadata(children);
expect(result.usage).toEqual({
cost: 0.003,
totalTokens: 70,
});
});
it('should average tps correctly', () => {
const children: AssistantContentBlock[] = [
{
content: 'First',
id: 'msg-1',
performance: { tps: 10 },
},
{
content: 'Second',
id: 'msg-2',
performance: { tps: 20 },
},
{
content: 'Third',
id: 'msg-3',
performance: { tps: 30 },
},
];
const result = transformer.aggregateMetadata(children);
expect(result.performance?.tps).toBe(20); // average of 10, 20, 30
});
it('should take first ttft value only', () => {
const children: AssistantContentBlock[] = [
{
content: 'First',
id: 'msg-1',
performance: { ttft: 100 },
},
{
content: 'Second',
id: 'msg-2',
performance: { ttft: 200 },
},
];
const result = transformer.aggregateMetadata(children);
expect(result.performance?.ttft).toBe(100); // first value only
});
});
});

View file

@ -0,0 +1,78 @@
import type { ContextNode, HelperMaps, IdNode, Message } from '../types';
import { BranchResolver } from './BranchResolver';
import { ContextTreeBuilder } from './ContextTreeBuilder';
import { FlatListBuilder } from './FlatListBuilder';
import { MessageCollector } from './MessageCollector';
import { MessageTransformer } from './MessageTransformer';
/**
* Phase 3: Transformation
* Converts structural idTree into semantic contextTree and flatList
*
* This is the main coordinator that delegates to specialized builders:
* - ContextTreeBuilder: Builds the tree structure for UI rendering
* - FlatListBuilder: Builds the flat list for API consumption
*/
export class Transformer {
private messageMap: Map<string, Message>;
private nodeIdCounter = 0;
// Utility classes
private branchResolver: BranchResolver;
private messageCollector: MessageCollector;
private messageTransformer: MessageTransformer;
// Builder classes
private contextTreeBuilder: ContextTreeBuilder;
private flatListBuilder: FlatListBuilder;
constructor(private helperMaps: HelperMaps) {
this.messageMap = helperMaps.messageMap;
// Initialize utility classes
this.branchResolver = new BranchResolver();
this.messageCollector = new MessageCollector(this.messageMap, helperMaps.childrenMap);
this.messageTransformer = new MessageTransformer();
// Initialize builder classes
this.contextTreeBuilder = new ContextTreeBuilder(
helperMaps.messageMap,
helperMaps.messageGroupMap,
this.branchResolver,
this.messageCollector,
this.generateNodeId.bind(this),
);
this.flatListBuilder = new FlatListBuilder(
helperMaps.messageMap,
helperMaps.messageGroupMap,
helperMaps.childrenMap,
this.branchResolver,
this.messageCollector,
this.messageTransformer,
);
}
/**
* Generates a unique node ID
*/
private generateNodeId(prefix: string, messageId: string): string {
return `${prefix}-${messageId}-${this.nodeIdCounter++}`;
}
/**
* Transform all root nodes to contextTree
* Returns a linear array of context nodes for UI rendering
*/
transformAll(idNodes: IdNode[]): ContextNode[] {
return this.contextTreeBuilder.transformAll(idNodes);
}
/**
* Generate flatList from messages array
* Only includes messages in the active path for API consumption
*/
flatten(messages: Message[]): Message[] {
return this.flatListBuilder.flatten(messages);
}
}

View file

@ -0,0 +1,36 @@
/**
* Type Index
*
* Centralized exports for all conversation flow types.
* Types are organized into three categories:
*
* 1. Context Tree (types/contextTree.ts) - Tree structure for navigation
* 2. Flat Message List (types/flatMessageList.ts) - Optimized for rendering
* 3. Shared (types/shared.ts) - Common types used across modules
*/
// Context Tree Types
export type {
AssistantGroupNode,
BranchNode,
CompareNode,
ContextNode,
MessageNode,
} from './types/contextTree';
// Flat Message List Types
export type {
BranchMetadata,
FlatMessage,
FlatMessageExtra,
FlatMessageRole,
} from './types/flatMessageList';
// Shared Types
export type {
HelperMaps,
IdNode,
Message,
MessageGroupMetadata,
ParseResult,
} from './types/shared';

View file

@ -0,0 +1,65 @@
/**
* Context Tree Types
*
* Tree structure for understanding conversation flow and navigation.
* Used for complex operations like branch switching and context understanding.
*/
/**
* Base interface for all display nodes
*/
interface BaseNode {
/** Unique identifier for this node */
id: string;
/** Type discriminator */
type: 'message' | 'assistantGroup' | 'compare' | 'branch';
}
/**
* Basic message node - leaf node representing a single message
*/
export interface MessageNode extends BaseNode {
/** Tool message IDs (for assistant messages with tool calls) */
tools?: string[];
type: 'message';
}
/**
* Assistant group node - aggregates an assistant message with its tool calls
*/
export interface AssistantGroupNode extends BaseNode {
/** Child nodes (assistant and tool messages) */
children: ContextNode[];
type: 'assistantGroup';
}
/**
* Compare node - renders multiple parallel outputs side by side
*/
export interface CompareNode extends BaseNode {
/** ID of the active column that enters LLM context */
activeColumnId?: string;
/** Each column represents a parallel output tree */
columns: ContextNode[][];
/** The message that triggered the comparison */
messageId: string;
type: 'compare';
}
/**
* Branch node - represents multiple alternate conversation paths
*/
export interface BranchNode extends BaseNode {
/** Index of the currently active branch */
activeBranchIndex: number;
/** Each branch is a separate conversation tree */
branches: ContextNode[][];
/** The parent message that has multiple branches */
parentMessageId: string;
type: 'branch';
}
/**
* Union type of all display nodes
*/
export type ContextNode = MessageNode | AssistantGroupNode | CompareNode | BranchNode;

View file

@ -0,0 +1,66 @@
import type { UIChatMessage } from '@lobechat/types';
/**
* Flat Message List Types
*
* Flattened array optimized for virtual list rendering.
* Contains virtual messages with extended role types for grouped display.
*/
/**
* Extended message role types for flat list rendering
*
* Standard roles from UIChatMessage:
* - 'user': User message
* - 'assistant': Assistant message (standalone, without tools)
* - 'tool': Tool execution result
* - 'system': System message
* - 'supervisor': Supervisor message in multi-agent
*
* Virtual roles created by parse():
* - 'assistantGroup': Assistant message + tool calls aggregation
* - 'messageGroup': Generic message group (manual/summary)
* - 'compare': Compare mode for parallel model outputs
*/
export type FlatMessageRole =
| 'user'
| 'assistant'
| 'tool'
| 'system'
| 'supervisor'
| 'assistantGroup'
| 'messageGroup'
| 'compare';
/**
* Message in flat list
*
* Can be either:
* 1. Original message from database
* 2. Virtual message created by parse() with extended role and children
*/
export type FlatMessage = UIChatMessage;
/**
* Branch metadata attached to user messages
*/
export interface BranchMetadata {
/** Active branch message ID */
activeId: string;
/** All branch message IDs */
branchIds: string[];
}
/**
* Virtual message extra fields for flat list
*/
export interface FlatMessageExtra {
/** Branch information for user messages with multiple children */
branches?: BranchMetadata;
/** Optional description for groups */
description?: string;
/** Group mode for messageGroup and compare virtual messages */
groupMode?: 'compare' | 'manual' | 'summary';
/** Parent message ID that triggered this group */
parentMessageId?: string;
}

View file

@ -0,0 +1,63 @@
import type { UIChatMessage } from '@lobechat/types';
import type { ContextNode } from './contextTree';
import type { FlatMessage } from './flatMessageList';
/**
* Shared Types
*
* Common types used across the conversation flow engine.
*/
/**
* Re-export UIChatMessage as Message for convenience
*/
export type Message = UIChatMessage;
/**
* Message group metadata from database
* Used for multi-model parallel conversations and manual grouping
*/
export interface MessageGroupMetadata {
description?: string;
id: string;
/** Presentation mode: compare, summary, or manual */
mode?: 'compare' | 'summary' | 'manual';
/** Parent message that triggered this group */
parentMessageId?: string;
title?: string;
}
/**
* Internal structure node used during tree building
*/
export interface IdNode {
children: IdNode[];
id: string;
}
/**
* Result of the parse function
*/
export interface ParseResult {
/** Semantic tree structure for navigation and context understanding */
contextTree: ContextNode[];
/** Flattened list optimized for virtual list rendering */
flatList: FlatMessage[];
/** Map for O(1) message access */
messageMap: Record<string, Message>;
}
/**
* Internal helper maps used during parsing
*/
export interface HelperMaps {
/** Maps parent ID to array of child IDs */
childrenMap: Map<string | null, string[]>;
/** Maps message group ID to its metadata */
messageGroupMap: Map<string, MessageGroupMetadata>;
/** Maps message ID to message */
messageMap: Map<string, Message>;
/** Maps thread ID to all messages in that thread */
threadMap: Map<string, Message[]>;
}

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
reporter: ['text', 'json', 'lcov', 'text-summary'],
},
environment: 'happy-dom',
},
});

View file

@ -103,4 +103,8 @@ export interface ModelPerformance {
latency?: number;
}
export interface MessageMetadata extends ModelUsage, ModelPerformance {}
export interface MessageMetadata extends ModelUsage, ModelPerformance {
activeBranchIndex?: number;
activeColumn?: boolean;
compare?: boolean;
}

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ChatMessageError,
ChatMessagePluginError,