summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 15:08:51 +0900
committerAdam Malczewski <[email protected]>2026-06-21 15:08:51 +0900
commit64823e68e8c6c3bc199f9cb82d97321c830687c3 (patch)
tree9340fbaede62f86b0de9f8281d9415778adcda44
parent4ccdfd34c55bee279ce5e190412afd19cfbb5238 (diff)
downloaddispatch-64823e68e8c6c3bc199f9cb82d97321c830687c3.tar.gz
dispatch-64823e68e8c6c3bc199f9cb82d97321c830687c3.zip
feat(tool-youtube-transcript): write full transcript to /tmp/dispatch on truncation
When the formatted transcript exceeds the 50K char output cap, the tool now writes the full output to /tmp/dispatch/{video_id}.txt and returns the truncated output with a notice pointing to the file path. The writeFile dep is injectable so tests verify without touching the filesystem.
-rw-r--r--packages/tool-youtube-transcript/src/format.test.ts7
-rw-r--r--packages/tool-youtube-transcript/src/format.ts14
-rw-r--r--packages/tool-youtube-transcript/src/tool.test.ts26
-rw-r--r--packages/tool-youtube-transcript/src/tool.ts22
4 files changed, 64 insertions, 5 deletions
diff --git a/packages/tool-youtube-transcript/src/format.test.ts b/packages/tool-youtube-transcript/src/format.test.ts
index 7555318..79832da 100644
--- a/packages/tool-youtube-transcript/src/format.test.ts
+++ b/packages/tool-youtube-transcript/src/format.test.ts
@@ -116,4 +116,11 @@ describe("truncateOutput", () => {
expect(truncateOutput("short", 100)).toBe("short");
expect(truncateOutput("exact", 5)).toBe("exact");
});
+
+ it("includes save path in notice when provided", () => {
+ const output = "a".repeat(100);
+ const result = truncateOutput(output, 50, "/tmp/dispatch/vid123.txt");
+ expect(result).toContain("/tmp/dispatch/vid123.txt");
+ expect(result).toContain("use read_file to access it");
+ });
});
diff --git a/packages/tool-youtube-transcript/src/format.ts b/packages/tool-youtube-transcript/src/format.ts
index 8bb409b..0f3ecc3 100644
--- a/packages/tool-youtube-transcript/src/format.ts
+++ b/packages/tool-youtube-transcript/src/format.ts
@@ -97,14 +97,18 @@ export function formatFailed(data: FailedResponse): string {
}
/**
- * Truncate output to `cap` characters with a trailing notice, identical in
- * spirit to tool-web-search. Duplication across features is the intended trade
- * (isolation over DRY).
+ * Truncate output to `cap` characters with a trailing notice. When `savePath`
+ * is provided, the notice tells the model where the full output was saved.
+ * Duplication across features is the intended trade (isolation over DRY).
*/
-export function truncateOutput(output: string, cap: number): string {
+export function truncateOutput(output: string, cap: number, savePath?: string): string {
if (output.length <= cap) {
return output;
}
const truncated = output.slice(0, cap);
- return `${truncated}\n\n[Output truncated: exceeded ${cap} characters]`;
+ const notice =
+ savePath !== undefined
+ ? `\n\n[Output truncated: exceeded ${cap} characters. Full transcript saved to ${savePath} — use read_file to access it.]`
+ : `\n\n[Output truncated: exceeded ${cap} characters]`;
+ return `${truncated}${notice}`;
}
diff --git a/packages/tool-youtube-transcript/src/tool.test.ts b/packages/tool-youtube-transcript/src/tool.test.ts
index 4fcb63e..59d90fa 100644
--- a/packages/tool-youtube-transcript/src/tool.test.ts
+++ b/packages/tool-youtube-transcript/src/tool.test.ts
@@ -110,4 +110,30 @@ describe("youtube_transcript", () => {
expect(spanOpen).toBeDefined();
expect(spanOpen?.conversationId).toBe("conv-xyz");
});
+
+ it("writes full transcript to /tmp/dispatch/{video_id}.txt when truncated", async () => {
+ const longText = "x".repeat(60_000);
+ const client = makeStubClient(async () => ({
+ status: "completed",
+ video_id: "vid5",
+ full_text: longText,
+ segments: [],
+ }));
+ let writtenPath = "";
+ let writtenContent = "";
+ const tool = createYoutubeTranscriptTool({
+ client,
+ outputCap: 1000,
+ writeFile: (path, content) => {
+ writtenPath = path;
+ writtenContent = content;
+ },
+ });
+ const result = await tool.execute({ url: "https://youtu.be/vid5" }, stubCtx());
+ expect(writtenPath).toBe("/tmp/dispatch/vid5.txt");
+ expect(writtenContent).toContain("x".repeat(60_000));
+ expect(result.content).toContain("/tmp/dispatch/vid5.txt");
+ expect(result.content).toContain("use read_file to access it");
+ expect(result.content.length).toBeLessThan(writtenContent.length);
+ });
});
diff --git a/packages/tool-youtube-transcript/src/tool.ts b/packages/tool-youtube-transcript/src/tool.ts
index 189e42c..a47d693 100644
--- a/packages/tool-youtube-transcript/src/tool.ts
+++ b/packages/tool-youtube-transcript/src/tool.ts
@@ -7,6 +7,7 @@
* rather than thrown, so the model can react to the message.
*/
+import { mkdirSync, writeFileSync } from "node:fs";
import type { ToolContract, ToolExecuteContext, ToolResult } from "@dispatch/kernel";
import type { TranscriptClient } from "./client.js";
import {
@@ -19,10 +20,13 @@ import {
import { validateUrl } from "./validate.js";
const OUTPUT_CAP = 50_000;
+const FULL_OUTPUT_DIR = "/tmp/dispatch";
export interface YoutubeTranscriptToolDeps {
readonly client: TranscriptClient;
readonly outputCap?: number;
+ /** Injected file writer (defaults to real fs write). */
+ readonly writeFile?: (path: string, content: string) => void;
}
const DESCRIPTION =
@@ -41,6 +45,12 @@ const DESCRIPTION =
export function createYoutubeTranscriptTool(deps: YoutubeTranscriptToolDeps): ToolContract {
const client = deps.client;
const cap = deps.outputCap ?? OUTPUT_CAP;
+ const writeFile =
+ deps.writeFile ??
+ ((path, content) => {
+ mkdirSync(FULL_OUTPUT_DIR, { recursive: true });
+ writeFileSync(path, content, "utf-8");
+ });
return {
name: "youtube_transcript",
@@ -67,17 +77,29 @@ export function createYoutubeTranscriptTool(deps: YoutubeTranscriptToolDeps): To
try {
const data: TranscriptResponse = await client.getTranscript(url, ctx.signal);
let output: string;
+ let videoId: string | undefined;
// Check the single-literal discriminants ("completed"/"failed") first,
// so the final else narrows to QueuedResponse — whose `status` is itself
// a `"queued" | "processing"` union TS cannot negatively narrow.
if (data.status === "completed") {
output = formatCompleted(url, data);
+ videoId = data.video_id;
} else if (data.status === "failed") {
output = formatFailed(data);
} else {
output = formatQueued(url, data, Date.now);
}
span.end();
+
+ if (output.length > cap && videoId !== undefined) {
+ const filePath = `${FULL_OUTPUT_DIR}/${videoId}.txt`;
+ try {
+ writeFile(filePath, output);
+ return { content: truncateOutput(output, cap, filePath) };
+ } catch {
+ return { content: truncateOutput(output, cap) };
+ }
+ }
return { content: truncateOutput(output, cap) };
} catch (err: unknown) {
span.end({ err });