mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 09:30:41 -07:00
broken refactor
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user