Ctrl K
ComunidadGuías…

utils/run.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { AgentResult } from "../agents/shared.ts";
import type { MainResult } from "../main.ts";
import { reportProgress } from "../mcp/comment.ts";
import type { ToolContext } from "../mcp/server.ts";
import { log } from "./cli.ts";
import { reportErrorToComment } from "./errorReport.ts";
import { renderFailure } from "./failureBody.ts";

export interface HandleAgentResultParams {
  result: AgentResult;
  toolContext: ToolContext;
  silent: boolean | undefined;
}

function getErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

export async function handleAgentResult(ctx: HandleAgentResultParams): Promise<MainResult> {
  if (!ctx.result.success) {
    // rendering + posting for the `!success` branch lives in
    // `finalizeSuccessRun` (called immediately before this function) so the
    // BYOK billing-exhausted, hang, and api-key bodies land on a single
    // surface — both for runs with a pre-existing progress comment AND for
    // silent triggers via `createIfMissing`. see #835.
    return {
      success: false,
      error: ctx.result.error || "Agent execution failed",
      output: ctx.result.output!,
    };
  }

  // IncrementalReview's non-substantive path exits cleanly without
  // submitting any review, so no MCP write tool flips wasUpdated and the
  // strict completion check below would otherwise fail the run. The
  // isReviewMode skip is load-bearing for that path: the agent's exit
  // code is the completion signal, not a progress-comment write.
  // (Review mode that submits a real review now flips wasUpdated via
  // create_pull_request_review, so the skip is redundant for the
  // substantive-review path but kept for symmetry with IncrementalReview.)
  // See plans/review_progress_comment_cleanup_b0120f6c.plan.md.
  const toolState = ctx.toolContext.toolState;
  const mode = toolState.selectedMode;
  const isReviewMode = mode === "Review" || mode === "IncrementalReview";
  // nothing is seeded under `progressComments: disabled`, and seeding can also fail
  // outright, so `hadProgressComment` is false on runs that still have a comment target —
  // salvage still applies, and reportProgress will create the comment for the answer.
  const payload = ctx.toolContext.payload;
  const hasCommentTarget = toolState.hadProgressComment || payload.event.issue_number !== undefined;
  if (!isReviewMode && !toolState.wasUpdated && hasCommentTarget && !ctx.silent) {
    // the agent exited successfully but never landed a GitHub write — either it
    // answered the mention in raw assistant text (which is never posted, only
    // logged) or a write tool failed (e.g. report_progress hit a 401). salvage
    // by delivering the content we have to the progress comment so the user
    // actually gets the answer instead of a failed run. (finalizeSuccessRun
    // preserves the progress comment whenever wasUpdated is false, so the write
    // lands here.)
    //
    // stop the todo tracker first: on this path the agent used todowrite but
    // never called report_progress, so a pending debounced render could still
    // be queued — draining it keeps it from clobbering the salvaged answer and
    // from holding the event loop open. mirrors ReportProgressTool.
    const tracker = toolState.todoTracker;
    if (tracker) {
      tracker.cancel();
      await tracker.settled();
    }
    // `lastProgressBody` is the body a failed report_progress already assembled
    // (task-list collapsible included), so use it verbatim; otherwise fall back
    // to the agent's final assistant text and append the collapsible the way
    // report_progress would.
    let salvage = toolState.lastProgressBody?.trim();
    if (!salvage) {
      const output = ctx.result.output?.trim();
      const collapsible = tracker?.renderCollapsible({ completeInProgress: true });
      salvage = output && collapsible ? `${output}\n\n${collapsible}` : output;
    }
    if (salvage) {
      try {
        await reportProgress(ctx.toolContext, { body: salvage });
        log.success("Task complete.");
        return { success: true, output: ctx.result.output || "" };
      } catch (writeError) {
        // the write itself is failing (auth/permissions) — surface THAT, not
        // the generic "no progress" message, so the real cause isn't masked.
        const error = `failed to deliver agent result: ${getErrorMessage(writeError)}`;
        await reportErrorToComment({
          toolState,
          error: renderFailure({
            headline: "Pullfrog couldn't post its answer to GitHub.",
            detail: "GitHub refused the write; its error is below. Re-trigger to try again.",
            error: getErrorMessage(writeError),
          }),
        }).catch(() => {});
        return { success: false, error, output: ctx.result.output || "" };
      }
    }

    const error = ctx.result.error || "agent completed without reporting progress";
    try {
      await reportErrorToComment({
        toolState,
        error: renderFailure({
          headline: "The agent finished without posting anything.",
          detail: "Re-trigger to try again.",
        }),
      });
    } catch {}
    return { success: false, error, output: ctx.result.output || "" };
  }

  log.success("Task complete.");

  return {
    success: true,
    output: ctx.result.output || "",
  };
}