CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
turn-interruption.ts137 linesDownload Raw Back to core
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import type { Content, Part } from '@google/genai';8import { isSystemReminderContent } from '../utils/environmentContext.js';9 10/**11 * Classification of how a session's last turn ended, computed from persisted12 * chat history alone (no in-memory request refs), so it works across process13 * restarts — unlike the Ctrl+Y retry path, which depends on14 * `lastPromptRef` surviving in the same process.15 *16 * The history tail determines the classification:17 *  - `interrupted_prompt`: the tail is one or more non-structural `user`18 *    entries — a prompt (or a tool_result submission) whose model response19 *    never landed. Continuing means re-submitting their `parts` with Retry20 *    semantics: the send path strips the orphaned trailing user entries and21 *    re-pushes the same content under the same logical turn, so the transcript22 *    gains no new user message.23 *  - `interrupted_turn`: the tail is a `model` entry carrying `functionCall`s24 *    that no `functionResponse` ever answered (crash/abort mid tool run).25 *    Continuing means closing each pair with a synthesized error26 *    `functionResponse` submitted as a ToolResult — a legal continuation27 *    signal that needs no synthetic user text.28 *  - `none`: the turn ended cleanly (model text tail), the tail is a29 *    structural pure system-reminder entry (strip refuses to pop those, so a30 *    Retry would duplicate content), or history is empty.31 *32 * A model text tail that was truncated mid-stream is indistinguishable from33 * a clean finish without persisted stop_reason metadata, so it classifies as34 * `none` here; recovering that case needs provider prefill support and is35 * tracked separately.36 */37export type TurnInterruption =38  | { kind: 'none' }39  | { kind: 'interrupted_prompt'; parts: Part[] }40  | {41      kind: 'interrupted_turn';42      danglingCalls: Array<{ callId: string; name: string }>;43    };44 45// Continue detection only walks the final run of trailing user/model entries.46// A bounded tail avoids deep-cloning long daemon histories for each probe while47// still leaving ample room for repeated failed sends and tool-result retries.48export const TURN_INTERRUPTION_HISTORY_TAIL_COUNT = 50;49 50/**51 * Detect whether the last turn of `history` was left unfinished, and if so52 * what kind of continuation applies. Pure read — never mutates `history`.53 *54 * Callers should pass enough tail entries to include all consecutive trailing55 * user entries. Accepting the full array keeps the function composable with56 * raw transcript fixtures in tests.57 *58 * @param history - Chat history in Gemini `Content[]` form, oldest first.59 * @returns The interruption classification; see {@link TurnInterruption}.60 */61export function detectTurnInterruption(history: Content[]): TurnInterruption {62  const last = history[history.length - 1];63  if (!last) {64    return { kind: 'none' };65  }66 67  if (last.role === 'user') {68    const trailingUserEntries: Content[] = [];69    for (let i = history.length - 1; i >= 0; i--) {70      const entry = history[i];71      if (!entry || entry.role !== 'user') {72        break;73      }74      // Structural reminder entries are not orphaned turns; the strip pass75      // refuses to pop them, so re-submitting would duplicate the prompt.76      if (isSystemReminderContent(entry)) {77        break;78      }79      trailingUserEntries.unshift(entry);80    }81    // Capture every part, including any per-turn system-reminder parts riding82    // alongside the prompt. The Retry send path does not re-inject per-turn83    // reminders, so replaying them keeps the continued turn complete. When a84    // continuation includes tool results, keep functionResponse parts first:85    // Anthropic-compatible backends require tool_result blocks before text.86    const allParts = trailingUserEntries.flatMap((entry) => entry.parts ?? []);87    const parts = [88      ...allParts.filter((part) => part.functionResponse),89      ...allParts.filter((part) => !part.functionResponse),90    ];91    if (parts.length === 0) {92      return { kind: 'none' };93    }94    // Public helper boundary: callers may pass raw history, so return detached95    // parts even when current continuation callers only read them.96    return { kind: 'interrupted_prompt', parts: structuredClone(parts) };97  }98 99  if (last.role === 'model') {100    // Nothing follows the final entry, so every id'd functionCall in it is101    // by definition unanswered. Calls without an id can't be paired on the102    // wire at all — the repair pass skips them too — so they're ignored.103    const danglingCalls: Array<{ callId: string; name: string }> = [];104    for (const part of last.parts ?? []) {105      const fc = part.functionCall;106      if (fc?.id) {107        danglingCalls.push({ callId: fc.id, name: fc.name ?? 'unknown' });108      }109    }110    if (danglingCalls.length > 0) {111      return { kind: 'interrupted_turn', danglingCalls };112    }113  }114 115  return { kind: 'none' };116}117 118/**119 * Build the error `functionResponse` parts that close the dangling120 * `functionCall`s of an `interrupted_turn`. Shape matches the repair pass's121 * synthesized responses (`applyRepair` in geminiChat.ts) so downstream122 * dedup and telemetry treat both identically.123 *124 * @param danglingCalls - The unanswered calls from {@link detectTurnInterruption}.125 * @param reason - Error text placed in each response; callers pass126 *   `ORPHAN_TOOL_USE_REPAIR_REASON` for consistency with the repair pass.127 * @returns One `functionResponse` part per dangling call, in input order.128 */129export function buildSyntheticToolResponseParts(130  danglingCalls: Array<{ callId: string; name: string }>,131  reason: string,132): Part[] {133  return danglingCalls.map(({ callId, name }) => ({134    functionResponse: { id: callId, name, response: { error: reason } },135  }));136}137 
basant307/AI_Governance_Project · CoolFace