Repository navigation
feat(chat): scannable tool call rows for every provider #9186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e8257a8
6009a45
0ab2e8b
d8c4e74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ import { | |
| type OrchestrationThreadActivity, | ||
| type ProviderRuntimeEvent, | ||
| RuntimeRequestId, | ||
| type ToolCallFacts, | ||
| } from "@t3tools/contracts"; | ||
| import * as Cache from "effect/Cache"; | ||
| import * as Cause from "effect/Cause"; | ||
|
|
@@ -50,6 +51,7 @@ import { | |
| } from "../Services/ProviderRuntimeIngestion.ts"; | ||
| import { projectActivityPayload } from "../ActivityPayloadProjection.ts"; | ||
| import { forkParked } from "../../serverActivation.ts"; | ||
| import { deriveToolCallFacts } from "../toolCallFacts.ts"; | ||
| import { ServerSettingsService } from "../../serverSettings.ts"; | ||
| import { canReplaceThreadTitle } from "../threadTitles.ts"; | ||
|
|
||
|
|
@@ -163,6 +165,18 @@ function maxCheckpointTurnCount( | |
| return maxTurnCount; | ||
| } | ||
|
|
||
| /** | ||
| * Provider-neutral facts (cwd, exit code, duration, bounded output, file | ||
| * stats) derived from the item's native data. Lives beside `data` so the | ||
| * payload projection keeps it while slimming `data` for the wire. | ||
| */ | ||
| function toolCallFactsField( | ||
| event: Extract<ProviderRuntimeEvent, { type: "item.updated" | "item.completed" }>, | ||
| ): { facts?: ToolCallFacts } { | ||
| const facts = deriveToolCallFacts({ provider: event.provider, data: event.payload.data }); | ||
| return facts ? { facts } : {}; | ||
| } | ||
|
|
||
| function truncateDetail(value: string, limit = 180): string { | ||
| return value.length > limit ? `${value.slice(0, limit - 3)}...` : value; | ||
| } | ||
|
|
@@ -815,6 +829,7 @@ export function runtimeEventToActivities( | |
| ...(event.payload.toolIcon ? { toolIcon: event.payload.toolIcon } : {}), | ||
| ...(event.payload.toolSource ? { toolSource: event.payload.toolSource } : {}), | ||
| ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), | ||
| ...toolCallFactsField(event), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Confirm whether projectActivityPayload preserves the `facts` key.
rg -nP -C 25 'function projectActivityPayload' --type=tsRepository: pingdotgg/t3code Length of output: 4826 🤖 get_repo_knowledge executed:
Length of output: 2718 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- projection implementation ---'
sed -n '359,430p' apps/server/src/orchestration/ActivityPayloadProjection.ts
printf '%s\n' '--- ingestion changed area and related definitions ---'
sed -n '150,190p' apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
sed -n '780,850p' apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
rg -n -C 18 'deriveToolCallFacts|toolCallFactsField|aggregatedOutput|textPairStat' apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.tsRepository: pingdotgg/t3code Length of output: 13436 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- facts derivation implementation ---'
fd -i 'toolCallFacts' . --type f
sed -n '1,280p' apps/server/src/orchestration/toolCallFacts.ts
printf '%s\n' '--- completed payload construction ---'
sed -n '844,875p' apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.tsRepository: pingdotgg/t3code Length of output: 12473 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- remaining facts derivation and dispatcher ---'
sed -n '280,430p' apps/server/src/orchestration/toolCallFacts.ts
printf '%s\n' '--- event merge/streaming construction references ---'
rg -n -C 12 'aggregatedOutput|item.updated|merge.*data|payload.data|textPairStat' apps/server/src/orchestration --glob '*.ts'Repository: pingdotgg/t3code Length of output: 50372 Avoid deriving facts from the full payload on every streaming update.
🤖 Prompt for AI Agents |
||
| ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), | ||
| ...(event.payload.parentToolUseId | ||
| ? { parentToolUseId: event.payload.parentToolUseId } | ||
|
|
@@ -847,6 +862,7 @@ export function runtimeEventToActivities( | |
| ...(event.payload.toolIcon ? { toolIcon: event.payload.toolIcon } : {}), | ||
| ...(event.payload.toolSource ? { toolSource: event.payload.toolSource } : {}), | ||
| ...(event.payload.data !== undefined ? { data: event.payload.data } : {}), | ||
| ...toolCallFactsField(event), | ||
| ...(event.payload.agentId ? { agentId: event.payload.agentId } : {}), | ||
| ...(event.payload.parentToolUseId | ||
| ? { parentToolUseId: event.payload.parentToolUseId } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
|
|
||
| import { | ||
| boundFiles, | ||
| boundToolOutput, | ||
| deriveToolCallFacts, | ||
| textPairStat, | ||
| TOOL_DIFF_TOTAL_MAX_CHARS, | ||
| TOOL_FILES_MAX, | ||
| TOOL_OUTPUT_MAX_LINES, | ||
| unifiedDiffStat, | ||
| } from "./toolCallFacts.ts"; | ||
|
|
||
| const DIFF = `--- a/src/app.ts | ||
| +++ b/src/app.ts | ||
| @@ -1,3 +1,4 @@ | ||
| import x from "x"; | ||
| -const a = 1; | ||
| +const a = 2; | ||
| +const b = 3; | ||
| `; | ||
|
|
||
| describe("deriveToolCallFacts", () => { | ||
| it("reads Codex command items verbatim", () => { | ||
| expect( | ||
| deriveToolCallFacts({ | ||
| provider: "codex", | ||
| data: { | ||
| item: { | ||
| type: "commandExecution", | ||
| command: "pnpm test", | ||
| cwd: "/repo/apps/web", | ||
| exitCode: 1, | ||
| durationMs: 4210, | ||
| aggregatedOutput: "FAIL src/a.test.ts\n expected 1 to be 2\n", | ||
| }, | ||
| }, | ||
| }), | ||
| ).toEqual({ | ||
| cwd: "/repo/apps/web", | ||
| exitCode: 1, | ||
| durationMs: 4210, | ||
| output: { text: "FAIL src/a.test.ts\n expected 1 to be 2", lineCount: 2, truncated: false }, | ||
| }); | ||
| }); | ||
|
|
||
| it("keeps Codex file changes with diff stats and a bounded hunk", () => { | ||
| const facts = deriveToolCallFacts({ | ||
| provider: "codex", | ||
| data: { | ||
| item: { type: "fileChange", changes: [{ path: "src/app.ts", kind: "update", diff: DIFF }] }, | ||
| }, | ||
| }); | ||
| expect(facts?.files).toEqual([ | ||
| { path: "src/app.ts", kind: "update", additions: 2, deletions: 1, diff: DIFF }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("reads Claude Bash intent and tool_result text", () => { | ||
| expect( | ||
| deriveToolCallFacts({ | ||
| provider: "claudeAgent", | ||
| data: { | ||
| toolName: "Bash", | ||
| input: { command: "ls", description: "List the project root" }, | ||
| result: { | ||
| type: "tool_result", | ||
| content: [{ type: "text", text: "a\nb" }], | ||
| is_error: false, | ||
| }, | ||
| }, | ||
| }), | ||
| ).toEqual({ | ||
| intent: "List the project root", | ||
| output: { text: "a\nb", lineCount: 2, truncated: false }, | ||
| }); | ||
| }); | ||
|
|
||
| it("derives edit stats for Claude Edit and Write calls", () => { | ||
| expect( | ||
| deriveToolCallFacts({ | ||
| provider: "claudeAgent", | ||
| data: { | ||
| toolName: "Edit", | ||
| input: { file_path: "/repo/a.ts", old_string: "x\ny", new_string: "x\nz\nw" }, | ||
| }, | ||
| })?.files, | ||
| ).toEqual([{ path: "/repo/a.ts", kind: "update", additions: 2, deletions: 1 }]); | ||
| expect( | ||
| deriveToolCallFacts({ | ||
| provider: "claudeAgent", | ||
| data: { toolName: "Write", input: { file_path: "/repo/b.ts", content: "1\n2\n3" } }, | ||
| })?.files, | ||
| ).toEqual([{ path: "/repo/b.ts", kind: "write", additions: 3, deletions: 0 }]); | ||
| }); | ||
|
|
||
| it("reads ACP content text and diff parts for Cursor, Grok, and Antigravity", () => { | ||
| const data = { | ||
| kind: "edit", | ||
| content: [ | ||
| { type: "content", content: { type: "text", text: "Applied" } }, | ||
| { type: "diff", path: "src/x.ts", oldText: "a\nb", newText: "a\nc" }, | ||
| ], | ||
| }; | ||
| for (const provider of ["cursor", "grok", "antigravity"]) { | ||
| expect(deriveToolCallFacts({ provider, data })).toEqual({ | ||
| output: { text: "Applied", lineCount: 1, truncated: false }, | ||
| files: [{ path: "src/x.ts", additions: 1, deletions: 1 }], | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| it("reads OpenCode state timing, output, and exit metadata", () => { | ||
| expect( | ||
| deriveToolCallFacts({ | ||
| provider: "opencode", | ||
| data: { | ||
| tool: "bash", | ||
| state: { | ||
| status: "completed", | ||
| input: { command: "make" }, | ||
| output: "ok", | ||
| metadata: { exit: 0 }, | ||
| time: { start: 1000, end: 1750 }, | ||
| }, | ||
| }, | ||
| }), | ||
| ).toEqual({ | ||
| exitCode: 0, | ||
| durationMs: 750, | ||
| output: { text: "ok", lineCount: 1, truncated: false }, | ||
| }); | ||
| }); | ||
|
|
||
| it("returns nothing for unknown providers or empty data", () => { | ||
| expect(deriveToolCallFacts({ provider: "codex", data: {} })).toBeUndefined(); | ||
| expect(deriveToolCallFacts({ provider: "other", data: { item: {} } })).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("boundToolOutput", () => { | ||
| it("caps lines, strips ANSI, and reports the real size", () => { | ||
| const raw = Array.from({ length: 100 }, (_, i) => `[32mline ${i}[0m`).join("\n"); | ||
| const output = boundToolOutput(raw); | ||
| expect(output?.lineCount).toBe(100); | ||
| expect(output?.truncated).toBe(true); | ||
| expect(output?.text.split("\n")).toHaveLength(TOOL_OUTPUT_MAX_LINES); | ||
| expect(output?.text.startsWith("line 0")).toBe(true); | ||
| }); | ||
|
|
||
| it("drops empty output", () => { | ||
| expect(boundToolOutput(" \n ")).toBeUndefined(); | ||
| expect(boundToolOutput(undefined)).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("diff stats", () => { | ||
| it("counts unified diff lines without headers", () => { | ||
| expect(unifiedDiffStat(DIFF)).toEqual({ additions: 2, deletions: 1 }); | ||
| }); | ||
|
|
||
| it("counts before/after line changes", () => { | ||
| expect(textPairStat("a\nb\nc", "a\nc\nd")).toEqual({ additions: 1, deletions: 1 }); | ||
| expect(textPairStat("", "one\ntwo")).toEqual({ additions: 2, deletions: 0 }); | ||
| }); | ||
|
|
||
| it("still detects a same-sized rewrite of a large file", () => { | ||
| const before = Array.from({ length: 700 }, (_, i) => `a${i}`).join("\n"); | ||
| const after = Array.from({ length: 700 }, (_, i) => `b${i}`).join("\n"); | ||
| expect(textPairStat(before, after)).toEqual({ additions: 700, deletions: 700 }); | ||
| }); | ||
|
|
||
| it("bounds file count and total hunk size", () => { | ||
| const hunk = "+".repeat(10_000); | ||
| const files = Array.from({ length: 30 }, (_, i) => ({ | ||
| path: `f${i}.ts`, | ||
| additions: 1, | ||
| deletions: 0, | ||
| diff: hunk, | ||
| })); | ||
| const bounded = boundFiles(files); | ||
| expect(bounded).toHaveLength(TOOL_FILES_MAX); | ||
| const kept = bounded.filter((file) => file.diff !== undefined); | ||
| expect(kept.length * hunk.length).toBeLessThanOrEqual(TOOL_DIFF_TOTAL_MAX_CHARS); | ||
| expect(bounded.every((file) => file.additions === 1)).toBe(true); | ||
| }); | ||
|
|
||
| it("combines ACP stdout and stderr", () => { | ||
| expect( | ||
| deriveToolCallFacts({ | ||
| provider: "cursor", | ||
| data: { kind: "execute", rawOutput: { stdout: "ok", stderr: "warn: x" } }, | ||
| })?.output?.text, | ||
| ).toBe("ok\nwarn: x"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
Layers/ProviderRuntimeIngestion.ts:221toolCallFactsFieldattaches an unboundedfacts.filesarray to everyitem.updatedactivity, so bulk edits can put megabytes of per-file diffs and stats into each streaming update.fromCodexmaps everychanges[]entry andfromAcpappends everydata.contentdiff without a file-count or aggregate-size limit, bypassing the payload projection's slimming and risking event-store, network, and client memory exhaustion. Enforce count and total-size caps before attachingfacts(including the 8 KB per-file diff cap) so the wire representation is bounded.Also found in 2 other location(s)
apps/server/src/orchestration/toolCallFacts.ts:229apps/server/src/orchestration/toolCallFacts.ts:141🤖 Copy this AI Prompt to have your agent fix this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in d8e5acf: facts.files is capped at 20 files and 24 KB of hunks per call (boundFiles), applied for every provider before the facts are attached. Stats stay on files whose hunk is dropped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.