diff --git a/.changeset/patch-large-mcp-output-handling.md b/.changeset/patch-large-mcp-output-handling.md new file mode 100644 index 0000000000..0d627f6516 --- /dev/null +++ b/.changeset/patch-large-mcp-output-handling.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add automatic file writing for large MCP tool outputs exceeding 16,000 tokens with compact schema descriptions diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index 625b3695bb..56e01439d9 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -282,6 +282,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -294,6 +339,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2487,96 +2559,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index ea622d7b5a..01df7f30da 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -432,6 +432,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -444,6 +489,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index eaaf2d246d..1d8580e35b 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -1290,6 +1290,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -1302,6 +1347,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -3601,96 +3673,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/changeset-generator.lock.yml b/.github/workflows/changeset-generator.lock.yml index 54bb360561..f560f22c5e 100644 --- a/.github/workflows/changeset-generator.lock.yml +++ b/.github/workflows/changeset-generator.lock.yml @@ -905,6 +905,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -917,6 +962,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index daa46585e3..be34b6b18c 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -682,6 +682,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -694,6 +739,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -3022,96 +3094,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index b52adbdeb9..cba49013af 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -398,6 +398,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -410,6 +455,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index f35c68e5cc..c267bcf83c 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -421,6 +421,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -433,6 +478,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index bfa8d1b134..e6bc4cbad4 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -412,6 +412,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -424,6 +469,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index a4063df573..e6619194a8 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -309,6 +309,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -321,6 +366,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2665,96 +2737,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index 2c0badd1c6..ec2e81fee6 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -659,6 +659,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -671,6 +716,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2922,96 +2994,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 50fae58af9..1b26e4b498 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -943,96 +943,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 211a339e2c..55bb8b5f52 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -309,6 +309,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -321,6 +366,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml index 19538b7522..e440704fd8 100644 --- a/.github/workflows/example-workflow-analyzer.lock.yml +++ b/.github/workflows/example-workflow-analyzer.lock.yml @@ -398,6 +398,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -410,6 +455,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index dd9a04ec30..2207b09a33 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -410,6 +410,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -422,6 +467,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 3809bf46d7..e7f9e4ece7 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -397,6 +397,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -409,6 +454,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index dce4400ddc..78548f5041 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -1010,6 +1010,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -1022,6 +1067,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index b949851ba0..d0404a101e 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -411,6 +411,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -423,6 +468,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index bfa03ea9c8..efe1385165 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -735,6 +735,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -747,6 +792,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -3443,96 +3515,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml index b4a9013695..23bd2794d0 100644 --- a/.github/workflows/notion-issue-summary.lock.yml +++ b/.github/workflows/notion-issue-summary.lock.yml @@ -282,6 +282,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -294,6 +339,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2459,96 +2531,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index 0b47eecb13..47f4a95e2d 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -1203,6 +1203,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -1215,6 +1260,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -3551,96 +3623,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index c4e7f5c4cd..6c6130b324 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -785,6 +785,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -797,6 +842,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -3067,96 +3139,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 0429a8cac7..00aa7532f4 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1465,6 +1465,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -1477,6 +1522,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -3795,96 +3867,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index 8b109d1bbb..a85ded0618 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -1242,6 +1242,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -1254,6 +1299,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -3854,96 +3926,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index f321bb8b7f..2833b34501 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -280,6 +280,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -292,6 +337,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2554,96 +2626,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml index 8636139c2a..f584557088 100644 --- a/.github/workflows/research.lock.yml +++ b/.github/workflows/research.lock.yml @@ -289,6 +289,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -301,6 +346,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2477,96 +2549,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index 3116830418..745f95fab6 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -1481,6 +1481,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -1493,6 +1538,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -4067,96 +4139,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index 504e3ef992..b73369213b 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -410,6 +410,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -422,6 +467,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 7dc598483e..9293585430 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -387,6 +387,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -399,6 +444,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 14cd7790d2..7889868968 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -283,6 +283,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -295,6 +340,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index e592f302e4..5a44084261 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -278,6 +278,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -290,6 +335,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2427,96 +2499,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/smoke-genaiscript.lock.yml b/.github/workflows/smoke-genaiscript.lock.yml index c50ce38ef6..cda1548c19 100644 --- a/.github/workflows/smoke-genaiscript.lock.yml +++ b/.github/workflows/smoke-genaiscript.lock.yml @@ -268,6 +268,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -280,6 +325,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/smoke-opencode.lock.yml b/.github/workflows/smoke-opencode.lock.yml index 8faf3f0f96..1e7b458560 100644 --- a/.github/workflows/smoke-opencode.lock.yml +++ b/.github/workflows/smoke-opencode.lock.yml @@ -268,6 +268,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -280,6 +325,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 23106a3012..12fd53dd15 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -810,6 +810,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -822,6 +867,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/test-jqschema.lock.yml b/.github/workflows/test-jqschema.lock.yml index 3e76318cc5..dfd7c6bb1d 100644 --- a/.github/workflows/test-jqschema.lock.yml +++ b/.github/workflows/test-jqschema.lock.yml @@ -1017,96 +1017,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/test-post-steps.lock.yml b/.github/workflows/test-post-steps.lock.yml index df85491cd4..552ce93c8e 100644 --- a/.github/workflows/test-post-steps.lock.yml +++ b/.github/workflows/test-post-steps.lock.yml @@ -914,96 +914,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/test-svelte.lock.yml b/.github/workflows/test-svelte.lock.yml index 858d7a361c..c04ec7ec65 100644 --- a/.github/workflows/test-svelte.lock.yml +++ b/.github/workflows/test-svelte.lock.yml @@ -952,96 +952,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 727b3cdcb0..0ba102e990 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -648,6 +648,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -660,6 +705,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2952,96 +3024,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index c25f22bbed..aeda8384eb 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -1154,6 +1154,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -1166,6 +1211,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index bab3df07f8..e547b3848c 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -297,6 +297,51 @@ jobs: }; writeMessage(res); } + function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); + } + function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + return `${typeof parsed}`; + } catch { + return "text content"; + } + } + function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + fs.writeFileSync(filepath, content, "utf8"); + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + const description = generateCompactSchema(content); + return { + filename: filename, + description: description, + }; + } function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); entry.type = entry.type.replace(/-/g, "_"); @@ -309,6 +354,33 @@ jobs: } const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + if (largeContent && largeFieldName) { + const fileInfo = writeLargeContentToFile(largeContent); + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } appendSafeOutput(entry); return { content: [ @@ -2722,96 +2794,96 @@ jobs: try { const jsonStr = currentJsonLines.join("\n"); const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; + if (jsonData.model) { + model = jsonData.model; + } + if (jsonData.choices && Array.isArray(jsonData.choices)) { + for (const choice of jsonData.choices) { + if (choice.message) { + const message = choice.message; + const content = []; + const toolResults = []; + if (message.content && message.content.trim()) { + content.push({ + type: "text", + text: message.content, + }); + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + for (const toolCall of message.tool_calls) { + if (toolCall.function) { + let toolName = toolCall.function.name; + let args = {}; + if (toolName.startsWith("github-")) { + toolName = "mcp__github__" + toolName.substring(7); + } else if (toolName === "bash") { + toolName = "Bash"; + } + try { + args = JSON.parse(toolCall.function.arguments); + } catch (e) { + args = {}; + } + const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; + content.push({ + type: "tool_use", + id: toolId, + name: toolName, + input: args, + }); + toolResults.push({ + type: "tool_result", + tool_use_id: toolId, + content: "", + is_error: false, + }); } - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: "", - is_error: false, - }); } } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { + if (content.length > 0) { entries.push({ - type: "user", - message: { content: toolResults }, + type: "assistant", + message: { content }, }); + turnCount++; + if (toolResults.length > 0) { + entries.push({ + type: "user", + message: { content: toolResults }, + }); + } } } } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, + if (jsonData.usage) { + if (!entries._accumulatedUsage) { + entries._accumulatedUsage = { + input_tokens: 0, + output_tokens: 0, + }; + } + if (jsonData.usage.prompt_tokens) { + entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; + } + if (jsonData.usage.completion_tokens) { + entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; + } + entries._lastResult = { + type: "result", + num_turns: turnCount, + usage: entries._accumulatedUsage, }; } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; } + } catch (e) { } - } catch (e) { } + inDataBlock = false; + currentJsonLines = []; + continue; + } else if (hasTimestamp && isJsonContent) { + currentJsonLines.push(cleanLine); } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } } else { const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); currentJsonLines.push(cleanLine); diff --git a/pkg/workflow/js/safe_outputs_mcp_large_content.test.cjs b/pkg/workflow/js/safe_outputs_mcp_large_content.test.cjs new file mode 100644 index 0000000000..5284134f09 --- /dev/null +++ b/pkg/workflow/js/safe_outputs_mcp_large_content.test.cjs @@ -0,0 +1,529 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import fs from "fs"; +import path from "path"; +import { spawn } from "child_process"; +import crypto from "crypto"; + +describe("safe_outputs_mcp_server.cjs large content handling", () => { + let originalEnv; + let tempOutputDir; + let tempConfigFile; + let tempOutputFile; + + beforeEach(() => { + originalEnv = { ...process.env }; + + // Create temporary directories for testing + tempOutputDir = path.join("/tmp", `test_large_content_${Date.now()}`); + fs.mkdirSync(tempOutputDir, { recursive: true }); + + tempConfigFile = path.join(tempOutputDir, "config.json"); + tempOutputFile = path.join(tempOutputDir, "outputs.jsonl"); + + // Create a simple config with create_issue enabled + const config = { + "create-issue": {}, + }; + fs.writeFileSync(tempConfigFile, JSON.stringify(config)); + }); + + afterEach(() => { + process.env = originalEnv; + + // Clean up temporary files + if (fs.existsSync(tempOutputDir)) { + fs.rmSync(tempOutputDir, { recursive: true, force: true }); + } + }); + + it("should write large content to file when exceeding 16000 tokens", async () => { + // Set up environment + process.env.GH_AW_SAFE_OUTPUTS = tempOutputFile; + process.env.GH_AW_SAFE_OUTPUTS_CONFIG = fs.readFileSync(tempConfigFile, "utf8"); + + const serverPath = path.join(__dirname, "safe_outputs_mcp_server.cjs"); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + child.kill(); + reject(new Error("Test timeout")); + }, 10000); + + const child = spawn("node", [serverPath], { + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env }, + }); + + let stderr = ""; + let stdout = ""; + + child.stderr.on("data", data => { + stderr += data.toString(); + }); + + child.stdout.on("data", data => { + stdout += data.toString(); + }); + + child.on("error", error => { + clearTimeout(timeout); + reject(error); + }); + + // Send initialization + const initMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test-client", version: "1.0.0" }, + }, + }) + "\n"; + + child.stdin.write(initMessage); + + // Wait for initialization, then send large content + setTimeout(() => { + // Create content that's larger than 16000 tokens (> 64000 characters) + const largeBody = "A".repeat(70000); // ~17500 tokens + + const toolCallMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "create_issue", + arguments: { + title: "Test Issue", + body: largeBody, + }, + }, + }) + "\n"; + + child.stdin.write(toolCallMessage); + + // Wait for response + setTimeout(() => { + child.kill(); + clearTimeout(timeout); + + // Parse the response + const responses = stdout.trim().split("\n"); + const toolCallResponse = responses.find(line => { + try { + const parsed = JSON.parse(line); + return parsed.id === 2; + } catch { + return false; + } + }); + + expect(toolCallResponse).toBeDefined(); + + const parsed = JSON.parse(toolCallResponse); + expect(parsed.result).toBeDefined(); + expect(parsed.result.content).toBeDefined(); + expect(parsed.result.content.length).toBeGreaterThan(0); + + const responseText = parsed.result.content[0].text; + const responseObj = JSON.parse(responseText); + + // Check response format + expect(responseObj.filename).toBeDefined(); + expect(responseObj.description).toBeDefined(); + // Description should be a schema description, not the old static text + expect(responseObj.description).not.toBe("generated content large!"); + + // Verify file was created + const expectedFilePath = path.join("/tmp/gh-aw/safe-outputs", responseObj.filename); + expect(fs.existsSync(expectedFilePath)).toBe(true); + + // Verify file content + const fileContent = fs.readFileSync(expectedFilePath, "utf8"); + expect(fileContent).toBe(largeBody); + + // Verify filename is SHA256 hash + .json extension (always JSON now) + const hash = crypto.createHash("sha256").update(largeBody).digest("hex"); + expect(responseObj.filename).toBe(`${hash}.json`); + + // Verify safe output was written with file reference + const outputLines = fs.readFileSync(tempOutputFile, "utf8").trim().split("\n"); + const lastOutput = JSON.parse(outputLines[outputLines.length - 1]); + expect(lastOutput.type).toBe("create_issue"); + expect(lastOutput.body).toContain("Content too large, saved to file:"); + expect(lastOutput.body).toContain(responseObj.filename); + + // Clean up created file + if (fs.existsSync(expectedFilePath)) { + fs.unlinkSync(expectedFilePath); + } + + resolve(); + }, 1000); + }, 1000); + }); + }); + + it("should handle normal content without writing to file", async () => { + // Set up environment + process.env.GH_AW_SAFE_OUTPUTS = tempOutputFile; + process.env.GH_AW_SAFE_OUTPUTS_CONFIG = fs.readFileSync(tempConfigFile, "utf8"); + + const serverPath = path.join(__dirname, "safe_outputs_mcp_server.cjs"); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + child.kill(); + reject(new Error("Test timeout")); + }, 10000); + + const child = spawn("node", [serverPath], { + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env }, + }); + + let stderr = ""; + let stdout = ""; + + child.stderr.on("data", data => { + stderr += data.toString(); + }); + + child.stdout.on("data", data => { + stdout += data.toString(); + }); + + child.on("error", error => { + clearTimeout(timeout); + reject(error); + }); + + // Send initialization + const initMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test-client", version: "1.0.0" }, + }, + }) + "\n"; + + child.stdin.write(initMessage); + + // Wait for initialization, then send normal content + setTimeout(() => { + const normalBody = "This is a normal issue body."; + + const toolCallMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "create_issue", + arguments: { + title: "Test Issue", + body: normalBody, + }, + }, + }) + "\n"; + + child.stdin.write(toolCallMessage); + + // Wait for response + setTimeout(() => { + child.kill(); + clearTimeout(timeout); + + // Parse the response + const responses = stdout.trim().split("\n"); + const toolCallResponse = responses.find(line => { + try { + const parsed = JSON.parse(line); + return parsed.id === 2; + } catch { + return false; + } + }); + + expect(toolCallResponse).toBeDefined(); + + const parsed = JSON.parse(toolCallResponse); + expect(parsed.result).toBeDefined(); + expect(parsed.result.content).toBeDefined(); + expect(parsed.result.content.length).toBeGreaterThan(0); + + const responseText = parsed.result.content[0].text; + const responseObj = JSON.parse(responseText); + + // Normal response should just be success + expect(responseObj.result).toBe("success"); + + // Verify safe output was written normally + const outputLines = fs.readFileSync(tempOutputFile, "utf8").trim().split("\n"); + const lastOutput = JSON.parse(outputLines[outputLines.length - 1]); + expect(lastOutput.type).toBe("create_issue"); + expect(lastOutput.body).toBe(normalBody); + + resolve(); + }, 1000); + }, 1000); + }); + }); + + it("should detect JSON content and use .json extension", async () => { + // Set up environment + process.env.GH_AW_SAFE_OUTPUTS = tempOutputFile; + process.env.GH_AW_SAFE_OUTPUTS_CONFIG = fs.readFileSync(tempConfigFile, "utf8"); + + const serverPath = path.join(__dirname, "safe_outputs_mcp_server.cjs"); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + child.kill(); + reject(new Error("Test timeout")); + }, 10000); + + const child = spawn("node", [serverPath], { + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env }, + }); + + let stderr = ""; + let stdout = ""; + + child.stderr.on("data", data => { + stderr += data.toString(); + }); + + child.stdout.on("data", data => { + stdout += data.toString(); + }); + + child.on("error", error => { + clearTimeout(timeout); + reject(error); + }); + + // Send initialization + const initMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test-client", version: "1.0.0" }, + }, + }) + "\n"; + + child.stdin.write(initMessage); + + // Wait for initialization, then send JSON content + setTimeout(() => { + // Create large JSON content (> 16000 tokens) + const largeArray = Array(2000) + .fill(null) + .map((_, i) => ({ id: i, name: `Item ${i}`, data: "X".repeat(30) })); + const largeBody = JSON.stringify(largeArray, null, 2); + + const toolCallMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "create_issue", + arguments: { + title: "Test Issue", + body: largeBody, + }, + }, + }) + "\n"; + + child.stdin.write(toolCallMessage); + + // Wait for response + setTimeout(() => { + child.kill(); + clearTimeout(timeout); + + // Parse the response + const responses = stdout.trim().split("\n"); + const toolCallResponse = responses.find(line => { + try { + const parsed = JSON.parse(line); + return parsed.id === 2; + } catch { + return false; + } + }); + + expect(toolCallResponse).toBeDefined(); + + const parsed = JSON.parse(toolCallResponse); + const responseText = parsed.result.content[0].text; + const responseObj = JSON.parse(responseText); + + // Check that filename has .json extension (always JSON now) + expect(responseObj.filename).toMatch(/\.json$/); + + // Verify description contains schema info for array + expect(responseObj.description).toBeDefined(); + expect(responseObj.description).toContain("items"); // Should mention number of items + expect(responseObj.description).toContain("id, name, data"); // Should list keys + + // Verify file was created with JSON extension + const expectedFilePath = path.join("/tmp/gh-aw/safe-outputs", responseObj.filename); + expect(fs.existsSync(expectedFilePath)).toBe(true); + + // Verify content is valid JSON + const fileContent = fs.readFileSync(expectedFilePath, "utf8"); + expect(() => JSON.parse(fileContent)).not.toThrow(); + + // Clean up + if (fs.existsSync(expectedFilePath)) { + fs.unlinkSync(expectedFilePath); + } + + resolve(); + }, 1000); + }, 1000); + }); + }); + + it("should always use .json extension even for non-JSON content", async () => { + // Set up environment + process.env.GH_AW_SAFE_OUTPUTS = tempOutputFile; + process.env.GH_AW_SAFE_OUTPUTS_CONFIG = fs.readFileSync(tempConfigFile, "utf8"); + + const serverPath = path.join(__dirname, "safe_outputs_mcp_server.cjs"); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + child.kill(); + reject(new Error("Test timeout")); + }, 10000); + + const child = spawn("node", [serverPath], { + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env }, + }); + + let stderr = ""; + let stdout = ""; + + child.stderr.on("data", data => { + stderr += data.toString(); + }); + + child.stdout.on("data", data => { + stdout += data.toString(); + }); + + child.on("error", error => { + clearTimeout(timeout); + reject(error); + }); + + // Send initialization + const initMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test-client", version: "1.0.0" }, + }, + }) + "\n"; + + child.stdin.write(initMessage); + + // Wait for initialization, then send Markdown content + setTimeout(() => { + // Create large Markdown content (> 16000 tokens) - not valid JSON + let largeBody = "# Large Markdown Document\n\n"; + for (let i = 0; i < 1000; i++) { + largeBody += `## Section ${i}\n\n`; + largeBody += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(20); + largeBody += "\n\n"; + largeBody += "```javascript\n"; + largeBody += "const example = 'code block';\n"; + largeBody += "console.log(example);\n"; + largeBody += "```\n\n"; + } + + const toolCallMessage = + JSON.stringify({ + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "create_issue", + arguments: { + title: "Test Issue", + body: largeBody, + }, + }, + }) + "\n"; + + child.stdin.write(toolCallMessage); + + // Wait for response + setTimeout(() => { + child.kill(); + clearTimeout(timeout); + + // Parse the response + const responses = stdout.trim().split("\n"); + const toolCallResponse = responses.find(line => { + try { + const parsed = JSON.parse(line); + return parsed.id === 2; + } catch { + return false; + } + }); + + expect(toolCallResponse).toBeDefined(); + + const parsed = JSON.parse(toolCallResponse); + const responseText = parsed.result.content[0].text; + const responseObj = JSON.parse(responseText); + + // Check that filename has .json extension (always .json now) + expect(responseObj.filename).toMatch(/\.json$/); + + // For non-JSON content, description should be "text content" + expect(responseObj.description).toBe("text content"); + + // Verify file was created with .json extension + const expectedFilePath = path.join("/tmp/gh-aw/safe-outputs", responseObj.filename); + expect(fs.existsSync(expectedFilePath)).toBe(true); + + // Verify content is preserved + const fileContent = fs.readFileSync(expectedFilePath, "utf8"); + expect(fileContent).toContain("# Large Markdown Document"); + expect(fileContent).toContain("```javascript"); + + // Clean up + if (fs.existsSync(expectedFilePath)) { + fs.unlinkSync(expectedFilePath); + } + + resolve(); + }, 1000); + }, 1000); + }); + }); +}); diff --git a/pkg/workflow/js/safe_outputs_mcp_server.cjs b/pkg/workflow/js/safe_outputs_mcp_server.cjs index ccf88571f2..e2fdc863ed 100644 --- a/pkg/workflow/js/safe_outputs_mcp_server.cjs +++ b/pkg/workflow/js/safe_outputs_mcp_server.cjs @@ -190,6 +190,87 @@ function replyError(id, code, message) { writeMessage(res); } +/** + * Estimates token count from text using 4 chars per token estimate + * @param {string} text - The text to estimate tokens for + * @returns {number} Approximate token count + */ +function estimateTokens(text) { + if (!text) return 0; + return Math.ceil(text.length / 4); +} + +/** + * Generates a compact schema description from JSON content + * @param {string} content - The JSON content to analyze + * @returns {string} Compact schema description for jq/agent + */ +function generateCompactSchema(content) { + try { + const parsed = JSON.parse(content); + + // Generate a compact schema based on the structure + if (Array.isArray(parsed)) { + if (parsed.length === 0) { + return "[]"; + } + // For arrays, describe the first element's structure + const firstItem = parsed[0]; + if (typeof firstItem === "object" && firstItem !== null) { + const keys = Object.keys(firstItem); + return `[{${keys.join(", ")}}] (${parsed.length} items)`; + } + return `[${typeof firstItem}] (${parsed.length} items)`; + } else if (typeof parsed === "object" && parsed !== null) { + // For objects, list top-level keys + const keys = Object.keys(parsed); + if (keys.length > 10) { + return `{${keys.slice(0, 10).join(", ")}, ...} (${keys.length} keys)`; + } + return `{${keys.join(", ")}}`; + } + + return `${typeof parsed}`; + } catch { + // If not valid JSON, return generic description + return "text content"; + } +} + +/** + * Writes large content to a file and returns metadata + * @param {string} content - The content to write + * @returns {Object} Object with filename and description + */ +function writeLargeContentToFile(content) { + const logsDir = "/tmp/gh-aw/safe-outputs"; + + // Ensure directory exists + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + + // Generate SHA256 hash of content + const hash = crypto.createHash("sha256").update(content).digest("hex"); + + // MCP tools return JSON, so always use .json extension + const filename = `${hash}.json`; + const filepath = path.join(logsDir, filename); + + // Write content to file + fs.writeFileSync(filepath, content, "utf8"); + + debug(`Wrote large content (${content.length} chars) to ${filepath}`); + + // Generate compact schema description for jq/agent + const description = generateCompactSchema(content); + + return { + filename: filename, + description: description, + }; +} + function appendSafeOutput(entry) { if (!outputFile) throw new Error("No output file configured"); // Normalize type to use underscores (convert any dashes to underscores) @@ -204,6 +285,46 @@ function appendSafeOutput(entry) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; + + // Check if any field in the entry has content exceeding 16000 tokens + let largeContent = null; + let largeFieldName = null; + const TOKEN_THRESHOLD = 16000; + + for (const [key, value] of Object.entries(entry)) { + if (typeof value === "string") { + const tokens = estimateTokens(value); + if (tokens > TOKEN_THRESHOLD) { + largeContent = value; + largeFieldName = key; + debug(`Field '${key}' has ${tokens} tokens (exceeds ${TOKEN_THRESHOLD})`); + break; + } + } + } + + if (largeContent && largeFieldName) { + // Write large content to file + const fileInfo = writeLargeContentToFile(largeContent); + + // Replace large field with file reference + entry[largeFieldName] = `[Content too large, saved to file: ${fileInfo.filename}]`; + + // Append modified entry to safe outputs + appendSafeOutput(entry); + + // Return file info to the agent + return { + content: [ + { + type: "text", + text: JSON.stringify(fileInfo), + }, + ], + }; + } + + // Normal case - no large content appendSafeOutput(entry); return { content: [