diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index 00b2786b688..87cfc618528 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -624,6 +624,9 @@ terminal summary, and sanitized error text. - `agent` requests can include `deliver=true` to request outbound delivery. - `bestEffortDeliver=false` keeps strict behavior: unresolved or internal-only delivery targets return `INVALID_REQUEST`. - `bestEffortDeliver=true` allows fallback to session-only execution when no external deliverable route can be resolved (for example internal/webchat sessions or ambiguous multi-channel configs). +- Final `agent` results may include `result.deliveryStatus` when delivery was + requested, using the same `sent`, `suppressed`, `partial_failed`, and `failed` + statuses documented for [`openclaw agent --json --deliver`](/cli/agent#json-delivery-status). ## Versioning diff --git a/docs/tools/agent-send.md b/docs/tools/agent-send.md index daf53324897..5fbf2c906ce 100644 --- a/docs/tools/agent-send.md +++ b/docs/tools/agent-send.md @@ -77,6 +77,9 @@ programmatic delivery. preserve isolation; direct chats collapse to `main`). - Thinking and verbose flags persist into the session store. - Output: plain text by default, or `--json` for structured payload + metadata. +- With `--json --deliver`, the JSON includes delivery status for sent, + suppressed, partial, and failed sends. See + [JSON delivery status](/cli/agent#json-delivery-status). ## Examples diff --git a/src/agents/pi-embedded-runner/delivery-evidence.ts b/src/agents/pi-embedded-runner/delivery-evidence.ts index b4a502dc946..27b78576b1a 100644 --- a/src/agents/pi-embedded-runner/delivery-evidence.ts +++ b/src/agents/pi-embedded-runner/delivery-evidence.ts @@ -11,6 +11,10 @@ type AgentPayloadLike = { export type AgentDeliveryEvidence = { payloads?: unknown; + deliveryStatus?: { + status?: unknown; + errorMessage?: unknown; + }; didSendViaMessagingTool?: unknown; messagingToolSentTexts?: unknown; messagingToolSentMediaUrls?: unknown; @@ -106,3 +110,15 @@ export function hasOutboundDeliveryEvidence(result: AgentDeliveryEvidence): bool hasPositiveNumber(result.meta?.toolSummary?.calls) ); } + +export function getAgentCommandDeliveryFailure(result: AgentDeliveryEvidence): string | undefined { + const status = result.deliveryStatus?.status; + if (status !== "failed" && status !== "partial_failed") { + return undefined; + } + const message = result.deliveryStatus?.errorMessage; + if (hasNonEmptyString(message)) { + return message; + } + return status === "partial_failed" ? "agent delivery partially failed" : "agent delivery failed"; +} diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index 081e193985b..ea3ee5df722 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -732,6 +732,48 @@ describe("deliverSubagentAnnouncement completion delivery", () => { expect(sendMessage).not.toHaveBeenCalled(); }); + it("reports requester-agent delivery failure even when output stayed visible", async () => { + const callGateway = createGatewayMock({ + result: { + payloads: [{ text: "Tests passed and the PR is ready for review." }], + deliveryStatus: { + status: "failed", + errorMessage: "Slack send failed: channel not found", + }, + }, + }); + const sendMessage = createSendMessageMock(); + const result = await deliverSlackThreadAnnouncement({ + callGateway, + sendMessage, + sessionId: "requester-session-4", + isActive: false, + expectsCompletionMessage: true, + directIdempotencyKey: "announce-thread-delivery-status-failed", + internalEvents: [ + { + type: "task_completion", + source: "subagent", + childSessionKey: "agent:worker:subagent:child", + childSessionId: "child-session-id", + announceType: "subagent task", + taskLabel: "thread completion smoke", + status: "ok", + statusLabel: "completed successfully", + result: "child completion output", + replyInstruction: "Summarize the result.", + }, + ], + }); + + expectRecordFields(result, { + delivered: false, + path: "direct", + error: "Slack send failed: channel not found", + }); + expect(sendMessage).not.toHaveBeenCalled(); + }); + it("does not raw-send grouped child results when requester-agent output is empty", async () => { const callGateway = createGatewayMock({ result: { diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index 847c41149df..c03659ef12d 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -23,6 +23,7 @@ import { import { buildAnnounceIdempotencyKey, resolveQueueAnnounceId } from "./announce-idempotency.js"; import type { AgentInternalEvent } from "./internal-events.js"; import { + getAgentCommandDeliveryFailure, getGatewayAgentResult, hasMessagingToolDeliveryEvidence, hasVisibleAgentPayload, @@ -571,6 +572,11 @@ function hasGatewayAgentMessagingToolDelivery(response: unknown): boolean { return Boolean(result && hasMessagingToolDeliveryEvidence(result)); } +function getGatewayAgentCommandDeliveryFailure(response: unknown): string | undefined { + const result = getGatewayAgentResult(response); + return result ? getAgentCommandDeliveryFailure(result) : undefined; +} + function isGatewayAgentRunPending(response: unknown): boolean { if (!response || typeof response !== "object") { return false; @@ -850,6 +856,16 @@ async function sendSubagentAnnounceDirectly(params: { error: "completion agent did not deliver through the message tool", }; } + const directDeliveryFailure = shouldDeliverAgentFinal + ? getGatewayAgentCommandDeliveryFailure(directAnnounceResponse) + : undefined; + if (directDeliveryFailure) { + return { + delivered: false, + path: "direct", + error: directDeliveryFailure, + }; + } if ( params.expectsCompletionMessage && shouldDeliverAgentFinal && diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index bad292a6de4..90f69760c0b 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -23,6 +23,7 @@ type AgentGatewayResult = { mediaUrl?: string | null; mediaUrls?: string[]; }>; + deliveryStatus?: unknown; meta?: unknown; };