chat: drop legacy thinking-steps, tool-fallback, hitl modules, and span-indent helper.

This commit is contained in:
CREDO23 2026-05-09 18:35:52 +02:00
parent 9c5a178468
commit ba0e1e70a0
14 changed files with 0 additions and 1942 deletions

View file

@ -1,19 +0,0 @@
/**
* Indent tool-call cards that belong to an open delegating ``task`` episode.
*
* The backend only stamps ``metadata.spanId`` on tool SSE / persisted parts
* while a ``task`` is active (see ``AgentEventRelayState.tool_activity_metadata``),
* so its presence is sufficient. The opening ``task`` row itself carries the
* same span id but stays flush it is the header of the delegation.
*/
export function shouldIndentToolCallForDelegationSpan(
toolName: string,
metadata: Record<string, unknown> | undefined
): boolean {
if (toolName === "task") return false;
const v = metadata?.spanId;
return typeof v === "string" && v.trim().length > 0;
}
export const DELEGATION_SPAN_INDENT_CLASS = "pl-3 sm:ml-4";

View file

@ -1,153 +0,0 @@
"use client";
import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from "react";
import type { HitlDecision } from "./types";
export type BundleSubmit = (orderedDecisions: HitlDecision[]) => void;
export interface HitlBundleAPI {
toolCallIds: readonly string[];
currentStep: number;
stagedCount: number;
isInBundle: (toolCallId: string) => boolean;
isCurrentStep: (toolCallId: string) => boolean;
getStaged: (toolCallId: string) => HitlDecision | undefined;
stage: (toolCallId: string, decision: HitlDecision) => void;
goToStep: (i: number) => void;
next: () => void;
prev: () => void;
submit: () => void;
}
const HitlBundleContext = createContext<HitlBundleAPI | null>(null);
const ToolCallIdContext = createContext<string | null>(null);
export function useHitlBundle(): HitlBundleAPI | null {
return useContext(HitlBundleContext);
}
export function useToolCallIdContext(): string | null {
return useContext(ToolCallIdContext);
}
export function ToolCallIdProvider({
toolCallId,
children,
}: {
toolCallId: string;
children: ReactNode;
}) {
return <ToolCallIdContext.Provider value={toolCallId}>{children}</ToolCallIdContext.Provider>;
}
interface HitlBundleProviderProps {
toolCallIds: readonly string[] | null;
onSubmit: BundleSubmit;
children: ReactNode;
}
/**
* Activates only when ``toolCallIds`` has 2+ entries; single-card interrupts
* keep their direct ``window`` dispatch path so N=1 UX is unchanged.
*/
export function HitlBundleProvider({ toolCallIds, onSubmit, children }: HitlBundleProviderProps) {
const active = toolCallIds !== null && toolCallIds.length >= 2;
const ids = useMemo(() => (active ? [...toolCallIds] : []), [active, toolCallIds]);
const bundleKey = ids.join("|");
// Derived-state-from-props: reset staging + step when the bundle changes.
const [prevBundleKey, setPrevBundleKey] = useState(bundleKey);
const [staged, setStaged] = useState<Map<string, HitlDecision>>(() => new Map());
const [currentStep, setCurrentStep] = useState(0);
if (bundleKey !== prevBundleKey) {
setPrevBundleKey(bundleKey);
setStaged(new Map());
setCurrentStep(0);
}
const isInBundle = useCallback((tcId: string) => ids.includes(tcId), [ids]);
const isCurrentStep = useCallback(
(tcId: string) => active === true && ids[currentStep] === tcId,
[active, ids, currentStep]
);
const getStaged = useCallback((tcId: string) => staged.get(tcId), [staged]);
const stage = useCallback(
(tcId: string, decision: HitlDecision) => {
if (!active || !ids.includes(tcId)) return;
setStaged((prev) => {
const next = new Map(prev);
next.set(tcId, decision);
return next;
});
// Mirror the staged decision onto the card immediately so prev/next
// nav doesn't re-show approve/reject buttons for already-decided cards.
// Submit's ``hitl-decision`` event re-applies these (no-op) and runs
// the actual resume.
window.dispatchEvent(
new CustomEvent("hitl-stage", { detail: { toolCallId: tcId, decision } })
);
const idx = ids.indexOf(tcId);
if (idx >= 0 && idx < ids.length - 1) {
setCurrentStep(idx + 1);
}
},
[active, ids]
);
const goToStep = useCallback(
(i: number) => {
if (i < 0 || i >= ids.length) return;
setCurrentStep(i);
},
[ids.length]
);
const next = useCallback(() => {
setCurrentStep((s) => Math.min(s + 1, Math.max(0, ids.length - 1)));
}, [ids.length]);
const prev = useCallback(() => {
setCurrentStep((s) => Math.max(s - 1, 0));
}, []);
const submit = useCallback(() => {
if (!active) return;
if (staged.size !== ids.length) return;
const ordered: HitlDecision[] = [];
for (const tcId of ids) {
const d = staged.get(tcId);
if (!d) return;
ordered.push(d);
}
onSubmit(ordered);
}, [active, ids, staged, onSubmit]);
const value = useMemo<HitlBundleAPI | null>(() => {
if (!active) return null;
return {
toolCallIds: ids,
currentStep,
stagedCount: staged.size,
isInBundle,
isCurrentStep,
getStaged,
stage,
goToStep,
next,
prev,
submit,
};
}, [
active,
ids,
currentStep,
staged,
isInBundle,
isCurrentStep,
getStaged,
stage,
goToStep,
next,
prev,
submit,
]);
return <HitlBundleContext.Provider value={value}>{children}</HitlBundleContext.Provider>;
}

View file

@ -1,22 +0,0 @@
export {
type BundleSubmit,
type HitlBundleAPI,
HitlBundleProvider,
ToolCallIdProvider,
useHitlBundle,
useToolCallIdContext,
} from "./bundle-context";
export {
type HitlRenderTarget,
HitlRenderTargetProvider,
useHitlRenderTarget,
withHitlInTimeline,
} from "./render-target";
export type {
HitlDecision,
InterruptActionRequest,
InterruptResult,
InterruptReviewConfig,
} from "./types";
export { isInterruptResult } from "./types";
export { useHitlDecision } from "./use-hitl-decision";

View file

@ -1,48 +0,0 @@
"use client";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { createContext, useContext } from "react";
import { isInterruptResult } from "./types";
/**
* Where this tool-call card is currently rendering.
*
* - ``"body"`` (default) assistant-ui's ``MessagePrimitive.Parts`` renders
* the card inside the message bubble.
* - ``"timeline"`` ``ThinkingStepsDisplay`` renders the SAME component
* inline under the matching step row so the HITL approval lives in the
* chain-of-thought instead of as a standalone card in the message body.
*
* The two render targets share one component implementation; the context
* lets the body render skip itself when the timeline copy will show the
* card, avoiding a double-render.
*/
export type HitlRenderTarget = "body" | "timeline";
const HitlRenderTargetContext = createContext<HitlRenderTarget>("body");
export const HitlRenderTargetProvider = HitlRenderTargetContext.Provider;
export function useHitlRenderTarget(): HitlRenderTarget {
return useContext(HitlRenderTargetContext);
}
/**
* Hide the body render of a tool-call whose result is a HITL interrupt.
* The same component is mounted again inside ``ThinkingStepsDisplay``
* with ``HitlRenderTargetProvider value="timeline"`` that copy renders
* normally, so the card "moves" from the message body to the timeline.
*
* Pure pass-through for non-HITL results AND for the timeline render.
*/
export function withHitlInTimeline(
Component: ToolCallMessagePartComponent
): ToolCallMessagePartComponent {
const Wrapped: ToolCallMessagePartComponent = (props) => {
const target = useHitlRenderTarget();
if (target === "body" && isInterruptResult(props.result)) return null;
return <Component {...props} />;
};
Wrapped.displayName = `withHitlInTimeline(${Component.displayName ?? Component.name ?? "ToolUI"})`;
return Wrapped;
}

View file

@ -1,45 +0,0 @@
/**
* Shared types for Human-in-the-Loop (HITL) approval across all tools.
*
* Every tool-ui component that handles interrupts should import from here
* instead of defining its own `InterruptResult` / `isInterruptResult`.
*/
export interface InterruptActionRequest {
name: string;
args: Record<string, unknown>;
}
export interface InterruptReviewConfig {
action_name: string;
allowed_decisions: Array<"approve" | "edit" | "reject">;
}
export interface InterruptResult<C extends Record<string, unknown> = Record<string, unknown>> {
__interrupt__: true;
__decided__?: "approve" | "reject" | "edit";
__completed__?: boolean;
action_requests: InterruptActionRequest[];
review_configs: InterruptReviewConfig[];
interrupt_type?: string;
context?: C;
message?: string;
}
export function isInterruptResult(result: unknown): result is InterruptResult {
return (
typeof result === "object" &&
result !== null &&
"__interrupt__" in result &&
(result as InterruptResult).__interrupt__ === true
);
}
export interface HitlDecision {
type: "approve" | "reject" | "edit";
message?: string;
edited_action?: {
name: string;
args: Record<string, unknown>;
};
}

View file

@ -1,41 +0,0 @@
/**
* Shared hook for dispatching HITL decisions.
*
* Tool-ui cards always call ``dispatch([decision])``. When a multi-card bundle
* is active (``HitlBundleProvider``), the dispatch is intercepted and staged
* against this card's ``toolCallId`` so the orchestrator can submit one
* ordered N-decision payload. With no bundle active (N=1 path), it falls back
* to the legacy ``window`` event the host listens for in ``page.tsx``.
*/
import { useCallback } from "react";
import { useHitlBundle, useToolCallIdContext } from "./bundle-context";
import type { HitlDecision } from "./types";
export function useHitlDecision() {
const bundle = useHitlBundle();
const toolCallId = useToolCallIdContext();
const dispatch = useCallback(
(decisions: HitlDecision[]) => {
if (bundle && toolCallId && bundle.isInBundle(toolCallId) && decisions.length > 0) {
if (decisions.length > 1 && process.env.NODE_ENV !== "production") {
// Tool-ui cards stage one decision per call; a multi-decision
// dispatch into an active bundle would silently drop tail entries.
// eslint-disable-next-line no-console
console.warn(
"[hitl] dispatch received %d decisions inside an active bundle; only [0] will be staged for %s",
decisions.length,
toolCallId
);
}
bundle.stage(toolCallId, decisions[0]);
return;
}
window.dispatchEvent(new CustomEvent("hitl-decision", { detail: { decisions } }));
},
[bundle, toolCallId]
);
return { dispatch };
}