* feat: multi-turn context and conversation arc memory PR 2E - Section 2.9, 2.10: - Add multiTurnContext.ts with turn tracking and state preservation - Add conversationArc.ts with goal/decision/milestone tracking - Wire into query.ts after tool execution - Feature-flags: MULTI_TURN_CONTEXT, CONVERSATION_ARC - Add comprehensive tests (22 passing) * feat(cli): add /knowledge command to manage native memory - Add /knowledge enable <yes|no> to toggle Knowledge Graph learning\n- Add /knowledge clear to reset memory\n- Add persistent knowledgeGraphEnabled setting to global config\n- Integrated user setting into the query execution loop * feat(cli): add /knowledge command (stable local-jsx version) - Resolve conflicts between .ts and .tsx files\n- Align with LocalJSXCommandCall signature\n- Fix onDone and args errors * test(cli): fix knowledge command tests by properly isolating global config * fix(cli): make knowledge command defensive against undefined args and leaky tests * fix(cli): correct data source for entity count and fix test isolation * fix(cli): reinforce knowledge test by explicitly defining property on test config * fix(cli): explicitly define property in test config to avoid undefined in CI * fix(cli): make knowledge tests resistant to global config mocks in CI * chore(memory): surgical improvements from architectural audit - Fix: Implement entity deduplication in Knowledge Graph\n- Fix: Ensure fact extraction from user messages in query loop\n- Fix: Refine regexes for better quality learning (less noise) --------- Co-authored-by: LifeJiggy <Bloomtonjovish@gmail.com>
134 lines
3.3 KiB
TypeScript
134 lines
3.3 KiB
TypeScript
/**
|
|
* Multi-Turn Context Tracking - Production Grade
|
|
*
|
|
* Tracks context across multiple tool use cycles.
|
|
* Preserves state between tool invocations.
|
|
*/
|
|
|
|
import { roughTokenCountEstimation } from '../services/tokenEstimation.js'
|
|
import type { Message } from '../types/message.js'
|
|
|
|
export interface TurnContext {
|
|
turnId: string
|
|
startTime: number
|
|
messages: Message[]
|
|
toolCalls: Array<{
|
|
id: string
|
|
name: string
|
|
input: Record<string, unknown>
|
|
timestamp: number
|
|
}>
|
|
state: Map<string, unknown>
|
|
tokens: number
|
|
}
|
|
|
|
export interface MultiTurnOptions {
|
|
maxTurns?: number
|
|
maxTokensPerTurn?: number
|
|
preserveState?: boolean
|
|
}
|
|
|
|
const DEFAULT_OPTIONS: Required<MultiTurnOptions> = {
|
|
maxTurns: 10,
|
|
maxTokensPerTurn: 50000,
|
|
preserveState: true,
|
|
}
|
|
|
|
let turnHistory: TurnContext[] = []
|
|
let currentTurn: TurnContext | null = null
|
|
let turnCounter = 0
|
|
let activeOptions: Required<MultiTurnOptions> = { ...DEFAULT_OPTIONS }
|
|
|
|
export function startNewTurn(): TurnContext {
|
|
const turn: TurnContext = {
|
|
turnId: `turn_${++turnCounter}_${Date.now()}`,
|
|
startTime: Date.now(),
|
|
messages: [],
|
|
toolCalls: [],
|
|
state: new Map(),
|
|
tokens: 0,
|
|
}
|
|
|
|
if (turnHistory.length >= activeOptions.maxTurns) {
|
|
turnHistory = turnHistory.slice(-activeOptions.maxTurns + 1)
|
|
}
|
|
|
|
currentTurn = turn
|
|
turnHistory.push(turn)
|
|
|
|
return turn
|
|
}
|
|
|
|
export function getCurrentTurn(): TurnContext | null {
|
|
return currentTurn
|
|
}
|
|
|
|
export function addMessageToTurn(message: Message): void {
|
|
const turn = currentTurn || startNewTurn()
|
|
turn.messages.push(message)
|
|
|
|
// Update token estimate
|
|
const content = typeof message.message.content === 'string'
|
|
? message.message.content
|
|
: JSON.stringify(message.message.content)
|
|
turn.tokens += roughTokenCountEstimation(content)
|
|
}
|
|
|
|
export function addToolCallToTurn(call: TurnContext['toolCalls'][0]): void {
|
|
const turn = currentTurn || startNewTurn()
|
|
turn.toolCalls.push(call)
|
|
}
|
|
|
|
export function setTurnState(key: string, value: unknown): void {
|
|
const turn = currentTurn || startNewTurn()
|
|
turn.state.set(key, value)
|
|
}
|
|
|
|
export function getTurnState<T>(key: string): T | undefined {
|
|
return currentTurn?.state.get(key) as T
|
|
}
|
|
|
|
export function getTurnHistory(): TurnContext[] {
|
|
return turnHistory
|
|
}
|
|
|
|
export function getRecentTurns(n: number): TurnContext[] {
|
|
return turnHistory.slice(-n)
|
|
}
|
|
|
|
export function getMultiTurnStats() {
|
|
return {
|
|
totalTurns: turnHistory.length,
|
|
totalTokens: turnHistory.reduce((acc, t) => acc + t.tokens, 0),
|
|
avgTokensPerTurn: turnHistory.length > 0
|
|
? Math.round(turnHistory.reduce((acc, t) => acc + t.tokens, 0) / turnHistory.length)
|
|
: 0,
|
|
}
|
|
}
|
|
|
|
export function clearTurnHistory(): void {
|
|
turnHistory = []
|
|
currentTurn = null
|
|
}
|
|
|
|
export function resetMultiTurnState(): void {
|
|
clearTurnHistory()
|
|
turnCounter = 0
|
|
}
|
|
|
|
export function createMultiTurnTracker(options: MultiTurnOptions = {}) {
|
|
activeOptions = { ...DEFAULT_OPTIONS, ...options }
|
|
return {
|
|
startTurn: startNewTurn,
|
|
getCurrentTurn,
|
|
addMessage: addMessageToTurn,
|
|
addToolCall: addToolCallToTurn,
|
|
setState: (k: string, v: unknown) => setTurnState(k, v),
|
|
getState: <T>(k: string) => getTurnState<T>(k),
|
|
getHistory: getTurnHistory,
|
|
getRecent: (n: number) => getRecentTurns(n),
|
|
getStats: getMultiTurnStats,
|
|
reset: resetMultiTurnState,
|
|
}
|
|
}
|