diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 5f979c9be63..effbd5cc105 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -1503,6 +1503,21 @@ export async function runEmbeddedPiAgent( "Please try again, or increase `agents.defaults.llm.idleTimeoutSeconds` in your config (set to 0 to disable)." : "Request timed out before a response was generated. " + "Please try again, or increase `agents.defaults.timeoutSeconds` in your config."; + const replayInvalid = resolveReplayInvalidFlag({ + attempt, + incompleteTurnText: null, + }); + const livenessState = resolveRunLivenessState({ + payloadCount: payloads.length, + aborted, + timedOut, + attempt, + incompleteTurnText: null, + }); + attempt.setTerminalLifecycleMeta?.({ + replayInvalid, + livenessState, + }); return { payloads: [ { @@ -1516,17 +1531,8 @@ export async function runEmbeddedPiAgent( aborted, systemPromptReport: attempt.systemPromptReport, finalAssistantVisibleText, - replayInvalid: resolveReplayInvalidFlag({ - attempt, - incompleteTurnText: null, - }), - livenessState: resolveRunLivenessState({ - payloadCount: payloads.length, - aborted, - timedOut, - attempt, - incompleteTurnText: null, - }), + replayInvalid, + livenessState, }, didSendViaMessagingTool: attempt.didSendViaMessagingTool, didSendDeterministicApprovalPrompt: attempt.didSendDeterministicApprovalPrompt, @@ -1619,6 +1625,21 @@ export async function runEmbeddedPiAgent( }; } if (incompleteTurnText) { + const replayInvalid = resolveReplayInvalidFlag({ + attempt, + incompleteTurnText, + }); + const livenessState = resolveRunLivenessState({ + payloadCount: payloads.length, + aborted, + timedOut, + attempt, + incompleteTurnText, + }); + attempt.setTerminalLifecycleMeta?.({ + replayInvalid, + livenessState, + }); const incompleteStopReason = attempt.lastAssistant?.stopReason; log.warn( `incomplete turn detected: runId=${params.runId} sessionId=${params.sessionId} ` + @@ -1647,17 +1668,8 @@ export async function runEmbeddedPiAgent( aborted, systemPromptReport: attempt.systemPromptReport, finalAssistantVisibleText, - replayInvalid: resolveReplayInvalidFlag({ - attempt, - incompleteTurnText, - }), - livenessState: resolveRunLivenessState({ - payloadCount: payloads.length, - aborted, - timedOut, - attempt, - incompleteTurnText, - }), + replayInvalid, + livenessState, }, didSendViaMessagingTool: attempt.didSendViaMessagingTool, didSendDeterministicApprovalPrompt: attempt.didSendDeterministicApprovalPrompt, @@ -1684,6 +1696,21 @@ export async function runEmbeddedPiAgent( agentDir: params.agentDir, }); } + const replayInvalid = resolveReplayInvalidFlag({ + attempt, + incompleteTurnText: null, + }); + const livenessState = resolveRunLivenessState({ + payloadCount: payloads.length, + aborted, + timedOut, + attempt, + incompleteTurnText: null, + }); + attempt.setTerminalLifecycleMeta?.({ + replayInvalid, + livenessState, + }); return { payloads: payloadsWithToolMedia?.length ? payloadsWithToolMedia : undefined, meta: { @@ -1692,17 +1719,8 @@ export async function runEmbeddedPiAgent( aborted, systemPromptReport: attempt.systemPromptReport, finalAssistantVisibleText, - replayInvalid: resolveReplayInvalidFlag({ - attempt, - incompleteTurnText: null, - }), - livenessState: resolveRunLivenessState({ - payloadCount: payloads.length, - aborted, - timedOut, - attempt, - incompleteTurnText: null, - }), + replayInvalid, + livenessState, // Handle client tool calls (OpenResponses hosted tools) // Propagate the LLM stop reason so callers (lifecycle events, // ACP bridge) can distinguish end_turn from max_tokens. diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index aee558865dd..1cfcde9bf48 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -1447,6 +1447,7 @@ export async function runEmbeddedAttempt( getSuccessfulCronAdds, didSendViaMessagingTool, getLastToolError, + setTerminalLifecycleMeta, getUsageTotals, getCompactionCount, } = subscription; @@ -2277,6 +2278,7 @@ export async function runEmbeddedAttempt( successfulCronAdds: getSuccessfulCronAdds(), }), itemLifecycle: getItemLifecycle(), + setTerminalLifecycleMeta, aborted, timedOut, idleTimedOut, diff --git a/src/agents/pi-embedded-runner/run/types.ts b/src/agents/pi-embedded-runner/run/types.ts index 88b698a7777..95da22cc659 100644 --- a/src/agents/pi-embedded-runner/run/types.ts +++ b/src/agents/pi-embedded-runner/run/types.ts @@ -8,6 +8,7 @@ import type { PluginHookBeforeAgentStartResult } from "../../../plugins/types.js import type { MessagingToolSend } from "../../pi-embedded-messaging.js"; import type { ToolErrorSummary } from "../../tool-error-summary.js"; import type { NormalizedUsage } from "../../usage.js"; +import type { EmbeddedRunLivenessState } from "../types.js"; import type { RunEmbeddedPiAgentParams } from "./params.js"; import type { PreemptiveCompactionRoute } from "./preemptive-compaction.js"; @@ -98,4 +99,8 @@ export type EmbeddedRunAttemptResult = { completedCount: number; activeCount: number; }; + setTerminalLifecycleMeta?: (meta: { + replayInvalid?: boolean; + livenessState?: EmbeddedRunLivenessState; + }) => void; }; diff --git a/src/agents/pi-embedded-subscribe.handlers.compaction.ts b/src/agents/pi-embedded-subscribe.handlers.compaction.ts index 3875adf6c1e..6b94b9af326 100644 --- a/src/agents/pi-embedded-subscribe.handlers.compaction.ts +++ b/src/agents/pi-embedded-subscribe.handlers.compaction.ts @@ -68,7 +68,9 @@ export function handleAutoCompactionEnd( ctx.resetForCompactionRetry(); ctx.log.debug(`embedded run compaction retry: runId=${ctx.params.runId}`); } else { - ctx.state.livenessState = "working"; + if (!wasAborted) { + ctx.state.livenessState = "working"; + } ctx.maybeResolveCompactionWait(); clearStaleAssistantUsageOnSessionMessages(ctx); } diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index 92c0f4cf5eb..de0284ada95 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -15,6 +15,7 @@ import { normalizeTextForComparison, } from "./pi-embedded-helpers.js"; import type { BlockReplyPayload } from "./pi-embedded-payloads.js"; +import type { EmbeddedRunLivenessState } from "./pi-embedded-runner/types.js"; import { createEmbeddedPiSessionEventHandler } from "./pi-embedded-subscribe.handlers.js"; import { consumePendingToolMediaIntoReply } from "./pi-embedded-subscribe.handlers.messages.js"; import type { @@ -776,6 +777,17 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar assistantTexts, toolMetas, unsubscribe, + setTerminalLifecycleMeta: (meta: { + replayInvalid?: boolean; + livenessState?: EmbeddedRunLivenessState; + }) => { + if (typeof meta.replayInvalid === "boolean") { + state.replayInvalid = meta.replayInvalid; + } + if (meta.livenessState) { + state.livenessState = meta.livenessState; + } + }, isCompacting: () => state.compactionInFlight || state.pendingCompactionRetry > 0, isCompactionInFlight: () => state.compactionInFlight, getMessagingToolSentTexts: () => messagingToolSentTexts.slice(), diff --git a/src/plugin-sdk/provider-tools.ts b/src/plugin-sdk/provider-tools.ts index 4c8a62a0efd..53bed344b5c 100644 --- a/src/plugin-sdk/provider-tools.ts +++ b/src/plugin-sdk/provider-tools.ts @@ -166,7 +166,13 @@ export function normalizeOpenAIToolSchemas( return ctx.tools; } return ctx.tools.map((tool) => { - if (!tool.parameters || typeof tool.parameters !== "object") { + if (tool.parameters == null) { + return { + ...tool, + parameters: normalizeOpenAIStrictCompatSchema({}), + }; + } + if (typeof tool.parameters !== "object") { return tool; } return { @@ -292,13 +298,23 @@ function normalizeOpenAIStrictCompatSchemaRecursive(schema: unknown): unknown { return changed ? normalized : schema; } -export function findOpenAIStrictSchemaViolations(schema: unknown, path: string): string[] { +export function findOpenAIStrictSchemaViolations( + schema: unknown, + path: string, + options?: { requireObjectRoot?: boolean }, +): string[] { if (Array.isArray(schema)) { + if (options?.requireObjectRoot) { + return [`${path}.type`]; + } return schema.flatMap((item, index) => findOpenAIStrictSchemaViolations(item, `${path}[${index}]`), ); } if (!schema || typeof schema !== "object") { + if (options?.requireObjectRoot) { + return [`${path}.type`]; + } return []; } @@ -363,8 +379,9 @@ export function inspectOpenAIToolSchemas( } return ctx.tools.flatMap((tool, toolIndex) => { const violations = findOpenAIStrictSchemaViolations( - normalizeOpenAIStrictCompatSchema(tool.parameters), + normalizeOpenAIStrictCompatSchema(tool.parameters ?? {}), `${tool.name}.parameters`, + { requireObjectRoot: true }, ); if (violations.length === 0) { return []; diff --git a/src/plugins/contracts/provider-family-plugin-tests.test.ts b/src/plugins/contracts/provider-family-plugin-tests.test.ts index 5d1eaca75cf..aceda2737f6 100644 --- a/src/plugins/contracts/provider-family-plugin-tests.test.ts +++ b/src/plugins/contracts/provider-family-plugin-tests.test.ts @@ -63,6 +63,7 @@ const EXPECTED_SHARED_FAMILY_CONTRACTS: Record