fix(web): show live agent work progress and fail truncated rectification answers
Rectification dropped tool.activity started events and treated length finishes as completed. Share generation settings with consultation, keep the activity line through streaming, and name multi-domain chart calculation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,8 @@ import {
|
||||
type V9CaseDossier,
|
||||
} from "./tool-service";
|
||||
import { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION } from "./case-status";
|
||||
import { agentGenerationSettings } from "../../agent-generation-settings.ts";
|
||||
import { toAgentModelFinishReason } from "../../agent-observability.ts";
|
||||
import {
|
||||
resolveExactSkillPackage,
|
||||
type ResolvedSkillPackageIdentity,
|
||||
@@ -58,6 +60,7 @@ export type V9AgentRunOptions = Readonly<{
|
||||
emit(event: PublicStreamEvent): Promise<void> | void;
|
||||
signal?: AbortSignal;
|
||||
timeContext?: string;
|
||||
generationModel?: unknown;
|
||||
}>;
|
||||
|
||||
export type V9AgentRunResult = Readonly<{
|
||||
@@ -100,6 +103,14 @@ const RETRYABLE_ERROR_CODES = new Set([
|
||||
"focus_persistence_failed",
|
||||
]);
|
||||
|
||||
function streamFinishReason(chunk: {
|
||||
type: string;
|
||||
payload?: { stepResult?: { reason?: unknown }; reason?: unknown };
|
||||
}): ReturnType<typeof toAgentModelFinishReason> | null {
|
||||
if (chunk.type !== "finish") return null;
|
||||
return toAgentModelFinishReason(chunk.payload?.stepResult?.reason ?? chunk.payload?.reason);
|
||||
}
|
||||
|
||||
function first(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value[0] ?? null;
|
||||
if (value && typeof value === "object" && "value" in value) {
|
||||
@@ -129,6 +140,7 @@ function safeErrorCode(error: unknown): string {
|
||||
"case_not_loaded",
|
||||
"repeated_tool_call",
|
||||
"focus_persistence_failed",
|
||||
"answer_truncated",
|
||||
]) {
|
||||
if (message.includes(code)) return code;
|
||||
}
|
||||
@@ -489,12 +501,15 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
await publish({ type: "skill.bound" });
|
||||
emittedKeys.add("event:skill.bound::");
|
||||
|
||||
const generation = agentGenerationSettings(options.generationModel);
|
||||
const result = await (agent as unknown as {
|
||||
stream(
|
||||
messages: unknown[],
|
||||
streamOptions: {
|
||||
maxSteps: number;
|
||||
abortSignal: AbortSignal;
|
||||
modelSettings?: { maxOutputTokens?: number };
|
||||
providerOptions?: Record<string, { thinking: { type: "disabled" } }>;
|
||||
prepareStep: (input: { stepNumber: number }) => {
|
||||
activeTools: string[];
|
||||
toolChoice: "auto";
|
||||
@@ -503,7 +518,14 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
): Promise<{
|
||||
fullStream: AsyncIterable<{
|
||||
type: string;
|
||||
payload?: { toolName?: unknown; text?: unknown; args?: unknown; error?: unknown };
|
||||
payload?: {
|
||||
toolName?: unknown;
|
||||
text?: unknown;
|
||||
args?: unknown;
|
||||
error?: unknown;
|
||||
stepResult?: { reason?: unknown };
|
||||
reason?: unknown;
|
||||
};
|
||||
object?: unknown;
|
||||
}>;
|
||||
totalUsage?: Promise<{ inputTokens?: number; outputTokens?: number }>;
|
||||
@@ -511,6 +533,7 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
}).stream(messages, {
|
||||
maxSteps,
|
||||
abortSignal: abortController.signal,
|
||||
...generation,
|
||||
// Thinking-mode providers reject named/required tool_choice. Restrict
|
||||
// the first step to read-case and keep tool_choice auto; the runner
|
||||
// still refuses any other public tool before case.loaded.
|
||||
@@ -522,6 +545,8 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
: undefined,
|
||||
});
|
||||
|
||||
let finishReason: ReturnType<typeof toAgentModelFinishReason> | null = null;
|
||||
|
||||
for await (const chunk of result.fullStream) {
|
||||
const rawToolName = typeof chunk.payload?.toolName === "string" ? chunk.payload.toolName : "";
|
||||
if (chunk.type === "tool-call") {
|
||||
@@ -592,7 +617,10 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
}
|
||||
for (const toolName of streamToolNames(chunk as never)) toolsUsed.add(toolName);
|
||||
if (chunk.type === "error" || chunk.type === "abort") streamFailed = true;
|
||||
if (chunk.type === "finish") finished = true;
|
||||
if (chunk.type === "finish") {
|
||||
finished = true;
|
||||
finishReason = streamFinishReason(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skillBound) return failedAttempt(attemptId, "skill_not_loaded");
|
||||
@@ -600,6 +628,22 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
if (streamFailed || abortController.signal.aborted) return failedAttempt(attemptId, "stream_aborted");
|
||||
if (!finished) return failedAttempt(attemptId, "stream_unfinished");
|
||||
if (!answerText.trim()) return failedAttempt(attemptId, "empty_stream");
|
||||
if (finishReason === "length") {
|
||||
return {
|
||||
ok: false,
|
||||
status: "failed",
|
||||
errorCode: "answer_truncated",
|
||||
usage: { inputTokens: 0, outputTokens: 0 },
|
||||
answerText,
|
||||
answerDeltas,
|
||||
phases,
|
||||
toolsUsed: [...toolsUsed],
|
||||
events,
|
||||
skillBound,
|
||||
caseLoaded,
|
||||
attemptId,
|
||||
};
|
||||
}
|
||||
if (toolTerminalStatus.get("rectification-set-focus") === "failed") {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user