broken refactor

This commit is contained in:
Your Name
2026-04-07 20:59:26 +00:00
parent 370e2b9e1d
commit 256a7a83fa
21 changed files with 355 additions and 85 deletions
+81 -7
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Episode } from './types.js';
import type { Episode, IrNode } from './types.js';
export interface MutationRecord {
episodeId: string;
@@ -19,27 +19,89 @@ export class EpisodeEditor {
private workingOrder: string[];
private workingMap: Map<string, Episode>;
private mutations: MutationRecord[] = [];
private targetNodes?: Set<string>;
constructor(episodes: Episode[]) {
constructor(episodes: Episode[], targetNodes?: Set<string>) {
this.originalMap = new Map(episodes.map((e) => [e.id, e]));
this.workingOrder = episodes.map((e) => e.id);
this.workingMap = new Map(episodes.map((e) => [e.id, e]));
this.targetNodes = targetNodes;
}
/**
* Provides a readonly view of the current working state of the episodes.
* Processors should iterate over this to decide what to mutate.
* Provides a readonly view of the specific targets this processor is allowed to touch.
* If no targets were specified (e.g. fallback pipeline), it returns the entire history.
*/
get episodes(): readonly Episode[] {
get targets(): Array<{ episode: Episode; node: IrNode | Episode }> {
const results: Array<{ episode: Episode; node: IrNode | Episode }> = [];
for (const epId of this.workingOrder) {
const ep = this.workingMap.get(epId)!;
// If we don't have restricted targets, everything is a target
if (!this.targetNodes) {
results.push({ episode: ep, node: ep });
continue;
}
// Check episode itself
if (this.targetNodes.has(ep.id)) {
results.push({ episode: ep, node: ep });
}
// Check trigger
if (this.targetNodes.has(ep.trigger.id)) {
results.push({ episode: ep, node: ep.trigger });
}
// Check steps
for (const step of ep.steps) {
if (this.targetNodes.has(step.id)) {
results.push({ episode: ep, node: step });
}
}
// Check yield
if (ep.yield && this.targetNodes.has(ep.yield.id)) {
results.push({ episode: ep, node: ep.yield });
}
}
return results;
}
/**
* Returns the full history for READ-ONLY context purposes.
* Processors should not iterate over this array to decide what to mutate.
* They should iterate over `editor.targets`.
*/
getFullHistory(): readonly Episode[] {
return this.workingOrder.map((id) => this.workingMap.get(id)!);
}
private isTargeted(episodeId: string): boolean {
if (!this.targetNodes) return true;
if (this.targetNodes.has(episodeId)) return true;
const ep = this.workingMap.get(episodeId);
if (!ep) return false;
if (this.targetNodes.has(ep.trigger.id)) return true;
if (ep.yield && this.targetNodes.has(ep.yield.id)) return true;
for (const step of ep.steps) {
if (this.targetNodes.has(step.id)) return true;
}
return false;
}
/**
* Safely edits an existing episode.
* The framework will handle deeply cloning the episode before passing it to the mutator,
* guaranteeing that original references are never modified.
* The framework will handle deeply cloning the episode before passing it to the mutator.
* Throws an error if the processor attempts to edit a non-targeted node.
*/
editEpisode(id: string, action: string, mutator: (draft: Episode) => void) {
if (!this.isTargeted(id)) {
throw new Error(`EpisodeEditor: Processor attempted to edit Episode ${id} which is outside its allowed target scope.`);
}
const ep = this.workingMap.get(id);
if (!ep) return;
@@ -82,6 +144,12 @@ export class EpisodeEditor {
* It inserts the new episode at the lowest index of the removed episodes.
*/
replaceEpisodes(oldIds: string[], newEpisode: Episode, action: string) {
for (const id of oldIds) {
if (!this.isTargeted(id)) {
throw new Error(`EpisodeEditor: Processor attempted to replace Episode ${id} which is outside its allowed target scope.`);
}
}
const indices = oldIds
.map((id) => this.workingOrder.indexOf(id))
.filter((i) => i !== -1);
@@ -112,6 +180,12 @@ export class EpisodeEditor {
* Removes episodes from the graph completely (e.g., emergency truncation).
*/
removeEpisodes(oldIds: string[], action: string) {
for (const id of oldIds) {
if (!this.isTargeted(id)) {
throw new Error(`EpisodeEditor: Processor attempted to remove Episode ${id} which is outside its allowed target scope.`);
}
}
this.workingOrder = this.workingOrder.filter((id) => !oldIds.includes(id));
for (const id of oldIds) {
this.workingMap.delete(id);
+4 -3
View File
@@ -6,12 +6,13 @@
import type { Content, Part } from '@google/genai';
import type { Episode, EpisodeStep, UserPrompt, AgentYield } from './types.js';
import { isAgentThought, isToolExecution, isUserPrompt } from './graphUtils.js';
export function fromIr(episodes: Episode[]): Content[] {
const history: Content[] = [];
for (const ep of episodes) {
if (ep.trigger.type === 'USER_PROMPT') {
if (isUserPrompt(ep.trigger)) {
const triggerContent = serializeTrigger(ep.trigger);
if (triggerContent) history.push(triggerContent);
}
@@ -66,12 +67,12 @@ function serializeSteps(steps: EpisodeStep[]): Content[] {
};
for (const step of steps) {
if (step.type === 'AGENT_THOUGHT') {
if (isAgentThought(step)) {
if (pendingUserParts.length > 0) flushPending();
pendingModelParts.push({
text: step.presentation?.text ?? step.text,
});
} else if (step.type === 'TOOL_EXECUTION') {
} else if (isToolExecution(step)) {
pendingModelParts.push({
functionCall: {
name: step.toolName,
+28 -3
View File
@@ -4,11 +4,35 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Episode } from './types.js';
import type { Episode, IrNode, AgentThought, ToolExecution, UserPrompt, AgentYield, SystemEvent } from './types.js';
import type { ContextTracer } from '../tracer.js';
import { debugLogger } from '../../utils/debugLogger.js';
import type { ContextEnvironment } from '../sidecar/environment.js';
export function isEpisode(node: IrNode | Episode): node is Episode {
return node.type === 'EPISODE';
}
export function isAgentThought(node: IrNode | Episode): node is AgentThought {
return node.type === 'AGENT_THOUGHT';
}
export function isToolExecution(node: IrNode | Episode): node is ToolExecution {
return node.type === 'TOOL_EXECUTION';
}
export function isUserPrompt(node: IrNode | Episode): node is UserPrompt {
return node.type === 'USER_PROMPT';
}
export function isAgentYield(node: IrNode | Episode): node is AgentYield {
return node.type === 'AGENT_YIELD';
}
export function isSystemEvent(node: IrNode | Episode): node is SystemEvent {
return node.type === 'SYSTEM_EVENT';
}
/**
* Generates a computed view of the pristine log.
* Sweeps backwards (newest to oldest), tracking rolling tokens.
@@ -42,7 +66,7 @@ export function generateWorkingBufferView(
let projectedTrigger: typeof ep.trigger;
if (ep.trigger.type === 'USER_PROMPT') {
if (isUserPrompt(ep.trigger)) {
projectedTrigger = {
...ep.trigger,
metadata: {
@@ -63,6 +87,7 @@ export function generateWorkingBufferView(
let projectedEp: Episode = {
...ep,
type: 'EPISODE',
trigger: projectedTrigger,
steps: ep.steps.map((step) => ({
...step,
@@ -145,7 +170,7 @@ export function generateWorkingBufferView(
masked.type === 'masked'
) {
if (
projectedEp.trigger.type === 'USER_PROMPT' &&
isUserPrompt(projectedEp.trigger) &&
projectedEp.trigger.semanticParts &&
projectedEp.trigger.semanticParts.length > 0
) {
+3 -1
View File
@@ -17,6 +17,7 @@ import type {
SystemEvent,
} from './types.js';
import type { ContextTokenCalculator } from '../utils/contextTokenCalculator.js';
import { isAgentThought } from './graphUtils.js';
// WeakMap to provide stable, deterministic identity across parses for the exact same Content/Part references
const nodeIdentityMap = new WeakMap<object, string>();
@@ -199,6 +200,7 @@ function parseUserParts(
};
return {
type: 'EPISODE',
id: getStableId(msg),
timestamp: Date.now(),
trigger,
@@ -247,7 +249,7 @@ function parseModelParts(
function finalizeYield(currentEpisode: Partial<Episode>) {
if (currentEpisode.steps && currentEpisode.steps.length > 0) {
const lastStep = currentEpisode.steps[currentEpisode.steps.length - 1];
if (lastStep.type === 'AGENT_THOUGHT') {
if (isAgentThought(lastStep)) {
const yieldNode: AgentYield = {
id: lastStep.id,
type: 'AGENT_YIELD',
+1
View File
@@ -188,6 +188,7 @@ export interface AgentYield extends IrNode {
* internal reasoning and observations (Steps).
*/
export interface Episode {
readonly type: 'EPISODE';
readonly id: string;
/** When the episode began */
readonly timestamp: number;