@runtypelabs/sdk 1.10.0 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2310,7 +2310,10 @@ function hasSufficientResearchEvidence(state) {
2310
2310
  return false;
2311
2311
  }
2312
2312
  if (state.isCreationTask) {
2313
- return (state.recentReadPaths?.length || 0) >= 1;
2313
+ const hasReadFiles = (state.recentReadPaths?.length || 0) >= 1;
2314
+ const isDiscoveryKey = (key) => key.startsWith("tree_directory:") || key.startsWith("list_directory:") || key === "server:tree_directory" || key === "server:list_directory";
2315
+ const hasPerformedDiscovery = state.sessions.some((session) => session.actionKeys?.some(isDiscoveryKey)) || (state.recentActionKeys?.some(isDiscoveryKey) ?? false);
2316
+ return hasReadFiles || hasPerformedDiscovery;
2314
2317
  }
2315
2318
  if (!state.bestCandidatePath) return false;
2316
2319
  const normalizedBestCandidatePath = normalizeCandidatePath(state.bestCandidatePath);
@@ -2525,6 +2528,21 @@ var researchPhase = {
2525
2528
  ].join("\n");
2526
2529
  },
2527
2530
  interceptToolCall(toolName, _args, ctx) {
2531
+ if (ctx.state.isCreationTask && !isExternalTask(ctx.state)) {
2532
+ const isWriteLikeTool = toolName === "write_file" || toolName === "edit_file" || toolName === "restore_file_checkpoint";
2533
+ if (isWriteLikeTool) {
2534
+ const normalizedPathArg2 = typeof _args.path === "string" && _args.path.trim() ? ctx.normalizePath(String(_args.path)) : void 0;
2535
+ const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
2536
+ if (normalizedPathArg2 && normalizedPlanPath && normalizedPathArg2 !== normalizedPlanPath) {
2537
+ return [
2538
+ `Blocked by marathon research guard: ${toolName} cannot create product files during the research phase.`,
2539
+ "Complete research first, then the system will advance you to planning.",
2540
+ `You may write the plan to "${normalizedPlanPath}" once research is complete.`
2541
+ ].join(" ");
2542
+ }
2543
+ }
2544
+ return void 0;
2545
+ }
2528
2546
  if (!isExternalTask(ctx.state)) {
2529
2547
  return void 0;
2530
2548
  }
@@ -2627,7 +2645,7 @@ var researchPhase = {
2627
2645
  },
2628
2646
  canAcceptCompletion(state, trace) {
2629
2647
  if (!isExternalTask(state)) {
2630
- return true;
2648
+ return false;
2631
2649
  }
2632
2650
  return Boolean(state.planWritten || trace.planWritten);
2633
2651
  }
@@ -2681,7 +2699,7 @@ var planningPhase = {
2681
2699
  interceptToolCall(toolName, args, ctx) {
2682
2700
  const normalizedPathArg = typeof args.path === "string" && args.path.trim() ? ctx.normalizePath(String(args.path)) : void 0;
2683
2701
  const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
2684
- const isWriteLikeTool = toolName === "write_file" || toolName === "restore_file_checkpoint";
2702
+ const isWriteLikeTool = toolName === "write_file" || toolName === "edit_file" || toolName === "restore_file_checkpoint";
2685
2703
  if (isWriteLikeTool && normalizedPathArg && normalizedPlanPath && normalizedPathArg !== normalizedPlanPath) {
2686
2704
  return [
2687
2705
  `Blocked by marathon planning guard: ${toolName} must target the exact plan path during planning.`,
@@ -2755,9 +2773,11 @@ var executionPhase = {
2755
2773
  "Do not write the plan file first in execution. Make a real repo-file edit before you update the plan with progress.",
2756
2774
  "Do not create scratch or test files to probe the repo or tool behavior.",
2757
2775
  "write_file automatically checkpoints original repo files before overwriting them. If an edit regresses behavior, use restore_file_checkpoint on that file.",
2758
- "Read the target file and edit it with write_file. Update the plan file with progress after completing real edits.",
2776
+ "Use edit_file for targeted changes instead of rewriting the entire file with write_file. edit_file takes old_string and new_string to surgically replace specific code.",
2777
+ "Read the target file and edit it with edit_file (preferred) or write_file. Update the plan file with progress after completing real edits.",
2759
2778
  "Before large edits, read any already discovered supporting source/style files that power the target so you preserve existing behavior.",
2760
- "Prefer minimal diffs over rewrites. If you cannot verify related behavior, stop and record what is still unverified instead of rewriting blindly.",
2779
+ "Prefer edit_file for small changes. Only use write_file when creating new files or when the changes are so extensive that a full rewrite is simpler.",
2780
+ "After writing a file 2+ times, you MUST read it back to verify correctness before writing again.",
2761
2781
  'Use run_check for real verification before TASK_COMPLETE. Good examples: "pnpm lint", "pnpm exec tsc --noEmit", "pnpm test", or a focused vitest/pytest command.',
2762
2782
  "Broad discovery is only allowed if a read of the current target file fails."
2763
2783
  ];
@@ -2769,7 +2789,7 @@ var executionPhase = {
2769
2789
  const normalizedPathArg = typeof args.path === "string" && args.path.trim() ? ctx.normalizePath(String(args.path)) : void 0;
2770
2790
  const normalizedPlanPath = ctx.state.planPath ? ctx.normalizePath(ctx.state.planPath) : void 0;
2771
2791
  const normalizedBestCandidatePath = ctx.state.bestCandidatePath ? ctx.normalizePath(ctx.state.bestCandidatePath) : void 0;
2772
- const isWriteLikeTool = toolName === "write_file" || toolName === "restore_file_checkpoint";
2792
+ const isWriteLikeTool = toolName === "write_file" || toolName === "edit_file" || toolName === "restore_file_checkpoint";
2773
2793
  if (normalizedBestCandidatePath && ctx.isDiscoveryTool(toolName) && !ctx.trace.bestCandidateReadFailed) {
2774
2794
  return [
2775
2795
  `Blocked by marathon execution guard: ${toolName} is disabled during execution.`,
@@ -2831,6 +2851,21 @@ var executionPhase = {
2831
2851
  },
2832
2852
  buildRecoveryMessage(state) {
2833
2853
  const recent = state.sessions.slice(-2);
2854
+ if (recent.length >= 2 && recent.every(
2855
+ (session) => session.hadTextOutput === true && session.wroteFiles !== true
2856
+ )) {
2857
+ const noToolActions = recent.every(
2858
+ (session) => !session.wroteFiles && !session.verificationAttempted
2859
+ );
2860
+ if (noToolActions) {
2861
+ return [
2862
+ "Recovery instruction: You have been rejected from completing multiple times.",
2863
+ "The likely reason is that verification has not passed.",
2864
+ "Your next action must be run_check with a concrete command (e.g., syntax check, lint, or test).",
2865
+ "Do NOT output TASK_COMPLETE again until verification passes."
2866
+ ].join("\n");
2867
+ }
2868
+ }
2834
2869
  if (recent.length >= 2 && recent.every(
2835
2870
  (session) => session.verificationAttempted === true && session.wroteFiles !== true
2836
2871
  )) {
@@ -2879,6 +2914,18 @@ var executionPhase = {
2879
2914
  if (!ctx.trace.executionFileWritten && snapshot.consecutiveDiscoveryPauseCount >= 18) {
2880
2915
  return "execution is looping on discovery instead of editing repo files and ending the turn";
2881
2916
  }
2917
+ const writeKeys = snapshot.recentActionKeys.filter((k) => k.startsWith("write_file:"));
2918
+ if (writeKeys.length >= 4) {
2919
+ const uniqueWriteTargets = new Set(writeKeys.map((k) => k.split(":").slice(1).join(":")));
2920
+ if (uniqueWriteTargets.size === 1) {
2921
+ return `write_file called ${writeKeys.length} times on the same file \u2014 read the file and verify before continuing`;
2922
+ }
2923
+ }
2924
+ for (const [filePath, count] of Object.entries(ctx.trace.writeCountByPath)) {
2925
+ if (count >= 4) {
2926
+ return `same file rewritten ${count} times without verification (${filePath}) \u2014 read the file and verify before continuing`;
2927
+ }
2928
+ }
2882
2929
  return void 0;
2883
2930
  }
2884
2931
  };
@@ -4815,7 +4862,8 @@ var _AgentsEndpoint = class _AgentsEndpoint {
4815
4862
  verificationAttempted: false,
4816
4863
  verificationPassed: false,
4817
4864
  verificationBlocked: false,
4818
- localToolLoopGuardTriggered: false
4865
+ localToolLoopGuardTriggered: false,
4866
+ writeCountByPath: {}
4819
4867
  };
4820
4868
  }
4821
4869
  isDiscoveryLocalTool(toolName) {
@@ -5018,8 +5066,26 @@ var _AgentsEndpoint = class _AgentsEndpoint {
5018
5066
  sessionTrace
5019
5067
  );
5020
5068
  }
5069
+ const phaseIndex = workflow.phases.findIndex((p) => p.name === state.workflowPhase);
5070
+ const executionPhaseIndex = workflow.phases.findIndex((p) => p.name === "execution");
5071
+ if (executionPhaseIndex >= 0 && phaseIndex < executionPhaseIndex) {
5072
+ return false;
5073
+ }
5021
5074
  return true;
5022
5075
  }
5076
+ computeCompletionRejectionReason(state, trace) {
5077
+ const reasons = [];
5078
+ if (!state.planWritten) {
5079
+ reasons.push("Plan file has not been written");
5080
+ }
5081
+ if (state.bestCandidatePath && !state.bestCandidateVerified && !trace.bestCandidateVerified) {
5082
+ reasons.push("Best candidate file has not been verified (read back after writing)");
5083
+ }
5084
+ if (state.verificationRequired && !state.lastVerificationPassed && !trace.verificationPassed) {
5085
+ reasons.push("Verification has not passed \u2014 run a verification command (run_check) before completing");
5086
+ }
5087
+ return reasons.length > 0 ? reasons.join("; ") : "Completion gates not satisfied for the current workflow phase";
5088
+ }
5023
5089
  summarizeUnknownForTrace(value, maxLength = 180) {
5024
5090
  const text = typeof value === "string" ? value : value === void 0 ? "" : JSON.stringify(value);
5025
5091
  return text.replace(/\s+/g, " ").trim().slice(0, maxLength);
@@ -5333,7 +5399,7 @@ var _AgentsEndpoint = class _AgentsEndpoint {
5333
5399
  const pathArg = typeof args.path === "string" && args.path.trim() ? ` path=${String(args.path)}` : "";
5334
5400
  const queryArg = typeof args.query === "string" && args.query.trim() ? ` query="${String(args.query)}"` : "";
5335
5401
  const patternArg = typeof args.pattern === "string" && args.pattern.trim() ? ` pattern="${String(args.pattern)}"` : "";
5336
- const isWriteLikeTool = toolName === "write_file" || toolName === "restore_file_checkpoint";
5402
+ const isWriteLikeTool = toolName === "write_file" || toolName === "edit_file" || toolName === "restore_file_checkpoint";
5337
5403
  const isVerificationTool = toolName === "run_check";
5338
5404
  const currentPhase = workflow.phases.find((p) => p.name === state.workflowPhase);
5339
5405
  if (currentPhase?.interceptToolCall) {
@@ -5388,8 +5454,10 @@ var _AgentsEndpoint = class _AgentsEndpoint {
5388
5454
  );
5389
5455
  throw error;
5390
5456
  }
5391
- if (isWriteLikeTool && normalizedPathArg) {
5457
+ const writeResultIndicatesError = isWriteLikeTool && typeof result === "string" && result.startsWith("Error:");
5458
+ if (isWriteLikeTool && normalizedPathArg && !writeResultIndicatesError) {
5392
5459
  trace.wroteFiles = true;
5460
+ trace.writeCountByPath[normalizedPathArg] = (trace.writeCountByPath[normalizedPathArg] || 0) + 1;
5393
5461
  if (normalizedPlanPath && normalizedPathArg === normalizedPlanPath) {
5394
5462
  trace.planWritten = true;
5395
5463
  } else if (state.workflowPhase === "execution") {
@@ -5697,6 +5765,8 @@ var _AgentsEndpoint = class _AgentsEndpoint {
5697
5765
  const maxCost = options.maxCost;
5698
5766
  const useStream = options.stream ?? true;
5699
5767
  const workflow = options.workflow ?? defaultWorkflow;
5768
+ const maxServerNetworkRetries = 3;
5769
+ let consecutiveServerNetworkErrors = 0;
5700
5770
  const agent = await this.get(id);
5701
5771
  const taskName = typeof options.trackProgress === "string" ? options.trackProgress : options.trackProgress ? `${agent.name} task` : "";
5702
5772
  const resolvedTaskName = taskName || `${agent.name} task`;
@@ -6003,16 +6073,60 @@ var _AgentsEndpoint = class _AgentsEndpoint {
6003
6073
  if (state.sessions.length > 50) {
6004
6074
  state.sessions = state.sessions.slice(-50);
6005
6075
  }
6076
+ if (sessionResult.stopReason !== "error") {
6077
+ consecutiveServerNetworkErrors = 0;
6078
+ }
6006
6079
  const detectedTaskCompletion = this.detectTaskCompletion(sessionResult.result);
6007
6080
  const acceptedTaskCompletion = detectedTaskCompletion && this.canAcceptTaskCompletion(sessionResult.result, state, sessionTrace, workflow);
6081
+ if (detectedTaskCompletion && !acceptedTaskCompletion) {
6082
+ state.lastCompletionRejectionReason = this.computeCompletionRejectionReason(state, sessionTrace);
6083
+ if (state.verificationRequired && !state.lastVerificationPassed && !sessionTrace.verificationPassed && !sessionTrace.verificationAttempted) {
6084
+ state.consecutiveBlockedVerificationSessions = (state.consecutiveBlockedVerificationSessions || 0) + 1;
6085
+ if ((state.consecutiveBlockedVerificationSessions || 0) >= 2) {
6086
+ state.verificationRequired = false;
6087
+ state.lastVerificationPassed = true;
6088
+ if (!state.planWritten) {
6089
+ state.planWritten = true;
6090
+ }
6091
+ if (!state.bestCandidateVerified) {
6092
+ state.bestCandidateVerified = true;
6093
+ }
6094
+ }
6095
+ }
6096
+ } else {
6097
+ state.lastCompletionRejectionReason = void 0;
6098
+ }
6099
+ const sessionHadActions = sessionTrace.wroteFiles || sessionTrace.readFiles || sessionTrace.discoveryPerformed || sessionTrace.verificationAttempted;
6100
+ if (sessionHadActions) {
6101
+ state.consecutiveEmptySessions = 0;
6102
+ } else {
6103
+ state.consecutiveEmptySessions = (state.consecutiveEmptySessions || 0) + 1;
6104
+ }
6008
6105
  if (sessionResult.stopReason === "complete" && !detectedTaskCompletion) {
6009
6106
  state.status = "complete";
6010
6107
  } else if (sessionResult.stopReason === "error") {
6011
- state.status = "error";
6108
+ if (_AgentsEndpoint.isRetryableSessionError(sessionResult.error) && consecutiveServerNetworkErrors < maxServerNetworkRetries) {
6109
+ consecutiveServerNetworkErrors++;
6110
+ const delayMs = Math.min(
6111
+ 5e3 * Math.pow(2, consecutiveServerNetworkErrors - 1),
6112
+ 3e4
6113
+ );
6114
+ const delaySec = Math.round(delayMs / 1e3);
6115
+ await this.emitContextNotice(options.onContextNotice, {
6116
+ kind: "server_network_retry",
6117
+ sessionIndex: session,
6118
+ message: `Server network error: ${sessionResult.error}. Retrying in ${delaySec}s (attempt ${consecutiveServerNetworkErrors}/${maxServerNetworkRetries})...`
6119
+ });
6120
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
6121
+ } else {
6122
+ state.status = "error";
6123
+ }
6012
6124
  } else if (sessionResult.stopReason === "max_cost") {
6013
6125
  state.status = "budget_exceeded";
6014
6126
  } else if (acceptedTaskCompletion) {
6015
6127
  state.status = "complete";
6128
+ } else if ((state.consecutiveEmptySessions || 0) >= 3) {
6129
+ state.status = "stalled";
6016
6130
  } else if (maxCost && state.totalCost >= maxCost) {
6017
6131
  state.status = "budget_exceeded";
6018
6132
  } else if (session + 1 >= maxSessions) {
@@ -6041,6 +6155,15 @@ var _AgentsEndpoint = class _AgentsEndpoint {
6041
6155
  recordId
6042
6156
  };
6043
6157
  }
6158
+ /** Returns true if a server-side session error message indicates a transient
6159
+ * network failure that is safe to retry. */
6160
+ static isRetryableSessionError(errorMessage) {
6161
+ if (!errorMessage) return false;
6162
+ const lower = errorMessage.toLowerCase();
6163
+ return _AgentsEndpoint.RETRYABLE_SESSION_ERROR_PATTERNS.some(
6164
+ (pattern) => lower.includes(pattern)
6165
+ );
6166
+ }
6044
6167
  /**
6045
6168
  * Client-side fallback for detecting task completion in agent output.
6046
6169
  * Mirrors the API's detectAutoComplete() for non-loop agents that return 'end_turn'.
@@ -6645,6 +6768,7 @@ Do NOT redo any of the above work.`
6645
6768
  ).join("\n");
6646
6769
  if (state.messages && state.messages.length > 0) {
6647
6770
  const recoveryMessage2 = this.buildStuckTurnRecoveryMessage(state, wf);
6771
+ const rejectionNotice = state.lastCompletionRejectionReason ? `TASK_COMPLETE was rejected because: ${state.lastCompletionRejectionReason}. Address this before signaling completion again.` : void 0;
6648
6772
  const continuationContent = [
6649
6773
  "Continue the task.",
6650
6774
  phaseBlock,
@@ -6656,6 +6780,7 @@ Do NOT redo any of the above work.`
6656
6780
  `Previous sessions:`,
6657
6781
  progressSummary,
6658
6782
  "",
6783
+ ...rejectionNotice ? [rejectionNotice, ""] : [],
6659
6784
  ...recoveryMessage2 ? [recoveryMessage2, ""] : [],
6660
6785
  "Do not redo previous work. If the task is already complete, respond with TASK_COMPLETE."
6661
6786
  ].join("\n");
@@ -6716,6 +6841,7 @@ Do NOT redo any of the above work.`
6716
6841
  };
6717
6842
  }
6718
6843
  const recoveryMessage = this.buildStuckTurnRecoveryMessage(state, wf);
6844
+ const fallbackRejectionNotice = state.lastCompletionRejectionReason ? `TASK_COMPLETE was rejected because: ${state.lastCompletionRejectionReason}. Address this before signaling completion again.` : void 0;
6719
6845
  const content = [
6720
6846
  originalMessage,
6721
6847
  phaseBlock,
@@ -6727,6 +6853,7 @@ Do NOT redo any of the above work.`
6727
6853
  `Previous sessions:`,
6728
6854
  progressSummary,
6729
6855
  "",
6856
+ ...fallbackRejectionNotice ? [fallbackRejectionNotice, ""] : [],
6730
6857
  ...recoveryMessage ? [recoveryMessage, ""] : [],
6731
6858
  `Last output (do NOT repeat this \u2014 build on it):`,
6732
6859
  state.lastOutput.slice(0, 1e3),
@@ -6788,6 +6915,24 @@ Do NOT redo any of the above work.`
6788
6915
  };
6789
6916
  _AgentsEndpoint.AUTO_COMPACT_SUMMARY_PREFIX = "You are continuing a long-running task. Here is a compact summary of prior work:";
6790
6917
  _AgentsEndpoint.FORCED_COMPACT_SUMMARY_PREFIX = "You are continuing a previously completed task. Here is a summary of prior work:";
6918
+ /** Error message patterns from server-side sessions that indicate a transient network failure
6919
+ * (e.g. AI provider connection dropped). These are retried automatically. */
6920
+ _AgentsEndpoint.RETRYABLE_SESSION_ERROR_PATTERNS = [
6921
+ "network connection lost",
6922
+ "network error",
6923
+ "fetch failed",
6924
+ "connection reset",
6925
+ "connection refused",
6926
+ "connection closed",
6927
+ "socket hang up",
6928
+ "econnreset",
6929
+ "econnrefused",
6930
+ "econnaborted",
6931
+ "etimedout",
6932
+ "enetunreach",
6933
+ "enotfound",
6934
+ "request timeout"
6935
+ ];
6791
6936
  /** Stop phrases that indicate the agent considers its task complete. */
6792
6937
  _AgentsEndpoint.STOP_PHRASES = [
6793
6938
  "DONE:",