Download packages/transcript/src/ops/apply.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 21 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/transcript/src/ops/apply.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/transcript/src/ops/apply.ts
-
curl -L -o apply.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/transcript/src/ops/apply.ts
21 kB
| import type { AttachmentId, InteractionId, PromptId, TaskId, TodoId, TurnId } from '../model/ids'; | |
| import { turnOrdinal } from '../model/ids'; | |
| import type { TranscriptAttachment } from '../model/attachment'; | |
| import type { TranscriptFrame } from '../model/frame'; | |
| import type { TranscriptInteraction } from '../model/interaction'; | |
| import type { TranscriptItem } from '../model/item'; | |
| import type { TranscriptMeta, TranscriptMetaMerge } from '../model/meta'; | |
| import type { TranscriptPrompt } from '../model/prompt'; | |
| import type { TranscriptTask } from '../model/task'; | |
| import type { TranscriptTodo } from '../model/todo'; | |
| import type { TranscriptStep, TranscriptTurn } from '../model/turn'; | |
| import type { | |
| AppendOp, | |
| TranscriptOperation, | |
| TurnHeader, | |
| StepHeader, | |
| } from './operation'; | |
| export interface AgentState { | |
| readonly items: readonly TranscriptItem[]; | |
| readonly tasks: ReadonlyMap<TaskId, TranscriptTask>; | |
| readonly interactions: ReadonlyMap<InteractionId, TranscriptInteraction>; | |
| readonly attachments: ReadonlyMap<AttachmentId, TranscriptAttachment>; | |
| readonly todos: ReadonlyMap<TodoId, TranscriptTodo>; | |
| readonly prompts: ReadonlyMap<PromptId, TranscriptPrompt>; | |
| readonly meta: TranscriptMeta; | |
| readonly pendingInteractions: ReadonlySet<InteractionId>; | |
| readonly hasMoreOlder: boolean; | |
| } | |
| export const EMPTY_AGENT_STATE: AgentState = { | |
| items: [], | |
| tasks: new Map(), | |
| interactions: new Map(), | |
| attachments: new Map(), | |
| todos: new Map(), | |
| prompts: new Map(), | |
| meta: {}, | |
| pendingInteractions: new Set(), | |
| hasMoreOlder: false, | |
| }; | |
| export interface ApplyResult { | |
| readonly state: AgentState; | |
| readonly changed: boolean; | |
| readonly gap?: { readonly expected: number; readonly got: number }; | |
| } | |
| export function applyOperation(state: AgentState, op: TranscriptOperation): ApplyResult { | |
| switch (op.op) { | |
| case 'reset': | |
| return applyReset(state, op); | |
| case 'turn.upsert': | |
| return applyTurnUpsert(state, op.turn); | |
| case 'step.upsert': | |
| return applyStepUpsert(state, op.turnId, op.step); | |
| case 'frame.upsert': | |
| return applyFrameUpsert(state, op); | |
| case 'append': | |
| return applyAppend(state, op); | |
| case 'marker.upsert': | |
| return applyItemUpsert(state, op.item, op.item.markerId, op.beforeTurn); | |
| case 'taskref.upsert': | |
| return applyItemUpsert(state, op.item, op.item.refId, op.beforeTurn); | |
| case 'task.upsert': | |
| return applyTaskUpsert(state, op.task); | |
| case 'interaction.upsert': | |
| return applyInteractionUpsert(state, op.interaction); | |
| case 'attachment.upsert': | |
| return applyAttachmentUpsert(state, op.attachment); | |
| case 'todo.upsert': | |
| return applyTodoUpsert(state, op.todo); | |
| case 'prompt.upsert': | |
| return applyPromptUpsert(state, op.prompt); | |
| case 'meta.merge': | |
| return applyMetaMerge(state, op.meta); | |
| case 'items.remove': | |
| return applyItemsRemove(state, op.ids); | |
| } | |
| } | |
| function applyReset(state: AgentState, op: Extract<TranscriptOperation, { op: 'reset' }>): ApplyResult { | |
| const pending = new Set<InteractionId>(); | |
| for (const interaction of op.snapshot.interactions) { | |
| if (interaction.state === 'pending') pending.add(interaction.interactionId); | |
| } | |
| return { | |
| state: { | |
| items: op.snapshot.items, | |
| tasks: new Map(op.snapshot.tasks.map((task) => [task.taskId, task])), | |
| interactions: new Map( | |
| op.snapshot.interactions.map((interaction) => [interaction.interactionId, interaction]), | |
| ), | |
| attachments: new Map( | |
| op.snapshot.attachments.map((attachment) => [attachment.attachmentId, attachment]), | |
| ), | |
| todos: new Map(op.snapshot.todos.map((todo) => [todo.todoId, todo])), | |
| prompts: new Map(op.snapshot.prompts.map((prompt) => [prompt.promptId, prompt])), | |
| meta: op.snapshot.meta, | |
| pendingInteractions: pending, | |
| hasMoreOlder: op.snapshot.hasMoreOlder ?? false, | |
| }, | |
| changed: true, | |
| }; | |
| } | |
| function turnHeaderToTurn(header: TurnHeader, steps: readonly TranscriptStep[]): TranscriptTurn { | |
| return { ...header, kind: 'turn', steps: [...steps] }; | |
| } | |
| function skeletonTurn(turnId: TurnId): TranscriptTurn { | |
| return { | |
| kind: 'turn', | |
| turnId, | |
| ordinal: turnOrdinal(turnId), | |
| state: 'running', | |
| origin: { kind: 'other' }, | |
| steps: [], | |
| }; | |
| } | |
| function skeletonStep(stepId: string, turnId: TurnId): TranscriptStep { | |
| const ordinal = Number(stepId.slice(turnId.length + 1)) || 0; | |
| return { kind: 'step', stepId, turnId, ordinal, state: 'running', frames: [] }; | |
| } | |
| function getTurn(state: AgentState, turnId: TurnId): TranscriptTurn | undefined { | |
| const item = state.items.find((entry) => entry.kind === 'turn' && entry.turnId === turnId); | |
| return item?.kind === 'turn' ? item : undefined; | |
| } | |
| function insertTurn(items: readonly TranscriptItem[], turn: TranscriptTurn): readonly TranscriptItem[] { | |
| const next = [...items]; | |
| let at = next.length; | |
| for (let i = 0; i < next.length; i += 1) { | |
| const entry = next[i]; | |
| if (entry?.kind === 'turn' && entry.ordinal > turn.ordinal) { | |
| at = i; | |
| break; | |
| } | |
| } | |
| next.splice(at, 0, turn); | |
| return next; | |
| } | |
| function replaceTurn( | |
| items: readonly TranscriptItem[], | |
| turnId: TurnId, | |
| fn: (turn: TranscriptTurn) => TranscriptTurn, | |
| ): readonly TranscriptItem[] { | |
| return items.map((entry) => | |
| entry.kind === 'turn' && entry.turnId === turnId ? fn(entry) : entry, | |
| ); | |
| } | |
| function applyTurnUpsert(state: AgentState, header: TurnHeader): ApplyResult { | |
| const existing = getTurn(state, header.turnId); | |
| if (existing) { | |
| if (turnEquals(existing, header)) return { state, changed: false }; | |
| return { | |
| state: { | |
| ...state, | |
| items: replaceTurn(state.items, header.turnId, (turn) => | |
| turnHeaderToTurn(header, turn.steps), | |
| ), | |
| }, | |
| changed: true, | |
| }; | |
| } | |
| return { | |
| state: { ...state, items: insertTurn(state.items, turnHeaderToTurn(header, [])) }, | |
| changed: true, | |
| }; | |
| } | |
| function turnEquals(turn: TranscriptTurn, header: TurnHeader): boolean { | |
| return ( | |
| turn.ordinal === header.ordinal && | |
| turn.triggerPromptId === header.triggerPromptId && | |
| turn.state === header.state && | |
| turn.prompt === header.prompt && | |
| turn.attachmentIds === header.attachmentIds && | |
| turn.startedAt === header.startedAt && | |
| turn.endedAt === header.endedAt && | |
| turn.origin.kind === header.origin.kind && | |
| turn.origin.payload === header.origin.payload && | |
| turn.usage === header.usage && | |
| turn.durationMs === header.durationMs && | |
| turn.error === header.error | |
| ); | |
| } | |
| function applyStepUpsert(state: AgentState, turnId: TurnId, header: StepHeader): ApplyResult { | |
| const turn = getTurn(state, turnId) ?? skeletonTurn(turnId); | |
| const stepIndex = turn.steps.findIndex((step) => step.stepId === header.stepId); | |
| let steps: readonly TranscriptStep[]; | |
| let changed = true; | |
| if (stepIndex >= 0) { | |
| const current = turn.steps[stepIndex]; | |
| if (current && stepEquals(current, header)) { | |
| changed = false; | |
| steps = turn.steps; | |
| } else { | |
| steps = turn.steps.map((step) => | |
| step.stepId === header.stepId ? { ...header, kind: 'step' as const, frames: step.frames } : step, | |
| ); | |
| } | |
| } else { | |
| steps = [...turn.steps, { ...header, kind: 'step' as const, frames: [] }].toSorted( | |
| (a, b) => a.ordinal - b.ordinal, | |
| ); | |
| } | |
| if (!changed) return { state, changed: false }; | |
| const nextTurn: TranscriptTurn = { ...turn, steps: [...steps] }; | |
| const items = getTurn(state, turnId) | |
| ? replaceTurn(state.items, turnId, () => nextTurn) | |
| : insertTurn(state.items, nextTurn); | |
| return { state: { ...state, items }, changed: true }; | |
| } | |
| function stepEquals(step: TranscriptStep, header: StepHeader): boolean { | |
| return ( | |
| step.ordinal === header.ordinal && | |
| step.state === header.state && | |
| step.startedAt === header.startedAt && | |
| step.endedAt === header.endedAt && | |
| step.usage === header.usage && | |
| step.finishReason === header.finishReason && | |
| step.timing === header.timing && | |
| step.retry === header.retry && | |
| step.endReason === header.endReason && | |
| step.endMessage === header.endMessage | |
| ); | |
| } | |
| function applyFrameUpsert( | |
| state: AgentState, | |
| op: Extract<TranscriptOperation, { op: 'frame.upsert' }>, | |
| ): ApplyResult { | |
| const turn = getTurn(state, op.turnId) ?? skeletonTurn(op.turnId); | |
| const step = turn.steps.find((entry) => entry.stepId === op.stepId) ?? skeletonStep(op.stepId, op.turnId); | |
| const existing = step.frames.findIndex((frame) => frame.frameId === op.frame.frameId); | |
| let frames: readonly TranscriptFrame[]; | |
| if (existing >= 0) { | |
| const current = step.frames[existing]; | |
| if (current !== undefined && frameEquals(current, op.frame)) { | |
| return { state, changed: false }; | |
| } | |
| frames = step.frames.map((frame) => (frame.frameId === op.frame.frameId ? op.frame : frame)); | |
| } else { | |
| frames = [...step.frames, op.frame]; | |
| } | |
| const nextStep: TranscriptStep = { ...step, frames: [...frames] }; | |
| const steps = turn.steps.some((entry) => entry.stepId === op.stepId) | |
| ? turn.steps.map((entry) => (entry.stepId === op.stepId ? nextStep : entry)) | |
| : [...turn.steps, nextStep].toSorted((a, b) => a.ordinal - b.ordinal); | |
| const nextTurn: TranscriptTurn = { ...turn, steps }; | |
| const items = getTurn(state, op.turnId) | |
| ? replaceTurn(state.items, op.turnId, () => nextTurn) | |
| : insertTurn(state.items, nextTurn); | |
| return { | |
| state: { ...state, items }, | |
| changed: true, | |
| }; | |
| } | |
| function frameEquals(a: TranscriptFrame, b: TranscriptFrame): boolean { | |
| if (a.kind !== b.kind) return false; | |
| if (a.kind === 'text' && b.kind === 'text') { | |
| return ( | |
| a.text === b.text && | |
| a.role === b.role && | |
| a.attachmentIds === b.attachmentIds && | |
| a.taskId === b.taskId | |
| ); | |
| } | |
| if (a.kind === 'thinking' && b.kind === 'thinking') return a.text === b.text; | |
| if (a.kind === 'tool' && b.kind === 'tool') { | |
| return ( | |
| a.state === b.state && | |
| a.toolCallId === b.toolCallId && | |
| a.name === b.name && | |
| a.view === b.view && | |
| a.input === b.input && | |
| a.output === b.output && | |
| a.display === b.display && | |
| a.error === b.error && | |
| a.inputText === b.inputText && | |
| a.progress === b.progress && | |
| a.taskId === b.taskId && | |
| a.approvalId === b.approvalId && | |
| a.todoId === b.todoId && | |
| a.agentRefs === b.agentRefs | |
| ); | |
| } | |
| if (a.kind === 'notice' && b.kind === 'notice') { | |
| return a.message === b.message && a.level === b.level && a.detail === b.detail; | |
| } | |
| return false; | |
| } | |
| function applyAppend(state: AgentState, op: AppendOp): ApplyResult { | |
| if (op.target.type === 'task') return applyTaskAppend(state, op); | |
| const { turnId, stepId, frameId } = op.target; | |
| const turn = getTurn(state, turnId); | |
| const step = turn?.steps.find((entry) => entry.stepId === stepId); | |
| const frame = step?.frames.find((entry) => entry.frameId === frameId); | |
| if (!turn || !step || !frame || (frame.kind !== 'text' && frame.kind !== 'thinking')) { | |
| return { state, changed: false, gap: { expected: 0, got: op.offset } }; | |
| } | |
| const merged = appendAtOffset(frame.text, op.offset, op.text); | |
| if (merged.gap) return { state, changed: false, gap: merged.gap }; | |
| if (!merged.changed) return { state, changed: false }; | |
| const nextFrame = { ...frame, text: merged.text }; | |
| const nextStep: TranscriptStep = { | |
| ...step, | |
| frames: step.frames.map((entry) => (entry.frameId === frameId ? nextFrame : entry)), | |
| }; | |
| const nextTurn: TranscriptTurn = { | |
| ...turn, | |
| steps: turn.steps.map((entry) => (entry.stepId === stepId ? nextStep : entry)), | |
| }; | |
| return { | |
| state: { ...state, items: replaceTurn(state.items, turnId, () => nextTurn) }, | |
| changed: true, | |
| }; | |
| } | |
| function applyTaskAppend(state: AgentState, op: AppendOp): ApplyResult { | |
| if (op.target.type !== 'task') throw new Error('unreachable'); | |
| const taskId = op.target.taskId; | |
| const task = state.tasks.get(taskId); | |
| const current = task?.outputTail ?? ''; | |
| const merged = appendAtOffset(current, op.offset, op.text); | |
| if (merged.gap) return { state, changed: false, gap: merged.gap }; | |
| if (!merged.changed) return { state, changed: false }; | |
| const nextTask: TranscriptTask = task | |
| ? { ...task, outputTail: merged.text } | |
| : { taskId, kind: 'other', state: 'running', detached: false, outputTail: merged.text }; | |
| const tasks = new Map(state.tasks); | |
| tasks.set(taskId, nextTask); | |
| return { state: { ...state, tasks }, changed: true }; | |
| } | |
| export function appendAtOffset( | |
| local: string, | |
| offset: number, | |
| chunk: string, | |
| ): { text: string; changed: boolean; gap?: { expected: number; got: number } } { | |
| if (offset > local.length) return { text: local, changed: false, gap: { expected: local.length, got: offset } }; | |
| if (local.slice(offset, offset + chunk.length) === chunk) { | |
| return { text: local, changed: false }; | |
| } | |
| const overlap = local.length - offset; | |
| if (local.slice(offset) !== chunk.slice(0, overlap)) { | |
| return { text: local, changed: false, gap: { expected: local.length, got: offset } }; | |
| } | |
| const novel = overlap > 0 ? chunk.slice(overlap) : chunk; | |
| if (novel.length === 0) return { text: local, changed: false }; | |
| return { text: local.slice(0, offset) + chunk, changed: true }; | |
| } | |
| function applyItemUpsert( | |
| state: AgentState, | |
| item: TranscriptItem, | |
| id: string, | |
| beforeTurn?: number, | |
| ): ApplyResult { | |
| const exists = state.items.some((entry) => itemIdOf(entry) === id); | |
| if (exists) { | |
| let changed = false; | |
| const items = state.items.map((entry) => { | |
| if (itemIdOf(entry) !== id) return entry; | |
| if (entry === item) return entry; | |
| changed = true; | |
| return item; | |
| }); | |
| if (!changed) return { state, changed: false }; | |
| return { state: { ...state, items }, changed: true }; | |
| } | |
| if (beforeTurn !== undefined) { | |
| const items = [...state.items]; | |
| let at = items.length; | |
| for (let i = 0; i < items.length; i += 1) { | |
| const entry = items[i]; | |
| if (entry?.kind === 'turn' && entry.ordinal >= beforeTurn) { | |
| at = i; | |
| break; | |
| } | |
| } | |
| items.splice(at, 0, item); | |
| return { state: { ...state, items }, changed: true }; | |
| } | |
| return { state: { ...state, items: [...state.items, item] }, changed: true }; | |
| } | |
| function itemIdOf(item: TranscriptItem): string { | |
| switch (item.kind) { | |
| case 'turn': | |
| return item.turnId; | |
| case 'marker': | |
| return item.markerId; | |
| case 'taskref': | |
| return item.refId; | |
| } | |
| } | |
| function applyItemsRemove(state: AgentState, ids: readonly string[]): ApplyResult { | |
| const drop = new Set(ids); | |
| const removedTurns = state.items.filter( | |
| (entry): entry is TranscriptTurn => entry.kind === 'turn' && drop.has(entry.turnId), | |
| ); | |
| const items = state.items.filter((entry) => !drop.has(itemIdOf(entry))); | |
| if (items.length === state.items.length) return { state, changed: false }; | |
| let pending = state.pendingInteractions; | |
| let interactions = state.interactions; | |
| if (removedTurns.length > 0) { | |
| const anchoredToolCallIds = new Set<string>(); | |
| const nextPending = new Set(pending); | |
| const deadEntityIds = new Set<InteractionId>(); | |
| for (const turn of removedTurns) { | |
| for (const step of turn.steps) { | |
| for (const frame of step.frames) { | |
| if (frame.kind === 'tool') anchoredToolCallIds.add(frame.toolCallId); | |
| } | |
| } | |
| } | |
| for (const interaction of interactions.values()) { | |
| if (interaction.toolCallId !== undefined && anchoredToolCallIds.has(interaction.toolCallId)) { | |
| deadEntityIds.add(interaction.interactionId); | |
| nextPending.delete(interaction.interactionId); | |
| } | |
| } | |
| if (deadEntityIds.size > 0) { | |
| const nextInteractions = new Map(interactions); | |
| for (const id of deadEntityIds) nextInteractions.delete(id); | |
| interactions = nextInteractions; | |
| } | |
| pending = nextPending; | |
| } | |
| return { state: { ...state, items, interactions, pendingInteractions: pending }, changed: true }; | |
| } | |
| function applyTaskUpsert(state: AgentState, task: TranscriptTask): ApplyResult { | |
| const current = state.tasks.get(task.taskId); | |
| if (current && taskEquals(current, task)) return { state, changed: false }; | |
| const tasks = new Map(state.tasks); | |
| tasks.set(task.taskId, task); | |
| return { state: { ...state, tasks }, changed: true }; | |
| } | |
| function applyInteractionUpsert( | |
| state: AgentState, | |
| interaction: TranscriptInteraction, | |
| ): ApplyResult { | |
| const current = state.interactions.get(interaction.interactionId); | |
| if (current && interactionEquals(current, interaction)) return { state, changed: false }; | |
| const interactions = new Map(state.interactions); | |
| interactions.set(interaction.interactionId, interaction); | |
| let pending = state.pendingInteractions; | |
| if (interaction.state === 'pending') { | |
| if (!pending.has(interaction.interactionId)) { | |
| const next = new Set(pending); | |
| next.add(interaction.interactionId); | |
| pending = next; | |
| } | |
| } else if (pending.has(interaction.interactionId)) { | |
| const next = new Set(pending); | |
| next.delete(interaction.interactionId); | |
| pending = next; | |
| } | |
| return { state: { ...state, interactions, pendingInteractions: pending }, changed: true }; | |
| } | |
| function interactionEquals(a: TranscriptInteraction, b: TranscriptInteraction): boolean { | |
| return ( | |
| a.interactionKind === b.interactionKind && | |
| a.toolCallId === b.toolCallId && | |
| a.state === b.state && | |
| a.request === b.request && | |
| a.response === b.response | |
| ); | |
| } | |
| function applyAttachmentUpsert( | |
| state: AgentState, | |
| attachment: TranscriptAttachment, | |
| ): ApplyResult { | |
| const current = state.attachments.get(attachment.attachmentId); | |
| if (current && attachmentEquals(current, attachment)) return { state, changed: false }; | |
| const attachments = new Map(state.attachments); | |
| attachments.set(attachment.attachmentId, attachment); | |
| return { state: { ...state, attachments }, changed: true }; | |
| } | |
| function attachmentEquals(a: TranscriptAttachment, b: TranscriptAttachment): boolean { | |
| return ( | |
| a.mediaType === b.mediaType && | |
| a.name === b.name && | |
| a.size === b.size && | |
| a.source === b.source && | |
| a.placeholder === b.placeholder | |
| ); | |
| } | |
| function applyTodoUpsert(state: AgentState, todo: TranscriptTodo): ApplyResult { | |
| const current = state.todos.get(todo.todoId); | |
| if (current && todoEquals(current, todo)) return { state, changed: false }; | |
| const todos = new Map(state.todos); | |
| todos.set(todo.todoId, todo); | |
| return { state: { ...state, todos }, changed: true }; | |
| } | |
| function todoEquals(a: TranscriptTodo, b: TranscriptTodo): boolean { | |
| return a.items === b.items && a.updatedAt === b.updatedAt; | |
| } | |
| function applyPromptUpsert(state: AgentState, prompt: TranscriptPrompt): ApplyResult { | |
| const current = state.prompts.get(prompt.promptId); | |
| if (current && promptEquals(current, prompt)) return { state, changed: false }; | |
| const prompts = new Map(state.prompts); | |
| prompts.set(prompt.promptId, prompt); | |
| return { state: { ...state, prompts }, changed: true }; | |
| } | |
| function promptEquals(a: TranscriptPrompt, b: TranscriptPrompt): boolean { | |
| return ( | |
| a.status === b.status && | |
| a.userMessageId === b.userMessageId && | |
| a.content === b.content && | |
| a.createdAt === b.createdAt && | |
| a.finishedAt === b.finishedAt && | |
| a.steeredAt === b.steeredAt | |
| ); | |
| } | |
| function taskEquals(a: TranscriptTask, b: TranscriptTask): boolean { | |
| return ( | |
| a.kind === b.kind && | |
| a.state === b.state && | |
| a.detached === b.detached && | |
| a.description === b.description && | |
| a.agentId === b.agentId && | |
| a.outputTail === b.outputTail && | |
| a.startedAt === b.startedAt && | |
| a.endedAt === b.endedAt && | |
| a.resultSummary === b.resultSummary && | |
| a.error === b.error && | |
| a.stateReason === b.stateReason && | |
| a.usage === b.usage | |
| ); | |
| } | |
| function applyMetaMerge(state: AgentState, meta: TranscriptMetaMerge): ApplyResult { | |
| const modes = | |
| meta.modes !== undefined | |
| ? { | |
| plan: meta.modes.plan === null ? undefined : (meta.modes.plan ?? state.meta.modes?.plan), | |
| swarm: meta.modes.swarm === null ? undefined : (meta.modes.swarm ?? state.meta.modes?.swarm), | |
| tower: meta.modes.tower === null ? undefined : (meta.modes.tower ?? state.meta.modes?.tower), | |
| } | |
| : state.meta.modes; | |
| const agent = | |
| meta.agent !== undefined ? { ...state.meta.agent, ...meta.agent } : state.meta.agent; | |
| const next: TranscriptMeta = { | |
| goal: meta.goal === null ? undefined : (meta.goal ?? state.meta.goal), | |
| activity: meta.activity ?? state.meta.activity, | |
| modes: | |
| modes !== undefined && | |
| modes.plan === undefined && | |
| modes.swarm === undefined && | |
| modes.tower === undefined | |
| ? undefined | |
| : modes, | |
| agent, | |
| }; | |
| if ( | |
| next.goal === state.meta.goal && | |
| next.activity === state.meta.activity && | |
| next.modes === state.meta.modes && | |
| next.agent === state.meta.agent | |
| ) { | |
| return { state, changed: false }; | |
| } | |
| return { state: { ...state, meta: next }, changed: true }; | |
| } | |