authorgravatar for zortosdev@proton.meZortos <zortosdev@proton.me> 2026-03-13 03:42:59+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-12 19:42:59-07:00
log64dc07a8e5a6d7e34de4e85a07126a9c04e462d3
treee01144e0650567763005fff3886abcdc66ab2a08
parentae4ae9973cd6cebc7d557e64e876534f15c916fd
signature Signed by PGP key B5690EEEBB952194

Add compact Codex tool-call icons and details to the chat timeline (#988)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

10 files changed, 321 insertions(+), 96 deletions(-)

apps/server/integration/fixtures/providerRuntime.ts+2-2
......@@ -73,7 +73,7 @@ export const codexTurnToolFixture = [
7373 turnId: TURN_ID,
7474 payload: {
7575 itemType: "command_execution",
76 title: "Command run",
76 title: "Ran command",
7777 detail: "echo integration",
7878 },
7979 },
......@@ -85,7 +85,7 @@ export const codexTurnToolFixture = [
8585 payload: {
8686 itemType: "command_execution",
8787 status: "completed",
88 title: "Command run",
88 title: "Ran command",
8989 detail: "echo integration",
9090 },
9191 },
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+2-13
......@@ -5,6 +5,7 @@ import {
55 MessageId,
66 type OrchestrationEvent,
77 CheckpointRef,
8 isToolLifecycleItemType,
89 ThreadId,
910 TurnId,
1011 type OrchestrationThreadActivity,
......@@ -173,18 +174,6 @@ function requestKindFromCanonicalRequestType(
173174 }
174175}
175176
176function isToolLifecycleItemType(itemType: string): boolean {
177 return (
178 itemType === "command_execution" ||
179 itemType === "file_change" ||
180 itemType === "mcp_tool_call" ||
181 itemType === "dynamic_tool_call" ||
182 itemType === "collab_agent_tool_call" ||
183 itemType === "web_search" ||
184 itemType === "image_view"
185 );
186}
187
188177function runtimeEventToActivities(
189178 event: ProviderRuntimeEvent,
190179): ReadonlyArray<OrchestrationThreadActivity> {
......@@ -449,7 +438,7 @@ function runtimeEventToActivities(
449438 createdAt: event.createdAt,
450439 tone: "tool",
451440 kind: "tool.completed",
452 summary: `${event.payload.title ?? "Tool"} complete`,
441 summary: event.payload.title ?? "Tool",
453442 payload: {
454443 itemType: event.payload.itemType,
455444 ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
apps/server/src/provider/Layers/CodexAdapter.ts+1-1
......@@ -164,7 +164,7 @@ function itemTitle(itemType: CanonicalItemType): string | undefined {
164164 case "plan":
165165 return "Plan";
166166 case "command_execution":
167 return "Command run";
167 return "Ran command";
168168 case "file_change":
169169 return "File change";
170170 case "mcp_tool_call":
apps/server/src/provider/Layers/ProviderService.test.ts+3-3
......@@ -703,7 +703,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
703703 threadId: session.threadId,
704704 turnId: asTurnId("turn-1"),
705705 toolKind: "command",
706 title: "Command run",
706 title: "Ran command",
707707 });
708708 fanout.codex.emit({
709709 type: "tool.completed",
......@@ -713,7 +713,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
713713 threadId: session.threadId,
714714 turnId: asTurnId("turn-1"),
715715 toolKind: "command",
716 title: "Command run",
716 title: "Ran command",
717717 });
718718 fanout.codex.emit({
719719 type: "turn.completed",
......@@ -768,7 +768,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
768768 threadId: session.threadId,
769769 turnId: asTurnId("turn-1"),
770770 toolKind: "command",
771 title: "Command run",
771 title: "Ran command",
772772 detail: "echo one",
773773 },
774774 {
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+11-1
......@@ -1,5 +1,5 @@
11import { describe, expect, it } from "vitest";
2import { computeMessageDurationStart } from "./MessagesTimeline.logic";
2import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic";
33
44describe("computeMessageDurationStart", () => {
55 it("returns message createdAt when there is no preceding user message", () => {
......@@ -133,3 +133,13 @@ describe("computeMessageDurationStart", () => {
133133 expect(computeMessageDurationStart([])).toEqual(new Map());
134134 });
135135});
136
137describe("normalizeCompactToolLabel", () => {
138 it("removes trailing completion wording from command labels", () => {
139 expect(normalizeCompactToolLabel("Ran command complete")).toBe("Ran command");
140 });
141
142 it("removes trailing completion wording from other labels", () => {
143 expect(normalizeCompactToolLabel("Read file completed")).toBe("Read file");
144 });
145});
apps/web/src/components/chat/MessagesTimeline.logic.ts+4
......@@ -23,3 +23,7 @@ export function computeMessageDurationStart(
2323
2424 return result;
2525}
26
27export function normalizeCompactToolLabel(value: string): string {
28 return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();
29}
apps/web/src/components/chat/MessagesTimeline.tsx+176-65
......@@ -10,7 +10,20 @@ import { AUTO_SCROLL_BOTTOM_THRESHOLD_PX } from "../../chat-scroll";
1010import { type TurnDiffSummary } from "../../types";
1111import { summarizeTurnDiffStats } from "../../lib/turnDiffTree";
1212import ChatMarkdown from "../ChatMarkdown";
13import { Undo2Icon } from "lucide-react";
13import {
14 BotIcon,
15 CheckIcon,
16 CircleAlertIcon,
17 EyeIcon,
18 GlobeIcon,
19 HammerIcon,
20 type LucideIcon,
21 SquarePenIcon,
22 TerminalIcon,
23 Undo2Icon,
24 WrenchIcon,
25 ZapIcon,
26} from "lucide-react";
1427import { Button } from "../ui/button";
1528import { clamp } from "effect/Number";
1629import { estimateTimelineMessageHeight } from "../timelineHeight";
......@@ -19,7 +32,8 @@ import { ProposedPlanCard } from "./ProposedPlanCard";
1932import { ChangedFilesTree } from "./ChangedFilesTree";
2033import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
2134import { MessageCopyButton } from "./MessageCopyButton";
22import { computeMessageDurationStart } from "./MessagesTimeline.logic";
35import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic";
36import { cn } from "~/lib/utils";
2337import { type TimestampFormat } from "../../appSettings";
2438import { formatTimestamp } from "../../timestampFormat";
2539
......@@ -289,72 +303,30 @@ export const MessagesTimeline = memo(function MessagesTimeline({
289303 : groupedEntries;
290304 const hiddenCount = groupedEntries.length - visibleEntries.length;
291305 const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool");
292 const groupLabel = onlyToolEntries
293 ? groupedEntries.length === 1
294 ? "Tool call"
295 : `Tool calls (${groupedEntries.length})`
296 : groupedEntries.length === 1
297 ? "Work event"
298 : `Work log (${groupedEntries.length})`;
306 const showHeader = hasOverflow || !onlyToolEntries;
307 const groupLabel = onlyToolEntries ? "Tool calls" : "Work log";
299308
300309 return (
301 <div className="rounded-lg border border-border/80 bg-card/45 px-3 py-2">
302 <div className="mb-1.5 flex items-center justify-between gap-3">
303 <p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">
304 {groupLabel}
305 </p>
306 {hasOverflow && (
307 <button
308 type="button"
309 className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/55 transition-colors duration-150 hover:text-muted-foreground/80"
310 onClick={() => onToggleWorkGroup(groupId)}
311 >
312 {isExpanded ? "Show less" : `Show ${hiddenCount} more`}
313 </button>
314 )}
315 </div>
316 <div className="space-y-1">
310 <div className="rounded-xl border border-border/45 bg-card/25 px-2 py-1.5">
311 {showHeader && (
312 <div className="mb-1.5 flex items-center justify-between gap-2 px-0.5">
313 <p className="text-[9px] uppercase tracking-[0.16em] text-muted-foreground/55">
314 {groupLabel} ({groupedEntries.length})
315 </p>
316 {hasOverflow && (
317 <button
318 type="button"
319 className="text-[9px] uppercase tracking-[0.12em] text-muted-foreground/55 transition-colors duration-150 hover:text-foreground/75"
320 onClick={() => onToggleWorkGroup(groupId)}
321 >
322 {isExpanded ? "Show less" : `Show ${hiddenCount} more`}
323 </button>
324 )}
325 </div>
326 )}
327 <div className="space-y-0.5">
317328 {visibleEntries.map((workEntry) => (
318 <div key={`work-row:${workEntry.id}`} className="flex items-start gap-2 py-0.5">
319 <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" />
320 <div className="min-w-0 flex-1 py-[2px]">
321 <p className={`text-[11px] leading-relaxed ${workToneClass(workEntry.tone)}`}>
322 {workEntry.label}
323 </p>
324 {workEntry.command && (
325 <pre className="mt-1 overflow-x-auto rounded-md border border-border/70 bg-background/80 px-2 py-1 font-mono text-[11px] leading-relaxed text-foreground/80">
326 {workEntry.command}
327 </pre>
328 )}
329 {workEntry.changedFiles && workEntry.changedFiles.length > 0 && (
330 <div className="mt-1 flex flex-wrap gap-1">
331 {workEntry.changedFiles.slice(0, 6).map((filePath) => (
332 <span
333 key={`${workEntry.id}:${filePath}`}
334 className="rounded-md border border-border/70 bg-background/65 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground/85"
335 title={filePath}
336 >
337 {filePath}
338 </span>
339 ))}
340 {workEntry.changedFiles.length > 6 && (
341 <span className="px-1 text-[10px] text-muted-foreground/65">
342 +{workEntry.changedFiles.length - 6} more
343 </span>
344 )}
345 </div>
346 )}
347 {workEntry.detail &&
348 (!workEntry.command || workEntry.detail !== workEntry.command) && (
349 <p
350 className="mt-1 text-[11px] leading-relaxed text-muted-foreground/75"
351 title={workEntry.detail}
352 >
353 {workEntry.detail}
354 </p>
355 )}
356 </div>
357 </div>
329 <SimpleWorkEntryRow key={`work-row:${workEntry.id}`} workEntry={workEntry} />
358330 ))}
359331 </div>
360332 </div>
......@@ -664,9 +636,148 @@ function formatMessageMeta(
664636 return `${formatTimestamp(createdAt, timestampFormat)} • ${duration}`;
665637}
666638
639function workToneIcon(tone: TimelineWorkEntry["tone"]): {
640 icon: LucideIcon;
641 className: string;
642} {
643 if (tone === "error") {
644 return {
645 icon: CircleAlertIcon,
646 className: "text-foreground/92",
647 };
648 }
649 if (tone === "thinking") {
650 return {
651 icon: BotIcon,
652 className: "text-foreground/92",
653 };
654 }
655 if (tone === "info") {
656 return {
657 icon: CheckIcon,
658 className: "text-foreground/92",
659 };
660 }
661 return {
662 icon: ZapIcon,
663 className: "text-foreground/92",
664 };
665}
666
667667function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
668668 if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50";
669669 if (tone === "tool") return "text-muted-foreground/70";
670670 if (tone === "thinking") return "text-muted-foreground/50";
671671 return "text-muted-foreground/40";
672672}
673
674function workEntryPreview(
675 workEntry: Pick<TimelineWorkEntry, "detail" | "command" | "changedFiles">,
676) {
677 if (workEntry.command) return workEntry.command;
678 if (workEntry.detail) return workEntry.detail;
679 if ((workEntry.changedFiles?.length ?? 0) === 0) return null;
680 const [firstPath] = workEntry.changedFiles ?? [];
681 if (!firstPath) return null;
682 return workEntry.changedFiles!.length === 1
683 ? firstPath
684 : `${firstPath} +${workEntry.changedFiles!.length - 1} more`;
685}
686
687function workEntryIcon(workEntry: TimelineWorkEntry): LucideIcon {
688 if (workEntry.requestKind === "command") return TerminalIcon;
689 if (workEntry.requestKind === "file-read") return EyeIcon;
690 if (workEntry.requestKind === "file-change") return SquarePenIcon;
691
692 if (workEntry.itemType === "command_execution" || workEntry.command) {
693 return TerminalIcon;
694 }
695 if (workEntry.itemType === "file_change" || (workEntry.changedFiles?.length ?? 0) > 0) {
696 return SquarePenIcon;
697 }
698 if (workEntry.itemType === "web_search") return GlobeIcon;
699 if (workEntry.itemType === "image_view") return EyeIcon;
700
701 switch (workEntry.itemType) {
702 case "mcp_tool_call":
703 return WrenchIcon;
704 case "dynamic_tool_call":
705 case "collab_agent_tool_call":
706 return HammerIcon;
707 }
708
709 return workToneIcon(workEntry.tone).icon;
710}
711
712function capitalizePhrase(value: string): string {
713 const trimmed = value.trim();
714 if (trimmed.length === 0) {
715 return value;
716 }
717 return `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`;
718}
719
720function toolWorkEntryHeading(workEntry: TimelineWorkEntry): string {
721 if (!workEntry.toolTitle) {
722 return capitalizePhrase(normalizeCompactToolLabel(workEntry.label));
723 }
724 return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle));
725}
726
727const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
728 workEntry: TimelineWorkEntry;
729}) {
730 const { workEntry } = props;
731 const iconConfig = workToneIcon(workEntry.tone);
732 const EntryIcon = workEntryIcon(workEntry);
733 const heading = toolWorkEntryHeading(workEntry);
734 const preview = workEntryPreview(workEntry);
735 const displayText = preview ? `${heading} - ${preview}` : heading;
736 const hasChangedFiles = (workEntry.changedFiles?.length ?? 0) > 0;
737 const previewIsChangedFiles = hasChangedFiles && !workEntry.command && !workEntry.detail;
738
739 return (
740 <div className="rounded-lg px-1 py-1">
741 <div className="flex items-center gap-2 transition-[opacity,translate] duration-200">
742 <span
743 className={cn("flex size-5 shrink-0 items-center justify-center", iconConfig.className)}
744 >
745 <EntryIcon className="size-3" />
746 </span>
747 <div className="min-w-0 flex-1 overflow-hidden">
748 <p
749 className={cn(
750 "truncate text-[11px] leading-5",
751 workToneClass(workEntry.tone),
752 preview ? "text-muted-foreground/70" : "",
753 )}
754 title={displayText}
755 >
756 <span className={cn("text-foreground/80", workToneClass(workEntry.tone))}>
757 {heading}
758 </span>
759 {preview && <span className="text-muted-foreground/55"> - {preview}</span>}
760 </p>
761 </div>
762 </div>
763 {hasChangedFiles && !previewIsChangedFiles && (
764 <div className="mt-1 flex flex-wrap gap-1 pl-6">
765 {workEntry.changedFiles?.slice(0, 4).map((filePath) => (
766 <span
767 key={`${workEntry.id}:${filePath}`}
768 className="rounded-md border border-border/55 bg-background/75 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground/75"
769 title={filePath}
770 >
771 {filePath}
772 </span>
773 ))}
774 {(workEntry.changedFiles?.length ?? 0) > 4 && (
775 <span className="px-1 text-[10px] text-muted-foreground/55">
776 +{(workEntry.changedFiles?.length ?? 0) - 4}
777 </span>
778 )}
779 </div>
780 )}
781 </div>
782 );
783});
apps/web/src/session-logic.test.ts+36-3
......@@ -400,7 +400,7 @@ describe("deriveWorkLogEntries", () => {
400400 makeActivity({
401401 id: "tool-complete",
402402 createdAt: "2026-02-23T00:00:02.000Z",
403 summary: "Command run complete",
403 summary: "Ran command",
404404 tone: "tool",
405405 kind: "tool.completed",
406406 }),
......@@ -437,7 +437,7 @@ describe("deriveWorkLogEntries", () => {
437437 makeActivity({
438438 id: "command-tool",
439439 kind: "tool.completed",
440 summary: "Command run complete",
440 summary: "Ran command",
441441 payload: {
442442 itemType: "command_execution",
443443 data: {
......@@ -453,12 +453,45 @@ describe("deriveWorkLogEntries", () => {
453453 expect(entry?.command).toBe("bun run lint");
454454 });
455455
456 it("keeps compact Codex tool metadata used for icons and labels", () => {
457 const activities: OrchestrationThreadActivity[] = [
458 makeActivity({
459 id: "tool-with-metadata",
460 kind: "tool.completed",
461 summary: "bash",
462 payload: {
463 itemType: "command_execution",
464 title: "bash",
465 status: "completed",
466 detail: '{ "dev": "vite dev --port 3000" } <exited with exit code 0>',
467 data: {
468 item: {
469 command: ["bun", "run", "dev"],
470 result: {
471 content: '{ "dev": "vite dev --port 3000" } <exited with exit code 0>',
472 exitCode: 0,
473 },
474 },
475 },
476 },
477 }),
478 ];
479
480 const [entry] = deriveWorkLogEntries(activities, undefined);
481 expect(entry).toMatchObject({
482 command: "bun run dev",
483 detail: '{ "dev": "vite dev --port 3000" }',
484 itemType: "command_execution",
485 toolTitle: "bash",
486 });
487 });
488
456489 it("extracts changed file paths for file-change tool activities", () => {
457490 const activities: OrchestrationThreadActivity[] = [
458491 makeActivity({
459492 id: "file-tool",
460493 kind: "tool.completed",
461 summary: "File change complete",
494 summary: "File change",
462495 payload: {
463496 itemType: "file_change",
464497 data: {
apps/web/src/session-logic.ts+70-3
......@@ -1,9 +1,11 @@
11import {
22 ApprovalRequestId,
3 isToolLifecycleItemType,
34 type OrchestrationLatestTurn,
45 type OrchestrationThreadActivity,
56 type OrchestrationProposedPlanId,
67 type ProviderKind,
8 type ToolLifecycleItemType,
79 type UserInputQuestion,
810 type TurnId,
911} from "@t3tools/contracts";
......@@ -36,6 +38,9 @@ export interface WorkLogEntry {
3638 command?: string;
3739 changedFiles?: ReadonlyArray<string>;
3840 tone: "thinking" | "tool" | "info" | "error";
41 toolTitle?: string;
42 itemType?: ToolLifecycleItemType;
43 requestKind?: PendingApproval["requestKind"];
3944}
4045
4146export interface PendingApproval {
......@@ -415,14 +420,20 @@ export function deriveWorkLogEntries(
415420 : null;
416421 const command = extractToolCommand(payload);
417422 const changedFiles = extractChangedFiles(payload);
423 const title = extractToolTitle(payload);
418424 const entry: WorkLogEntry = {
419425 id: activity.id,
420426 createdAt: activity.createdAt,
421427 label: activity.summary,
422428 tone: activity.tone === "approval" ? "info" : activity.tone,
423429 };
430 const itemType = extractWorkLogItemType(payload);
431 const requestKind = extractWorkLogRequestKind(payload);
424432 if (payload && typeof payload.detail === "string" && payload.detail.length > 0) {
425 entry.detail = payload.detail;
433 const detail = stripTrailingExitCode(payload.detail).output;
434 if (detail) {
435 entry.detail = detail;
436 }
426437 }
427438 if (command) {
428439 entry.command = command;
......@@ -430,6 +441,15 @@ export function deriveWorkLogEntries(
430441 if (changedFiles.length > 0) {
431442 entry.changedFiles = changedFiles;
432443 }
444 if (title) {
445 entry.toolTitle = title;
446 }
447 if (itemType) {
448 entry.itemType = itemType;
449 }
450 if (requestKind) {
451 entry.requestKind = requestKind;
452 }
433453 return entry;
434454 });
435455}
......@@ -474,6 +494,53 @@ function extractToolCommand(payload: Record<string, unknown> | null): string | n
474494 return candidates.find((candidate) => candidate !== null) ?? null;
475495}
476496
497function extractToolTitle(payload: Record<string, unknown> | null): string | null {
498 return asTrimmedString(payload?.title);
499}
500
501function stripTrailingExitCode(value: string): {
502 output: string | null;
503 exitCode?: number | undefined;
504} {
505 const trimmed = value.trim();
506 const match = /^(?<output>[\s\S]*?)(?:\s*<exited with exit code (?<code>\d+)>)\s*$/i.exec(
507 trimmed,
508 );
509 if (!match?.groups) {
510 return {
511 output: trimmed.length > 0 ? trimmed : null,
512 };
513 }
514 const exitCode = Number.parseInt(match.groups.code ?? "", 10);
515 const normalizedOutput = match.groups.output?.trim() ?? "";
516 return {
517 output: normalizedOutput.length > 0 ? normalizedOutput : null,
518 ...(Number.isInteger(exitCode) ? { exitCode } : {}),
519 };
520}
521
522function extractWorkLogItemType(
523 payload: Record<string, unknown> | null,
524): WorkLogEntry["itemType"] | undefined {
525 if (typeof payload?.itemType === "string" && isToolLifecycleItemType(payload.itemType)) {
526 return payload.itemType;
527 }
528 return undefined;
529}
530
531function extractWorkLogRequestKind(
532 payload: Record<string, unknown> | null,
533): WorkLogEntry["requestKind"] | undefined {
534 if (
535 payload?.requestKind === "command" ||
536 payload?.requestKind === "file-read" ||
537 payload?.requestKind === "file-change"
538 ) {
539 return payload.requestKind;
540 }
541 return requestKindFromRequestType(payload?.requestType) ?? undefined;
542}
543
477544function pushChangedFile(target: string[], seen: Set<string>, value: unknown) {
478545 const normalized = asTrimmedString(value);
479546 if (!normalized || seen.has(normalized)) {
......@@ -532,9 +599,9 @@ function collectChangedFiles(value: unknown, target: string[], seen: Set<string>
532599}
533600
534601function extractChangedFiles(payload: Record<string, unknown> | null): string[] {
535 const data = asRecord(payload?.data);
536602 const changedFiles: string[] = [];
537 collectChangedFiles(data, changedFiles, new Set<string>(), 0);
603 const seen = new Set<string>();
604 collectChangedFiles(asRecord(payload?.data), changedFiles, seen, 0);
538605 return changedFiles;
539606}
540607
packages/contracts/src/providerRuntime.ts+16-5
......@@ -93,11 +93,7 @@ const RuntimeErrorClass = Schema.Literals([
9393]);
9494export type RuntimeErrorClass = typeof RuntimeErrorClass.Type;
9595
96export const CanonicalItemType = Schema.Literals([
97 "user_message",
98 "assistant_message",
99 "reasoning",
100 "plan",
96export const TOOL_LIFECYCLE_ITEM_TYPES = [
10197 "command_execution",
10298 "file_change",
10399 "mcp_tool_call",
......@@ -105,6 +101,21 @@ export const CanonicalItemType = Schema.Literals([
105101 "collab_agent_tool_call",
106102 "web_search",
107103 "image_view",
104] as const;
105
106export const ToolLifecycleItemType = Schema.Literals(TOOL_LIFECYCLE_ITEM_TYPES);
107export type ToolLifecycleItemType = typeof ToolLifecycleItemType.Type;
108
109export function isToolLifecycleItemType(value: string): value is ToolLifecycleItemType {
110 return TOOL_LIFECYCLE_ITEM_TYPES.includes(value as ToolLifecycleItemType);
111}
112
113export const CanonicalItemType = Schema.Literals([
114 "user_message",
115 "assistant_message",
116 "reasoning",
117 "plan",
118 ...TOOL_LIFECYCLE_ITEM_TYPES,
108119 "review_entered",
109120 "review_exited",
110121 "context_compaction",