@standardagents/builder 0.25.4 → 0.25.5

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/runtime.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as ThreadEnv, A as Agent, h as ThreadMetadata, i as ToolResult, M as Message, l as FileRecord, F as FlowState, n as FileStats, G as GrepResult, m as AttachmentRef, r as MessageContent, E as Env, g as ThreadInstance } from './index-BsS0cHzL.js';
2
- export { f as BuilderThreadEndpointHandler, B as Controller, a as ControllerContext, j as FlowResult, p as ImageContentPart, I as ImageMetadata, L as LLMResponse, q as MultimodalContent, R as RequestContext, S as StorageBackend, k as TelemetryEvent, o as TextContentPart, b as ThreadEndpointContext, T as ToolCall, c as createThreadEndpointHandler, d as defineController } from './index-BsS0cHzL.js';
1
+ import { e as ThreadEnv, A as Agent, h as ThreadMetadata, i as ToolResult, M as Message, l as FileRecord, F as FlowState, n as FileStats, G as GrepResult, m as AttachmentRef, r as MessageContent, E as Env, g as ThreadInstance } from './index-CX9Ti98s.js';
2
+ export { f as BuilderThreadEndpointHandler, B as Controller, a as ControllerContext, j as FlowResult, p as ImageContentPart, I as ImageMetadata, L as LLMResponse, q as MultimodalContent, R as RequestContext, S as StorageBackend, k as TelemetryEvent, o as TextContentPart, b as ThreadEndpointContext, T as ToolCall, c as createThreadEndpointHandler, d as defineController } from './index-CX9Ti98s.js';
3
3
  import { CodeExecutionOptions, CodeExecutionResult, SubagentRegistryEntry, InjectMessageInput, QueueMessageInput, ClientToolRequest, ClientToolResponse, SkillMetadata, SkillFileInput, ModelDefinition as ModelDefinition$1, ToolArgs, PromptTextPart, PromptEnvPart, VariableDefinition, SubpromptConfig as SubpromptConfig$1, PromptToolConfig as PromptToolConfig$1, SubagentToolConfig as SubagentToolConfig$1, ReasoningConfig, SideConfig as SideConfig$1, LLMProviderInterface, ContentPart, TextPart, ImagePart, FilePart, NamespaceContext, DefinitionLoader } from '@standardagents/spec';
4
4
  export { AgentType, DefinitionLoader, GlobalNamespaceContext, HookSignatures, ImageContent, ModelCapabilities, ModelProvider, NamespaceContext, PackageSignature, PackedExports, PackedMeta, PackedMetadata, PackedNamespaceContext, PromptInput, PromptTextPart, ProviderAssistantMessage, ProviderError, ProviderErrorCode, ProviderFactory, ProviderFactoryConfig, ProviderFinishReason, ProviderGeneratedImage, ProviderMessage, ProviderMessageContent, ModelCapabilities as ProviderModelCapabilities, ProviderReasoningDetail, ProviderRequest, ProviderResponse, ProviderStreamChunk, ProviderSystemMessage, ProviderTool, ProviderToolCallPart, ProviderToolMessage, ProviderToolResultContent, ProviderUsage, ProviderUserMessage, ReasoningConfig, StructuredPrompt, TextContent, Tool, ToolArgs, ToolArgsNode, ToolArgsRawShape, ToolContent, belongsToPackage, defineAgent, defineHook, defineModel, definePrompt, defineTool, isPacked, isVisibleInNamespace, mapReasoningLevel } from '@standardagents/spec';
5
5
  import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
@@ -184,6 +184,11 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
184
184
  private alarmQueueRearmed;
185
185
  private currentAbortController;
186
186
  private isExecuting;
187
+ private lastProgressAt;
188
+ private watchdogWedgeStrikes;
189
+ private readonly watchdogIntervalMs;
190
+ private readonly watchdogResumeWindowMs;
191
+ private readonly watchdogMaxResumesPerWindow;
187
192
  private codeExecutionBridges;
188
193
  constructor(ctx: DurableObjectState, env: Env);
189
194
  /**
@@ -1080,6 +1085,21 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
1080
1085
  * the alarm chain. All errors are caught and logged.
1081
1086
  */
1082
1087
  alarm(): Promise<void>;
1088
+ /** Heartbeat: called by LLMRequest on each attempt and streamed chunk. */
1089
+ noteExecutionProgress(): void;
1090
+ /** Arm the standing watchdog for an execution start. One pending row max. */
1091
+ private armExecutionWatchdog;
1092
+ /** Disarm on clean execution end. Completed check rows are noise, not history. */
1093
+ private disarmExecutionWatchdog;
1094
+ /**
1095
+ * The standing check. Evaluates the thread's state AT FIRE TIME — it knows
1096
+ * nothing about whichever request was in flight when it was armed.
1097
+ */
1098
+ private watchdogCheck;
1099
+ /** Mark logs the dead executor abandoned, and tell connected clients. */
1100
+ private finalizeLostExecutionLogs;
1101
+ /** At most N auto-resumes per rolling window, tracked durably. */
1102
+ private consumeWatchdogResumeBudget;
1083
1103
  private normalizePlainRecord;
1084
1104
  private buildExecutionArguments;
1085
1105
  /**
package/dist/runtime.js CHANGED
@@ -1057,6 +1057,19 @@ function resolveStreamTimeoutMs(env, key, fallback) {
1057
1057
  const parsed = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseInt(raw, 10) : NaN;
1058
1058
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1059
1059
  }
1060
+ function resolveWatchdogStaleCeilingMs(env) {
1061
+ const firstChunk = resolveStreamTimeoutMs(
1062
+ env,
1063
+ "STANDARD_AGENTS_STREAM_FIRST_CHUNK_TIMEOUT_MS",
1064
+ STREAM_FIRST_CHUNK_TIMEOUT_MS
1065
+ );
1066
+ const stall = resolveStreamTimeoutMs(
1067
+ env,
1068
+ "STANDARD_AGENTS_STREAM_STALL_TIMEOUT_MS",
1069
+ STREAM_STALL_TIMEOUT_MS
1070
+ );
1071
+ return Math.max(firstChunk, stall) + 6e4;
1072
+ }
1060
1073
  function modelSupportsImageInput(modelDef) {
1061
1074
  const supportsImages = modelDef.capabilities?.supportsImages;
1062
1075
  if (typeof supportsImages === "boolean") {
@@ -1569,6 +1582,7 @@ var init_LLMRequest = __esm({
1569
1582
  attempts++;
1570
1583
  try {
1571
1584
  logId = await this.logRequest(state, actualModelId, context, modelDef, previousLogId);
1585
+ state.thread.instance.noteExecutionProgress?.();
1572
1586
  state.emitTelemetry?.({
1573
1587
  type: "llm_request",
1574
1588
  model: actualModelId,
@@ -1835,6 +1849,7 @@ var init_LLMRequest = __esm({
1835
1849
  }
1836
1850
  if (next.done) break;
1837
1851
  const chunk = next.value;
1852
+ state.thread.instance.noteExecutionProgress?.();
1838
1853
  if (firstChunkAt === 0) {
1839
1854
  firstChunkAt = Date.now();
1840
1855
  emitLogPatch({ time_to_first_token_ms: firstChunkAt - requestSentAt });
@@ -15287,6 +15302,7 @@ var AlarmQueue = class {
15287
15302
 
15288
15303
  // src/durable-objects/DurableThread.ts
15289
15304
  init_StreamManager();
15305
+ init_LLMRequest();
15290
15306
 
15291
15307
  // src/packing/resolver.ts
15292
15308
  var NamespaceResolutionError = class extends Error {
@@ -15774,6 +15790,18 @@ var DurableThread = class _DurableThread extends DurableObject {
15774
15790
  alarmQueueRearmed = false;
15775
15791
  currentAbortController = null;
15776
15792
  isExecuting = false;
15793
+ // ── Execution watchdog state (see watchdogCheck) ─────────────────────────
15794
+ // In-memory heartbeat: bumped by noteExecutionProgress() on every streamed
15795
+ // chunk and request attempt. Deliberately NOT persisted — after a DO restart
15796
+ // the isExecuting flag alone proves the executor is gone, and while the DO
15797
+ // is alive the in-memory value is authoritative. Zero hot-path storage cost.
15798
+ lastProgressAt = 0;
15799
+ // Consecutive watchdog firings that found a live-but-silent executor. Strike
15800
+ // one aborts the in-flight request; strike two forces takeover.
15801
+ watchdogWedgeStrikes = 0;
15802
+ watchdogIntervalMs = 3e4;
15803
+ watchdogResumeWindowMs = 15 * 60 * 1e3;
15804
+ watchdogMaxResumesPerWindow = 2;
15777
15805
  codeExecutionBridges = /* @__PURE__ */ new Map();
15778
15806
  constructor(ctx, env) {
15779
15807
  super(ctx, env);
@@ -15809,6 +15837,9 @@ var DurableThread = class _DurableThread extends DurableObject {
15809
15837
  case "continueExecution":
15810
15838
  await this.doContinueExecution(args.threadId, args.side);
15811
15839
  break;
15840
+ case "watchdogCheck":
15841
+ await this.watchdogCheck(args.threadId);
15842
+ break;
15812
15843
  default:
15813
15844
  throw new Error(`Unknown method: ${method}`);
15814
15845
  }
@@ -19561,6 +19592,190 @@ ${resultContent}${attachmentSummary}`;
19561
19592
  console.error("[DurableThread] Alarm handler failed:", error);
19562
19593
  }
19563
19594
  }
19595
+ // ══════════════════════════════════════════════════════════════════════════
19596
+ // Execution watchdog
19597
+ //
19598
+ // The in-flight stream guards (LLMRequest's first-chunk/stall timeouts) live
19599
+ // inside the execution's promise chain — they die with the DO. A deploy or
19600
+ // eviction mid-request therefore used to leave a durable log row stuck at
19601
+ // is_complete = 0 forever: the thread looked "working", queued messages
19602
+ // never played, and nothing woke it (finalizeOrphanedLogs only runs at the
19603
+ // START of the next execution). Persisted DO alarms are the one timer that
19604
+ // survives restarts and fires without inbound traffic, so a standing
19605
+ // per-thread watchdog alarm re-checks the thread's CURRENT state every 30s
19606
+ // while work is active.
19607
+ //
19608
+ // Design invariants (do not weaken):
19609
+ // - Progress, never duration: only silence ages. A response can stream for
19610
+ // ten minutes; every chunk bumps the in-memory heartbeat.
19611
+ // - Per-thread, not per-request: the check carries no request identity. By
19612
+ // the time it fires the thread may be several requests further along —
19613
+ // it always evaluates fresh SQL + live flags, never a stale snapshot.
19614
+ // - Strictly second line: the staleness ceiling is DERIVED from the stream
19615
+ // timeouts (resolveWatchdogStaleCeilingMs = max(first-chunk, stall) + 60s)
19616
+ // so the in-flight guards always get to act first. The watchdog only
19617
+ // moves when the guard that should have fired didn't.
19618
+ // - Restart proof: isExecuting is volatile; an incomplete log with
19619
+ // isExecuting === false is PROOF the executor died (executions cannot
19620
+ // survive a restart). No heartbeat needed for that branch.
19621
+ // - Nonce re-read: every await inside the check is a yield point; state is
19622
+ // re-read immediately before acting so a healthy thread racing the
19623
+ // watchdog always wins.
19624
+ // ══════════════════════════════════════════════════════════════════════════
19625
+ /** Heartbeat: called by LLMRequest on each attempt and streamed chunk. */
19626
+ noteExecutionProgress() {
19627
+ this.lastProgressAt = Date.now();
19628
+ }
19629
+ /** Arm the standing watchdog for an execution start. One pending row max. */
19630
+ async armExecutionWatchdog(threadId) {
19631
+ try {
19632
+ this.lastProgressAt = Date.now();
19633
+ this.watchdogWedgeStrikes = 0;
19634
+ const pending = (await this.ctx.storage.sql.exec(
19635
+ `SELECT id FROM alarm_queue WHERE method = 'watchdogCheck' AND status = 'pending' LIMIT 1`
19636
+ )).toArray();
19637
+ if (pending.length > 0) return;
19638
+ await this.alarmQueue.enqueue("watchdogCheck", { threadId }, this.watchdogIntervalMs);
19639
+ } catch (error) {
19640
+ console.error("[DurableThread] Failed to arm execution watchdog (non-fatal):", error);
19641
+ }
19642
+ }
19643
+ /** Disarm on clean execution end. Completed check rows are noise, not history. */
19644
+ async disarmExecutionWatchdog() {
19645
+ try {
19646
+ await this.ctx.storage.sql.exec(
19647
+ `DELETE FROM alarm_queue WHERE method = 'watchdogCheck' AND status IN ('pending', 'completed')`
19648
+ );
19649
+ await this.alarmQueue.ensureAlarmScheduled();
19650
+ } catch (error) {
19651
+ console.error("[DurableThread] Failed to disarm execution watchdog (non-fatal):", error);
19652
+ }
19653
+ }
19654
+ /**
19655
+ * The standing check. Evaluates the thread's state AT FIRE TIME — it knows
19656
+ * nothing about whichever request was in flight when it was armed.
19657
+ */
19658
+ async watchdogCheck(threadId) {
19659
+ const rearm = () => this.alarmQueue.enqueue("watchdogCheck", { threadId }, this.watchdogIntervalMs);
19660
+ try {
19661
+ await this.ctx.storage.sql.exec(
19662
+ `DELETE FROM alarm_queue WHERE method = 'watchdogCheck' AND status = 'completed'`
19663
+ );
19664
+ const readIncomplete = async () => (await this.ctx.storage.sql.exec(
19665
+ `SELECT id FROM logs WHERE is_complete = 0`
19666
+ )).toArray();
19667
+ let incomplete = await readIncomplete();
19668
+ if (this.isExecuting) {
19669
+ if (incomplete.length === 0) {
19670
+ this.watchdogWedgeStrikes = 0;
19671
+ await rearm();
19672
+ return;
19673
+ }
19674
+ const ceiling = resolveWatchdogStaleCeilingMs(this.env);
19675
+ const silenceMs = Date.now() - this.lastProgressAt;
19676
+ if (silenceMs <= ceiling) {
19677
+ this.watchdogWedgeStrikes = 0;
19678
+ await rearm();
19679
+ return;
19680
+ }
19681
+ this.watchdogWedgeStrikes++;
19682
+ if (this.watchdogWedgeStrikes < 2) {
19683
+ console.warn(
19684
+ `[DurableThread] Watchdog: execution silent for ${Math.round(silenceMs / 1e3)}s (ceiling ${Math.round(ceiling / 1e3)}s) \u2014 aborting in-flight request`
19685
+ );
19686
+ try {
19687
+ this.currentAbortController?.abort();
19688
+ } catch {
19689
+ }
19690
+ await rearm();
19691
+ return;
19692
+ }
19693
+ console.error(
19694
+ "[DurableThread] Watchdog: wedged execution did not unwind after abort \u2014 forcing takeover"
19695
+ );
19696
+ this.isExecuting = false;
19697
+ this.currentAbortController = null;
19698
+ }
19699
+ incomplete = await readIncomplete();
19700
+ if (this.isExecuting) {
19701
+ await rearm();
19702
+ return;
19703
+ }
19704
+ if (incomplete.length > 0) {
19705
+ await this.finalizeLostExecutionLogs(incomplete.map((row) => row.id));
19706
+ }
19707
+ const stopped = await this.shouldStop();
19708
+ const queued = (await this.ctx.storage.sql.exec(
19709
+ `SELECT COUNT(*) AS c FROM message_queue`
19710
+ )).toArray()[0]?.c ?? 0;
19711
+ if (stopped || incomplete.length === 0 && queued === 0) {
19712
+ await this.disarmExecutionWatchdog();
19713
+ return;
19714
+ }
19715
+ if (await this.consumeWatchdogResumeBudget()) {
19716
+ const side = await this.getLatestQueuedMessageSide() ?? "a";
19717
+ await this.alarmQueue.enqueue("continueExecution", { threadId, side }, 0);
19718
+ console.warn(
19719
+ `[DurableThread] Watchdog: lost execution recovered \u2014 resuming thread ${threadId}`
19720
+ );
19721
+ } else {
19722
+ console.error(
19723
+ `[DurableThread] Watchdog: resume budget exhausted for thread ${threadId} \u2014 leaving thread stopped`
19724
+ );
19725
+ }
19726
+ await this.disarmExecutionWatchdog();
19727
+ } catch (error) {
19728
+ console.error("[DurableThread] Watchdog check failed (non-fatal):", error);
19729
+ try {
19730
+ await rearm();
19731
+ } catch {
19732
+ }
19733
+ }
19734
+ }
19735
+ /** Mark logs the dead executor abandoned, and tell connected clients. */
19736
+ async finalizeLostExecutionLogs(logIds) {
19737
+ const error = "Request lost \u2014 the executor restarted (deploy or eviction) or hung before finishing. The thread watchdog finalized it.";
19738
+ const errorType = "execution_lost";
19739
+ await this.ctx.storage.sql.exec(
19740
+ `UPDATE logs SET is_complete = 1, error = ?1, error_type = ?2 WHERE is_complete = 0`,
19741
+ error,
19742
+ errorType
19743
+ );
19744
+ for (const id of logIds) {
19745
+ this.broadcastLog({
19746
+ type: "log_data",
19747
+ log_id: id,
19748
+ data: { id, is_complete: true, error, error_type: errorType }
19749
+ });
19750
+ }
19751
+ }
19752
+ /** At most N auto-resumes per rolling window, tracked durably. */
19753
+ async consumeWatchdogResumeBudget() {
19754
+ const now = Date.now();
19755
+ let count = 0;
19756
+ let windowStart = now;
19757
+ const row = (await this.ctx.storage.sql.exec(
19758
+ `SELECT value FROM execution_state WHERE key = 'watchdog_resume_state'`
19759
+ )).toArray()[0];
19760
+ if (row) {
19761
+ try {
19762
+ const parsed = JSON.parse(row.value);
19763
+ if (typeof parsed.count === "number" && typeof parsed.windowStart === "number" && now - parsed.windowStart < this.watchdogResumeWindowMs) {
19764
+ count = parsed.count;
19765
+ windowStart = parsed.windowStart;
19766
+ }
19767
+ } catch {
19768
+ }
19769
+ }
19770
+ if (count >= this.watchdogMaxResumesPerWindow) return false;
19771
+ const value = JSON.stringify({ count: count + 1, windowStart });
19772
+ await this.ctx.storage.sql.exec(
19773
+ `INSERT INTO execution_state (key, value) VALUES ('watchdog_resume_state', ?1)
19774
+ ON CONFLICT(key) DO UPDATE SET value = ?1`,
19775
+ value
19776
+ );
19777
+ return true;
19778
+ }
19564
19779
  normalizePlainRecord(value) {
19565
19780
  if (!value || typeof value !== "object" || Array.isArray(value)) {
19566
19781
  return null;
@@ -19586,6 +19801,7 @@ ${resultContent}${attachmentSummary}`;
19586
19801
  await this.assertNotTerminated();
19587
19802
  this.isExecuting = true;
19588
19803
  this.currentAbortController = new AbortController();
19804
+ await this.armExecutionWatchdog(threadId);
19589
19805
  try {
19590
19806
  const agentDef = await this.loadAgent(agentName);
19591
19807
  const packageId = agentDef.__package?.packageId;
@@ -19716,6 +19932,7 @@ ${resultContent}${attachmentSummary}`;
19716
19932
  } finally {
19717
19933
  this.isExecuting = false;
19718
19934
  this.currentAbortController = null;
19935
+ await this.disarmExecutionWatchdog();
19719
19936
  }
19720
19937
  }
19721
19938
  /**
@@ -19762,6 +19979,7 @@ ${resultContent}${attachmentSummary}`;
19762
19979
  }
19763
19980
  this.isExecuting = true;
19764
19981
  this.currentAbortController = new AbortController();
19982
+ await this.armExecutionWatchdog(threadId);
19765
19983
  try {
19766
19984
  const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
19767
19985
  const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
@@ -19900,6 +20118,7 @@ ${resultContent}${attachmentSummary}`;
19900
20118
  } finally {
19901
20119
  this.isExecuting = false;
19902
20120
  this.currentAbortController = null;
20121
+ await this.disarmExecutionWatchdog();
19903
20122
  }
19904
20123
  }
19905
20124
  /**
@@ -19910,6 +20129,7 @@ ${resultContent}${attachmentSummary}`;
19910
20129
  await this.assertNotTerminated();
19911
20130
  this.isExecuting = true;
19912
20131
  this.currentAbortController = new AbortController();
20132
+ await this.armExecutionWatchdog(threadId);
19913
20133
  try {
19914
20134
  const agentBuilderId = this.env.AGENT_BUILDER.idFromName("singleton");
19915
20135
  const agentBuilder = this.env.AGENT_BUILDER.get(agentBuilderId);
@@ -20018,6 +20238,7 @@ ${resultContent}${attachmentSummary}`;
20018
20238
  } finally {
20019
20239
  this.isExecuting = false;
20020
20240
  this.currentAbortController = null;
20241
+ await this.disarmExecutionWatchdog();
20021
20242
  const latestQueuedSide = await this.getLatestQueuedMessageSide();
20022
20243
  if (latestQueuedSide) {
20023
20244
  await this.ensureQueuedMessageExecution(threadId, latestQueuedSide);