diff --git a/.changeset/fix-deepseek-failure-capture.md b/.changeset/fix-deepseek-failure-capture.md new file mode 100644 index 00000000000..2a873533bc5 --- /dev/null +++ b/.changeset/fix-deepseek-failure-capture.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Capture DeepSeek Harness AWF timeouts and failed wrapper exits as canonical session errors. Preserve failed-step diagnostics when stdout is incomplete, and prevent a contradictory wrapper exit zero from masking observed failures in live detection or unified sessions without inventing an exit code. diff --git a/actions/setup/js/detect_agent_errors.cjs b/actions/setup/js/detect_agent_errors.cjs index 8680fe40351..87caa505a22 100644 --- a/actions/setup/js/detect_agent_errors.cjs +++ b/actions/setup/js/detect_agent_errors.cjs @@ -76,9 +76,10 @@ const { detectErrors, buildOutputLines, isStepTimeout, sanitizeModelName } = err const { parseUnknownModelAICreditsAndModelFromAuditLog, parseMaxCacheMissesExceededFromEventLog } = require("./ai_credits_context.cjs"); const { renderLogFromFile } = require("./render_detection_log.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); -const { collectAgentExecution, agentErrorDiagnosticText, parseAgentExitCode } = require("./agent_execution.cjs"); +const { collectAgentExecution, agentErrorDiagnosticText, parseAgentExitCode, isAgentExecutionEvent } = require("./agent_execution.cjs"); const { writeSessionArtifact } = require("./session_artifact.cjs"); const { collectAddMaskedValues } = require("./add_mask_redaction.cjs"); +const { parseDeepSeekLog, reconcileDeepSeekExecution } = require("./parse_deepseek_log.cjs"); // File written by the engine execution step with the epoch milliseconds at which the // engine CLI was started. Used to measure how long the engine ran before it was killed. @@ -229,15 +230,30 @@ async function renderInternalEngineLogOnFailure() { */ function persistAgentExecution(logContent, results, rootDir = process.env.GH_AW_TMP_DIR || "/tmp/gh-aw") { const exitPath = path.join(rootDir, "agent_execution_exit_code.txt"); - const execution = collectAgentExecution({ + const deepSeek = process.env.GH_AW_ENGINE_ID === "deepseek-harness"; + const events = deepSeek ? parseDeepSeekLog(logContent).logEntries : []; + /** @type {import("./types/agent_session").SessionEvent[]} */ + const supplementalErrors = []; + if (deepSeek && !events.some(event => event.type === "session.error")) { + if (results.agenticEngineTimeout) { + supplementalErrors.push({ type: "session.error", data: { message: "DeepSeek Harness execution timed out", code: "agentic_engine_timeout", errorType: "TimeoutError" } }); + } else if (process.env.GH_AW_AGENTIC_EXECUTION_OUTCOME === "failure") { + supplementalErrors.push({ type: "session.error", data: { message: "DeepSeek Harness execution failed", code: "agentic_engine_execution_failed" } }); + } + } + const diagnostics = [...events, ...supplementalErrors]; + let execution = collectAgentExecution({ content: logContent, ...(process.env.GH_AW_ENGINE_ID === "aider" ? { rawSourceEngine: "aider" } : {}), + events: diagnostics, + observations: events.filter(isAgentExecutionEvent).map(event => event.data), categories: buildOutputLines(results) .filter(line => line.endsWith("=true")) .map(line => line.slice(0, -5)), ...(fs.existsSync(exitPath) ? { exitCode: parseAgentExitCode(fs.readFileSync(exitPath, "utf8")) } : {}), }); - writeSessionArtifact(path.join(rootDir, "agent-errors.jsonl"), execution ? [execution] : [], collectAddMaskedValues(logContent)); + if (deepSeek) execution = reconcileDeepSeekExecution(execution, diagnostics); + writeSessionArtifact(path.join(rootDir, "agent-errors.jsonl"), [...supplementalErrors, ...(execution ? [execution] : [])], collectAddMaskedValues(logContent)); } async function main({ rootDir = process.env.GH_AW_TMP_DIR || "/tmp/gh-aw" } = {}) { @@ -255,6 +271,9 @@ async function main({ rootDir = process.env.GH_AW_TMP_DIR || "/tmp/gh-aw" } = {} } const stdioResults = detectErrors(agentErrorDiagnosticText(logContent, process.env.GH_AW_ENGINE_ID === "aider" ? "aider" : undefined)); + if (process.env.GH_AW_ENGINE_ID === "deepseek-harness" && parseDeepSeekLog(logContent).logEntries.some(event => event.type === "session.error" && event.data.code === "agentic_engine_timeout")) { + stdioResults.agenticEngineTimeout = true; + } // Also check the AWF firewall structured JSONL logs for the `unknown_model_ai_credits` // event — the API proxy event log is preferred and the audit log is used as a fallback. diff --git a/actions/setup/js/detect_agent_errors.test.cjs b/actions/setup/js/detect_agent_errors.test.cjs index 5e5bade57cb..58b8b69f5b9 100644 --- a/actions/setup/js/detect_agent_errors.test.cjs +++ b/actions/setup/js/detect_agent_errors.test.cjs @@ -44,6 +44,31 @@ const { } = require("./detect_agent_errors.cjs"); describe("detect_agent_errors.cjs", () => { + it("emits the DeepSeek timeout classification from attributed AWF stdout", async () => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-deepseek-timeout-")); + const stderr = vi.spyOn(process.stderr, "write").mockReturnValue(true); + try { + vi.stubEnv("GH_AW_ENGINE_ID", "deepseek-harness"); + vi.stubEnv("GH_AW_AGENTIC_EXECUTION_OUTCOME", "failure"); + vi.stubEnv("GITHUB_OUTPUT", path.join(rootDir, "github-output")); + vi.stubEnv("GH_AW_ENGINE_INTERNAL_LOGS_DIR", ""); + fs.writeFileSync(path.join(rootDir, "agent-stdio.log"), "[deepseek-harness] configured provider=github model=auto\n[WARN] Agent command timed out after 15 minutes, stopping container...\n"); + fs.writeFileSync(path.join(rootDir, "agent_execution_exit_code.txt"), "0"); + + await main({ rootDir }); + + expect(fs.readFileSync(path.join(rootDir, "github-output"), "utf8")).toContain("agentic_engine_timeout=true"); + const execution = JSON.parse(fs.readFileSync(path.join(rootDir, "agent-errors.jsonl"), "utf8").trim()); + expect(execution.data.categories).toContain("agentic_engine_timeout"); + expect(execution.data).not.toHaveProperty("exitCode"); + expect(stderr.mock.calls.flat().join("\n")).toContain("Detected agentic engine timeout signature"); + } finally { + stderr.mockRestore(); + vi.unstubAllEnvs(); + fs.rmSync(rootDir, { recursive: true, force: true }); + } + }); + it.each([ { log: "", outcome: "success", linked: false }, { log: "Assistant: Access denied by policy settings", outcome: "success", linked: false }, diff --git a/actions/setup/js/parse_deepseek_log.cjs b/actions/setup/js/parse_deepseek_log.cjs index 2ec6cffe1f6..f38a73734d9 100644 --- a/actions/setup/js/parse_deepseek_log.cjs +++ b/actions/setup/js/parse_deepseek_log.cjs @@ -8,6 +8,7 @@ const { stripVTControlCharacters } = require("node:util"); const main = createEngineLogParser({ parserName: "DeepSeek Harness", parseFunction: parseDeepSeekLog, supportsDirectories: false }); const HARNESS_FAILURE = /^\[deepseek-harness\] DeepSeek Harness execution failed with exit code ([1-9]\d*)(?: \(signal=[A-Z0-9]+\))?$/; +const AWF_TIMEOUT = /^\[WARN\] Agent command timed out after [1-9]\d* minutes, stopping container\.\.\.$/; const TOKEN_CLEANUP = /^\[entrypoint\] Unset [A-Z][A-Z0-9_]* from \/proc\/1\/environ$/; const PERMISSION_CLEANUP = /^\[entrypoint\] Relaxed \S+ group permissions for host-side post-processing$/; @@ -69,8 +70,25 @@ function extractHeadlessErrors(lines, configuration) { const shutdown = lines.findIndex((line, index) => index > configuration.index && line === "[INFO] Stopping containers..."); const body = lines.slice(configuration.index + 1, shutdown < 0 ? undefined : shutdown); if (body.some(line => /^[ \t]*(?:(?:user|system)\s*(?:prompt|message)?\s*:|prompt\s*:)/i.test(line))) return []; + const start = body.findIndex(line => line.trim() !== "" && line !== "[entrypoint] Unsetting sensitive tokens from parent shell environment..." && !TOKEN_CLEANUP.test(line)); + if (shutdown < 0 && start >= 0 && AWF_TIMEOUT.test(body[start]) && body.slice(start + 1).every(line => line.trim() === "" || PERMISSION_CLEANUP.test(line))) { + const error = createSessionEvent({}, "session.error", { message: body[start].slice("[WARN] ".length), code: "agentic_engine_timeout", errorType: "TimeoutError" }); + const execution = collectAgentExecution({ events: [error], categories: ["agentic_engine_timeout"] }); + return execution ? [error, execution] : [error]; + } const failureIndex = body.findIndex(line => HARNESS_FAILURE.test(line)); - if (failureIndex < 0) return []; + if (failureIndex < 0) { + if (shutdown < 0) return []; + const terminal = lines.slice(shutdown + 1); + const failure = terminal.find(line => /^\[WARN\] Command completed with exit code: [1-9]\d*$/.test(line)); + const exit = terminal.find(line => /^Process exiting with code:/.test(line)); + if (!failure || terminal.includes("[SUCCESS] Command completed successfully")) return []; + const exitCode = Number(failure.slice("[WARN] Command completed with exit code: ".length)); + if (exitCode > 255 || exit !== `Process exiting with code: ${exitCode}`) return []; + const error = createSessionEvent({}, "session.error", { message: `DeepSeek Harness execution exited with code ${exitCode}`, exitCode }); + const execution = collectAgentExecution({ events: [error], exitCode }); + return execution ? [error, execution] : [error]; + } const exitCode = Number(body[failureIndex].match(HARNESS_FAILURE)?.[1]); if (shutdown >= 0) { const terminal = lines.slice(shutdown + 1); @@ -78,7 +96,6 @@ function extractHeadlessErrors(lines, configuration) { if (terminal.includes("[SUCCESS] Command completed successfully") || (exit !== undefined && exit !== `Process exiting with code: ${exitCode}`)) return []; } if (!body.slice(failureIndex + 1).every(line => line.trim() === "" || PERMISSION_CLEANUP.test(line))) return []; - const start = body.findIndex(line => line.trim() !== "" && line !== "[entrypoint] Unsetting sensitive tokens from parent shell environment..." && !TOKEN_CLEANUP.test(line)); const stackStart = body.findIndex(line => /^file:\/\/\/\S+:\d+(?::\d+)?$/.test(line)); const nodeVersion = body.findIndex((line, index) => index > stackStart && /^Node\.js v\d+\.\d+\.\d+$/.test(line)); const hasNodeStack = stackStart === start && stackStart >= 0 && nodeVersion > stackStart && nodeVersion < failureIndex && body.slice(nodeVersion + 1, failureIndex).every(line => line.trim() === ""); @@ -101,6 +118,23 @@ function extractHeadlessErrors(lines, configuration) { return entries; } +/** + * A wrapper's zero status cannot establish success against an attributed failure. + * Preserve an observed native failure code; a timeout alone has no known code. + * @param {import("./types/agent_session").AgentExecutionEvent | undefined} execution + * @param {import("./types/agent_session").SessionEvent[]} events + * @returns {import("./types/agent_session").AgentExecutionEvent | undefined} + */ +function reconcileDeepSeekExecution(execution, events) { + const errors = events.filter(event => event.type === "session.error"); + if (!execution || execution.data.exitCode !== 0 || !errors.length) return execution; + const reconciled = structuredClone(execution); + const nativeExit = errors.map(event => event.data.exitCode).find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 255); + if (typeof nativeExit === "number") reconciled.data.exitCode = nativeExit; + else delete reconciled.data.exitCode; + return reconciled; +} + /** @param {string} content @returns {boolean} */ function isDeepSeekLog(content) { return findConfiguration(stripVTControlCharacters(content).split(/\r?\n/)) !== undefined; @@ -138,4 +172,4 @@ function parseDeepSeekLog(content) { }; } -module.exports = { main, parseDeepSeekLog, isDeepSeekLog }; +module.exports = { main, parseDeepSeekLog, isDeepSeekLog, reconcileDeepSeekExecution }; diff --git a/actions/setup/js/parse_deepseek_log.test.cjs b/actions/setup/js/parse_deepseek_log.test.cjs index c76b4fe07b9..8fa47b27f17 100644 --- a/actions/setup/js/parse_deepseek_log.test.cjs +++ b/actions/setup/js/parse_deepseek_log.test.cjs @@ -1,15 +1,18 @@ // @ts-check -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import fs, { readFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; +import { spawnSync } from "node:child_process"; import { parseDeepSeekLog, isDeepSeekLog } from "./parse_deepseek_log.cjs"; import { startupFailure, timedOut } from "./fixtures/deepseek_ci_logs.cjs"; import { projectSessionResult } from "./agent_session.cjs"; import { serializeSessionArtifact, writeSessionArtifact } from "./session_artifact.cjs"; import { collectUnifiedSession, parseEngineSession, writeUnifiedSession } from "./unified_session.cjs"; import { parseCustomLog } from "./parse_custom_log.cjs"; +import { persistAgentExecution, detectErrors } from "./detect_agent_errors.cjs"; +import { validateSession } from "./scripts/validate_session.cjs"; // Verified against agent artifact 9781648195 from existing successful Actions // run https://github.com/github/gh-aw/actions/runs/33456355349. @@ -138,6 +141,141 @@ describe("parseDeepSeekLog", () => { expect(parseEngineSession(timedOut, "deepseek-harness")).toEqual(result.logEntries); }); + it("captures the observed AWF timeout without inventing an exit code", () => { + // Existing live run https://github.com/github/gh-aw/actions/runs/42540358186. + const raw = `${timedOut}[WARN] Agent command timed out after 15 minutes, stopping container...\n`; + const result = parseDeepSeekLog(raw); + + expect(result.logEntries.map(event => event.type)).toEqual(["session.init", "session.error", "agent.execution"]); + expect(result.logEntries[1].data).toEqual({ message: "Agent command timed out after 15 minutes, stopping container...", code: "agentic_engine_timeout", errorType: "TimeoutError" }); + expect(result.logEntries[2].data).toEqual({ categories: ["agentic_engine_timeout"], errorCodes: ["agentic_engine_timeout"], errorTypes: ["TimeoutError"] }); + expect(projectSessionResult(result.logEntries)).toBeUndefined(); + expect(JSON.stringify(result)).not.toContain("private fixture prompt"); + }); + + it.each([ + fixture.replace(answer, "[WARN] Agent command timed out after 15 minutes, stopping container..."), + `${timedOut}User prompt: private-input\n[WARN] Agent command timed out after 15 minutes, stopping container...\n`, + `${timedOut}The log says:\n[WARN] Agent command timed out after 15 minutes, stopping container...\n`, + `${timedOut}[WARN] Agent command timed out after 15 minutes, stopping container...\nOther assistant text\n`, + ])("does not attribute quoted or ambiguous timeout text to execution", raw => { + expect(parseDeepSeekLog(raw).logEntries.some(event => event.type === "session.error")).toBe(false); + }); + + it("captures a matching failed wrapper exit independently of unlabeled output", () => { + const raw = fixture.replace("[SUCCESS] Command completed successfully", "[WARN] Command completed with exit code: 7").replace("Process exiting with code: 0", "Process exiting with code: 7"); + const result = parseDeepSeekLog(raw); + + expect(result.logEntries.map(event => event.type)).toEqual(["session.init", "session.error", "agent.execution"]); + expect(result.logEntries[1].data).toEqual({ message: "DeepSeek Harness execution exited with code 7", exitCode: 7 }); + expect(result.logEntries[2].data.exitCode).toBe(7); + expect(JSON.stringify(result)).not.toContain(answer); + }); + + it.each([ + fixture.replace("[SUCCESS] Command completed successfully", "[WARN] Command completed with exit code: 7"), + `${fixture}[WARN] Command completed with exit code: 7\nProcess exiting with code: 7\n`, + fixture.replace("[SUCCESS] Command completed successfully", "[WARN] Command completed with exit code: 256").replace("Process exiting with code: 0", "Process exiting with code: 256"), + ])("rejects contradictory or invalid wrapper failure framing", raw => { + expect(parseDeepSeekLog(raw).logEntries.some(event => event.type === "session.error")).toBe(false); + }); + + it("normalizes a real local timeout even when its wrapper returns zero", () => { + const result = spawnSync( + process.execPath, + [ + "-e", + [ + 'const { spawnSync } = require("node:child_process");', + 'console.log("[deepseek-harness] configured provider=github model=auto");', + 'const child = spawnSync(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { timeout: 50 });', + 'if (child.error?.code !== "ETIMEDOUT") process.exit(1);', + 'console.log("[WARN] Agent command timed out after 15 minutes, stopping container...");', + ].join("\n"), + ], + { encoding: "utf8", timeout: 5000 } + ); + expect(result.error).toBeUndefined(); + expect(result.status).toBe(0); + const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepseek-timeout-capture-")); + try { + vi.stubEnv("GH_AW_ENGINE_ID", "deepseek-harness"); + vi.stubEnv("GH_AW_AGENTIC_EXECUTION_OUTCOME", "failure"); + fs.writeFileSync(path.join(root, "agent-stdio.log"), result.stdout); + fs.writeFileSync(path.join(root, "agent_execution_exit_code.txt"), String(result.status)); + persistAgentExecution(result.stdout, detectErrors(result.stdout), root); + const detected = fs.readFileSync(path.join(root, "agent-errors.jsonl"), "utf8").trimEnd().split("\n").map(JSON.parse); + expect(detected).toHaveLength(1); + expect(detected[0].data.categories).toContain("agentic_engine_timeout"); + expect(detected[0].data).not.toHaveProperty("exitCode"); + + for (const canonical of [false, true]) { + if (canonical) writeSessionArtifact(path.join(root, "agent-session.jsonl"), parseDeepSeekLog(result.stdout).logEntries); + const events = writeUnifiedSession({ rootDir: root, engine: "deepseek-harness", dailyAIC: {} }); + const errors = events.filter(event => event.type === "session.error"); + expect(errors).toHaveLength(1); + expect(errors[0].data.code).toBe("agentic_engine_timeout"); + const execution = events.filter(event => event.type === "agent.execution"); + expect(execution).toHaveLength(1); + expect(execution[0].data.categories).toContain("agentic_engine_timeout"); + expect(execution[0].data).not.toHaveProperty("exitCode"); + expect(events.some(event => event.type === "assistant.message" || event.type === "session.result" || event.type.startsWith("tool."))).toBe(false); + const jsonl = fs.readFileSync(path.join(root, "usage/aw_session.jsonl"), "utf8"); + expect(jsonl.trimEnd().split("\n").map(JSON.parse)).toEqual(events); + expect(validateSession(jsonl)).toBe(events.length); + } + } finally { + vi.unstubAllEnvs(); + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it.each([ + { raw: startupFailure, recordedExit: 0, expectedExit: 1 }, + { raw: startupFailure, recordedExit: 7, expectedExit: 7 }, + { raw: `${timedOut}[WARN] Agent command timed out after 15 minutes, stopping container...\n`, recordedExit: 124, expectedExit: 124 }, + { raw: fixture, recordedExit: 0, expectedExit: 0 }, + ])("reconciles recorded exit $recordedExit with attributed DeepSeek observations", ({ raw, recordedExit, expectedExit }) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepseek-exit-capture-")); + try { + vi.stubEnv("GH_AW_ENGINE_ID", "deepseek-harness"); + vi.stubEnv("GH_AW_AGENTIC_EXECUTION_OUTCOME", ""); + fs.writeFileSync(path.join(root, "agent-stdio.log"), raw); + fs.writeFileSync(path.join(root, "agent_execution_exit_code.txt"), String(recordedExit)); + const fallback = collectUnifiedSession({ rootDir: root, engine: "deepseek-harness", dailyAIC: {} }).events; + expect(fallback.find(event => event.type === "agent.execution").data.exitCode).toBe(expectedExit); + persistAgentExecution(raw, detectErrors(""), root); + const detected = JSON.parse(fs.readFileSync(path.join(root, "agent-errors.jsonl"), "utf8").trim()); + expect(detected.data.exitCode).toBe(expectedExit); + const persisted = collectUnifiedSession({ rootDir: root, engine: "deepseek-harness", dailyAIC: {} }).events; + expect(persisted.find(event => event.type === "agent.execution").data.exitCode).toBe(expectedExit); + } finally { + vi.unstubAllEnvs(); + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it.each([ + { outcome: "failure", timeout: false, code: "agentic_engine_execution_failed" }, + { outcome: "failure", timeout: true, code: "agentic_engine_timeout" }, + ])("retains failed step metadata when no native terminal diagnostics exist: $code", ({ outcome, timeout, code }) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepseek-step-failure-")); + try { + vi.stubEnv("GH_AW_ENGINE_ID", "deepseek-harness"); + vi.stubEnv("GH_AW_AGENTIC_EXECUTION_OUTCOME", outcome); + fs.writeFileSync(path.join(root, "agent_execution_exit_code.txt"), "0"); + persistAgentExecution("", { ...detectErrors(""), agenticEngineTimeout: timeout }, root); + const events = writeUnifiedSession({ rootDir: root, engine: "deepseek-harness", dailyAIC: {} }); + expect(events.filter(event => event.type === "session.error")).toHaveLength(1); + expect(events.find(event => event.type === "session.error").data.code).toBe(code); + expect(events.find(event => event.type === "agent.execution").data).not.toHaveProperty("exitCode"); + expect(events.find(event => event.type === "agent.execution").data.errorCodes).toContain(code); + } finally { + vi.unstubAllEnvs(); + fs.rmSync(root, { recursive: true, force: true }); + } + }); + it("preserves the actual startup error and observed exit as standard session observations", () => { const result = parseDeepSeekLog(startupFailure); const message = "dsh: user patch-layer watching requires the Cordis HMR service"; diff --git a/actions/setup/js/unified_session.cjs b/actions/setup/js/unified_session.cjs index fef9ac5c966..b1ca963303e 100644 --- a/actions/setup/js/unified_session.cjs +++ b/actions/setup/js/unified_session.cjs @@ -18,6 +18,7 @@ const { estimateBaseline, projectModelConfig } = require("./routing_analytics.cj const { normalizeHarnessRetryRecord } = require("./harness_retry_record.cjs"); const { parseHarnessAttempts, correlateHarnessAttempts } = require("./harness_retry_session.cjs"); const { isDurableSessionRecord } = require("./durable_session_record.cjs"); +const { reconcileDeepSeekExecution } = require("./parse_deepseek_log.cjs"); const SESSION_FILE_FORMAT_VERSION = 1; @@ -727,13 +728,18 @@ function collectUnifiedSession({ ) .find(Boolean)?.data; const exitCode = observedExit ?? nativeExit?.exitCode ?? nativeExit?.exit_code; - const execution = collectAgentExecution({ + let execution = collectAgentExecution({ content: stdioContent, ...(engine === "aider" ? { rawSourceEngine: "aider" } : {}), events: executionAgentSources.flatMap(source => source.events), observations: executionObservations, ...(exists(exitFile) ? { exitCode: parseAgentExitCode(read(exitFile)) } : exitCode !== undefined ? { exitCode: validateAgentExitCode(exitCode) } : {}), }); + if (engine === "deepseek-harness") + execution = reconcileDeepSeekExecution( + execution, + executionAgentSources.flatMap(source => source.events) + ); for (const source of agentSources) source.events = source.events.filter(event => event.type !== "agent.execution"); if (execution) { const primary = executionObservations.length diff --git a/docs/src/content/docs/reference/third-party-agent.md b/docs/src/content/docs/reference/third-party-agent.md index 086d762b4f2..5fa9cfa2f98 100644 --- a/docs/src/content/docs/reference/third-party-agent.md +++ b/docs/src/content/docs/reference/third-party-agent.md @@ -64,6 +64,8 @@ These definitions are examples for integration authors, **not supported gh-aw en Read the pinned definition for its exact install and provider contract. Native events and accounting are retained only when observed; process exit zero alone does not prove task completion. +DeepSeek Harness captures attributed startup errors, matching failed wrapper exits, and the observed AWF timeout diagnostic as `session.error` events. Live step failure or timeout metadata also supplies an error when stdout has no diagnostic. These failures survive unified-session collection even if a wrapper records exit zero: an observed nonzero code is retained, otherwise `agent.execution.exitCode` is omitted rather than fabricated. A timeout does not imply native reasoning, tool, usage, or final-answer coverage. + ## Defining an engine A definition contains `engine.id`, metadata, and `engine.behaviors`. Its behaviors install the CLI, configure inference and MCP, invoke it headlessly, and expose logs for session reconstruction. Keep secrets outside the agent sandbox and fail explicitly when required credentials, endpoints, or restrictions cannot be honored.