@polpo-ai/node 0.18.6 → 0.18.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"loop-engine.d.ts","sourceRoot":"","sources":["../../src/adapters/loop-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAc,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,IAAI,CAAC;AAuCZ,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAkDjC;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,GAAG,YAAY,CAwB7E;AA8CD;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAkB,CAAC;AAiCpD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,GAAG,WAAW,CAunClH"}
1
+ {"version":3,"file":"loop-engine.d.ts","sourceRoot":"","sources":["../../src/adapters/loop-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAc,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,IAAI,CAAC;AA8CZ,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAkDjC;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,GAAG,YAAY,CAwB7E;AA8CD;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAkB,CAAC;AAiCpD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,GAAG,WAAW,CAy5ClH"}
@@ -29,6 +29,7 @@ import { generateText, tool as aiTool, } from "ai";
29
29
  import { normalizeResponseMessagesForHistory, prepareModelMessagesForTransport, runModelPolicyTurn, isStructuredModelOutputError, modelOutputForJsonSchema, classifyRuntimeError, markToolStrictness, toValidatedToolInputSchema, validateToolInput, } from "@polpo-ai/llm";
30
30
  import { cleanupAgentBrowserSession } from "@polpo-ai/tools";
31
31
  import { LoopRunner, LoopToolInputValidationError, PipelineExecutor, buildLoopStepAgent, loopContextPrompt, loopAgentInputPrompt, maybeParseJson, LoopAgentOutputError, prepareLoopAgentOutput, normalizeProjectLoop, normalizeToolInput, protectRuntimeToolResultMessages, renderRuntimeToolResult, resolveLoopSelection, compactIfNeeded, extractToolUsageRecord, } from "@polpo-ai/core";
32
+ import { runtimeHookFileChangeForTool } from "@polpo-ai/core/runtime-hooks";
32
33
  import { InMemorySteeringController, } from "@polpo-ai/core/steering";
33
34
  import { projectLoopConfigSchema } from "@polpo-ai/core/schemas";
34
35
  import { NodeFileSystem } from "./node-filesystem.js";
@@ -220,6 +221,72 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
220
221
  const sink = handle.onTranscript ?? ctx?.onTranscript;
221
222
  sink?.(entry);
222
223
  };
224
+ const runtimeHooks = ctx?.inject?.runtimeHooks ?? ctx?.runtimeHooks;
225
+ const dispatchRuntimeHook = async (event, data = {}, identity = {}) => {
226
+ if (!runtimeHooks)
227
+ return { action: "allow" };
228
+ const result = await runtimeHooks.dispatch({
229
+ event,
230
+ surface: ctx?.inject?.runtimeHookSurface
231
+ ?? ctx?.runtimeHookSurface
232
+ ?? (ctx?.inject ? "chat" : "task"),
233
+ identity: {
234
+ runId: ctx?.runId ?? task.id,
235
+ sessionId: ctx?.inject?.sessionId,
236
+ agent: agentConfig.name,
237
+ ...identity,
238
+ },
239
+ data,
240
+ });
241
+ return result.decision;
242
+ };
243
+ const decisionError = (decision) => {
244
+ if (decision.action === "allow" || decision.action === "modify")
245
+ return undefined;
246
+ const reason = "reason" in decision ? decision.reason : "Runtime hook stopped execution";
247
+ return new Error(`Runtime hook ${decision.action}: ${reason ?? "stopped"}`);
248
+ };
249
+ const runHookedModel = async (data, operation, resultData = () => ({})) => {
250
+ const before = await dispatchRuntimeHook("model:before", data);
251
+ const beforeError = decisionError(before);
252
+ if (beforeError)
253
+ throw beforeError;
254
+ try {
255
+ const result = await operation();
256
+ const after = await dispatchRuntimeHook("model:after", { ...data, ...resultData(result) });
257
+ const afterError = decisionError(after);
258
+ if (afterError)
259
+ throw afterError;
260
+ return result;
261
+ }
262
+ catch (error) {
263
+ await dispatchRuntimeHook("model:error", {
264
+ ...data,
265
+ error: error instanceof Error ? error.message : String(error),
266
+ });
267
+ throw error;
268
+ }
269
+ };
270
+ const dispatchModelPolicyHook = async (event, identity) => {
271
+ if (event.type !== "model-fallback-selected")
272
+ return;
273
+ const { raw: _raw, ...reason } = event.reason;
274
+ const data = {
275
+ from: event.from.model,
276
+ to: event.to.model,
277
+ fromIndex: event.from.index,
278
+ toIndex: event.to.index,
279
+ reason,
280
+ };
281
+ const before = await dispatchRuntimeHook("model:switch:before", data, identity);
282
+ const beforeError = decisionError(before);
283
+ if (beforeError)
284
+ throw beforeError;
285
+ const after = await dispatchRuntimeHook("model:switch:after", data, identity);
286
+ const afterError = decisionError(after);
287
+ if (afterError)
288
+ throw afterError;
289
+ };
223
290
  /**
224
291
  * Execute one tool call: dispatch, activity tracking, outcome collection,
225
292
  * toolUsage harvesting, transcript. Shared by LLM-session tool execution
@@ -232,6 +299,23 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
232
299
  };
233
300
  let isError = false;
234
301
  let executionError;
302
+ const before = await dispatchRuntimeHook("tool:before", {
303
+ tool: toolCall.name,
304
+ input: toolCall.args,
305
+ }, { toolCallId: toolCall.id });
306
+ const beforeError = decisionError(before);
307
+ if (beforeError) {
308
+ isError = true;
309
+ executionError = beforeError;
310
+ result = { content: [{ type: "text", text: beforeError.message }], details: {} };
311
+ return { llmText: beforeError.message, isError, error: executionError };
312
+ }
313
+ const effectiveArgs = before.action === "modify"
314
+ && before.data.input
315
+ && typeof before.data.input === "object"
316
+ && !Array.isArray(before.data.input)
317
+ ? before.data.input
318
+ : toolCall.args;
235
319
  const dispatch = async (args) => {
236
320
  if (!pt) {
237
321
  isError = true;
@@ -257,15 +341,38 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
257
341
  };
258
342
  }
259
343
  }
260
- return result.content
344
+ let rendered = result.content
261
345
  .map((content) => content.type === "text" ? content.text : `[image: ${content.mimeType}]`)
262
346
  .join("\n");
347
+ const after = await dispatchRuntimeHook(isError ? "tool:error" : "tool:after", {
348
+ tool: toolCall.name,
349
+ input: args,
350
+ result: rendered,
351
+ }, { toolCallId: toolCall.id });
352
+ const afterError = decisionError(after);
353
+ if (afterError) {
354
+ isError = true;
355
+ executionError = afterError;
356
+ rendered = afterError.message;
357
+ result = { content: [{ type: "text", text: rendered }], details: {} };
358
+ }
359
+ else if (after.action === "modify" && typeof after.data.result === "string") {
360
+ rendered = after.data.result;
361
+ result = { ...result, content: [{ type: "text", text: rendered }] };
362
+ }
363
+ if (!isError) {
364
+ const fileChange = runtimeHookFileChangeForTool(toolCall.name, args, rendered);
365
+ if (fileChange) {
366
+ await dispatchRuntimeHook("sandbox:file_changed", fileChange, { toolCallId: toolCall.id });
367
+ }
368
+ }
369
+ return rendered;
263
370
  };
264
371
  const llmText = ctx?.runToolMiddleware
265
372
  ? (await ctx.runToolMiddleware.execute({
266
373
  callId: toolCall.id,
267
374
  name: toolCall.name,
268
- args: toolCall.args,
375
+ args: effectiveArgs,
269
376
  schema: pt?.parameters,
270
377
  context: {
271
378
  agent: agentConfig.name,
@@ -276,7 +383,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
276
383
  },
277
384
  signal: abortController.signal,
278
385
  }, (request) => dispatch(request.args))).output
279
- : await dispatch(toolCall.args);
386
+ : await dispatch(effectiveArgs);
280
387
  activity.lastUpdate = new Date().toISOString();
281
388
  // Track file operations from tool details
282
389
  const details = result.details;
@@ -327,9 +434,42 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
327
434
  async function runLoopSession(options) {
328
435
  const { sessionAgent, loopName, loop, contextPrompt } = options;
329
436
  let sessionCleanup;
437
+ let agentHookStarted = false;
438
+ let sandboxHookStarted = false;
330
439
  try {
331
440
  const resume = usableResumeState(options.resume, loopName);
332
441
  const inject = ctx?.inject;
442
+ await dispatchRuntimeHook("run:start", { loop: loopName, resumed: Boolean(resume) }, { agent: sessionAgent.name, loop: loopName });
443
+ await dispatchRuntimeHook(resume ? "session:resume" : "session:start", {
444
+ loop: loopName,
445
+ resumed: Boolean(resume),
446
+ }, { agent: sessionAgent.name, loop: loopName });
447
+ const spawnDecision = await dispatchRuntimeHook("agent:spawn", {
448
+ loop: loopName,
449
+ source: inject ? "chat-run" : "task-run",
450
+ }, { agent: sessionAgent.name, loop: loopName });
451
+ const spawnError = decisionError(spawnDecision);
452
+ if (spawnError)
453
+ throw spawnError;
454
+ agentHookStarted = true;
455
+ if (ctx?.fs || ctx?.shell) {
456
+ const sandboxDecision = await dispatchRuntimeHook("sandbox:start", {
457
+ cwd,
458
+ filesystem: Boolean(ctx.fs),
459
+ shell: Boolean(ctx.shell),
460
+ }, { agent: sessionAgent.name, loop: loopName });
461
+ const sandboxError = decisionError(sandboxDecision);
462
+ if (sandboxError)
463
+ throw sandboxError;
464
+ sandboxHookStarted = true;
465
+ }
466
+ const inputDecision = await dispatchRuntimeHook("input:before", {
467
+ loop: loopName,
468
+ hasProjectedInput: Boolean(options.initialUserMessage),
469
+ }, { agent: sessionAgent.name, loop: loopName });
470
+ const inputError = decisionError(inputDecision);
471
+ if (inputError)
472
+ throw inputError;
333
473
  // Inject path (chat-via-executeRun, F1c): use the completions route's
334
474
  // already-resolved model/prompt/tools/messages instead of re-resolving from
335
475
  // the AgentConfig — parity by construction. Every branch below is
@@ -371,6 +511,9 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
371
511
  toolSet = toToolDeclarations(allPolpoTools);
372
512
  compactionTools = allPolpoTools.map((t) => ({ description: t.description ?? "" }));
373
513
  }
514
+ if (inputDecision.action === "modify" && typeof inputDecision.data.context === "string" && inputDecision.data.context.trim()) {
515
+ systemPrompt += `\n\n## Runtime hook context\n\n${inputDecision.data.context.trim()}`;
516
+ }
374
517
  const primaryResolved = { model, providerOptions };
375
518
  const modelSelection = inject?.modelSelection ??
376
519
  resolvedModelSelection ??
@@ -441,7 +584,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
441
584
  const isRecovery = attemptIndex === 1;
442
585
  const resolvedAttempts = new Map();
443
586
  try {
444
- const terminal = await runModelPolicyTurn({
587
+ const terminal = await runHookedModel({ phase: "terminal", recovery: isRecovery, model: model.id ?? model.provider }, () => runModelPolicyTurn({
445
588
  selection: modelSelection,
446
589
  resolveAttempt: async (attempt) => {
447
590
  const resolvedAttempt = attempt.index === 0
@@ -461,13 +604,17 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
461
604
  };
462
605
  },
463
606
  preserveSingleAttemptError: true,
607
+ onPolicyEvent: (event) => dispatchModelPolicyHook(event, {
608
+ agent: sessionAgent.name,
609
+ loop: loopName,
610
+ }),
464
611
  allowCommittedFallback: true,
465
612
  maxRecoverableStreamRetries: 0,
466
613
  system: systemPrompt,
467
614
  messages: terminalMessages(isRecovery),
468
615
  output: modelOutput,
469
616
  abortSignal: abortController.signal,
470
- });
617
+ }), (result) => ({ finishReason: result.finishReason, toolCallCount: result.toolCalls?.length ?? 0 }));
471
618
  if (terminal.toolCalls.length > 0 || terminal.output === undefined) {
472
619
  throw new Error(terminal.toolCalls.length > 0
473
620
  ? "Terminal structured-output phase returned a tool call"
@@ -551,6 +698,10 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
551
698
  }
552
699
  // Context compaction — same call, same config, same quirks as the
553
700
  // legacy engine (see engine-behavior characterization tests).
701
+ await dispatchRuntimeHook("context:compact:before", {
702
+ messageCount: messages.length,
703
+ model: model.id ?? model.provider,
704
+ });
554
705
  const compactionResult = await compactIfNeeded({
555
706
  systemPrompt,
556
707
  messages,
@@ -577,6 +728,10 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
577
728
  });
578
729
  if (compactionResult.compacted) {
579
730
  messages = normalizeResponseMessagesForHistory(compactionResult.messages);
731
+ await dispatchRuntimeHook("context:compact:after", {
732
+ messageCount: messages.length,
733
+ compacted: true,
734
+ });
580
735
  }
581
736
  messages = normalizeResponseMessagesForHistory(messages);
582
737
  if (contextTrust === "enforce") {
@@ -588,7 +743,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
588
743
  const resolvedAttempts = new Map();
589
744
  let turn;
590
745
  try {
591
- turn = await runModelPolicyTurn({
746
+ turn = await runHookedModel({ phase: "operational", model: model.id ?? model.provider }, () => runModelPolicyTurn({
592
747
  selection: modelSelection,
593
748
  resolveAttempt: async (attempt) => {
594
749
  const resolvedAttempt = attempt.index === 0
@@ -608,6 +763,10 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
608
763
  };
609
764
  },
610
765
  preserveSingleAttemptError: true,
766
+ onPolicyEvent: (event) => dispatchModelPolicyHook(event, {
767
+ agent: sessionAgent.name,
768
+ loop: loopName,
769
+ }),
611
770
  system: systemPrompt,
612
771
  messages,
613
772
  tools: toolSet,
@@ -768,7 +927,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
768
927
  break;
769
928
  }
770
929
  }
771
- });
930
+ }), (result) => ({ finishReason: result.finishReason, toolCallCount: result.toolCalls?.length ?? 0 }));
772
931
  }
773
932
  catch (error) {
774
933
  if (outputSchema !== undefined && isStructuredModelOutputError(error)) {
@@ -919,7 +1078,7 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
919
1078
  model: modelStep,
920
1079
  executeTool,
921
1080
  parallelToolCalls: inject?.parallelToolCalls,
922
- onToolResults: (toolResults) => {
1081
+ onToolResults: async (toolResults) => {
923
1082
  for (const { toolCall, result, isError } of toolResults) {
924
1083
  // AI SDK already persisted provider-executed results and invalid
925
1084
  // argument errors in responseMessages. Only local results belong
@@ -942,6 +1101,18 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
942
1101
  }],
943
1102
  });
944
1103
  }
1104
+ if (toolResults.length > 0) {
1105
+ await dispatchRuntimeHook("tool:batch:after", {
1106
+ count: toolResults.length,
1107
+ failed: toolResults.filter((result) => result.isError).length,
1108
+ tools: toolResults.map(({ toolCall, isError, skipped }) => ({
1109
+ id: toolCall.id,
1110
+ name: toolCall.name,
1111
+ isError,
1112
+ skipped,
1113
+ })),
1114
+ }, { agent: sessionAgent.name, loop: loopName });
1115
+ }
945
1116
  },
946
1117
  steering,
947
1118
  onSteering: async (steeringMessages) => {
@@ -972,9 +1143,118 @@ export function spawnLoopEngine(agentConfig, task, cwd, ctx) {
972
1143
  if (outputSchema !== undefined && !completedStructuredOutput && !interruptedForClientTool) {
973
1144
  throw new LoopAgentOutputError("loop_agent_output_invalid", loopName, `Agent step ${JSON.stringify(loopName)} exhausted maxTurns before producing structured output`, { phase: "max_turns" });
974
1145
  }
1146
+ const stopDecision = await dispatchRuntimeHook("turn:stop", {
1147
+ loop: loopName,
1148
+ textLength: lastStepText.length,
1149
+ }, { agent: sessionAgent.name, loop: loopName });
1150
+ const stopError = decisionError(stopDecision);
1151
+ if (stopError && stopDecision.action !== "stop")
1152
+ throw stopError;
1153
+ const stopFeedback = stopDecision.action === "stop" ? stopDecision.feedback?.trim() : undefined;
1154
+ if (stopFeedback && !hasStructuredOutput && !abortController.signal.aborted) {
1155
+ const resolvedAttempts = new Map();
1156
+ const retry = await runHookedModel({ phase: "stop-feedback", retry: 1, model: model.id ?? model.provider }, () => runModelPolicyTurn({
1157
+ selection: modelSelection,
1158
+ resolveAttempt: async (attempt) => {
1159
+ const resolvedAttempt = attempt.index === 0
1160
+ ? primaryResolved
1161
+ : inject
1162
+ ? await inject.resolveModelAttempt?.(attempt.model)
1163
+ : resolveSpawnModelAttempt(sessionAgent, attempt.model, ctx);
1164
+ if (!resolvedAttempt)
1165
+ throw new Error(`No model resolver is available for fallback "${attempt.model}"`);
1166
+ resolvedAttempts.set(attempt.index, resolvedAttempt);
1167
+ return {
1168
+ model: resolvedAttempt.model.aiModel,
1169
+ maxOutputTokens: resolvedAttempt.model.maxTokens,
1170
+ providerOptions: resolvedAttempt.providerOptions,
1171
+ capabilities: resolvedAttempt.model.capabilities,
1172
+ };
1173
+ },
1174
+ preserveSingleAttemptError: true,
1175
+ onPolicyEvent: (event) => dispatchModelPolicyHook(event, {
1176
+ agent: sessionAgent.name,
1177
+ loop: loopName,
1178
+ }),
1179
+ system: systemPrompt,
1180
+ messages: [
1181
+ ...messages,
1182
+ {
1183
+ role: "user",
1184
+ content: `A trusted runtime hook rejected ending the turn and returned this feedback:\n\n${stopFeedback}\n\nContinue once and return the corrected final response. Do not repeat completed side effects.`,
1185
+ },
1186
+ ],
1187
+ tools: {},
1188
+ toolChoice: "none",
1189
+ abortSignal: abortController.signal,
1190
+ }), (result) => ({ finishReason: result.finishReason, toolCallCount: result.toolCalls?.length ?? 0 }));
1191
+ if (retry.toolCalls.length > 0)
1192
+ throw new Error("Stop-feedback recovery returned a tool call");
1193
+ const selectedResolved = resolvedAttempts.get(retry.selectedAttempt.index);
1194
+ if (selectedResolved) {
1195
+ model = selectedResolved.model;
1196
+ providerOptions = selectedResolved.providerOptions;
1197
+ }
1198
+ let retryText = retry.text;
1199
+ if (retryText && ctx?.runOutputPolicy) {
1200
+ retryText = (await ctx.runOutputPolicy.evaluate({
1201
+ output: retryText,
1202
+ mode: ctx.runOutputPolicyMode ?? "enforce",
1203
+ context: { agent: sessionAgent.name, runId: ctx.runId, source: "task", surface: "task" },
1204
+ signal: abortController.signal,
1205
+ })).output;
1206
+ }
1207
+ if (retryText) {
1208
+ try {
1209
+ ctx?.onDelta?.({ text: `${lastStepText ? "\n\n" : ""}${retryText}` });
1210
+ }
1211
+ catch { /* subscriber cannot sink the run */ }
1212
+ emitTranscript({ type: "assistant", text: retryText });
1213
+ accumText += retryText;
1214
+ lastStepText = `${lastStepText ? `${lastStepText}\n\n` : ""}${retryText}`;
1215
+ }
1216
+ if (inject)
1217
+ emitTranscript({ type: "usage", usage: retry.usage, providerMetadata: retry.providerMetadata });
1218
+ const retryStop = await dispatchRuntimeHook("turn:stop", {
1219
+ loop: loopName,
1220
+ textLength: lastStepText.length,
1221
+ stopFeedbackRetry: 1,
1222
+ }, { agent: sessionAgent.name, loop: loopName });
1223
+ const retryStopError = decisionError(retryStop);
1224
+ if (retryStopError && retryStop.action !== "stop")
1225
+ throw retryStopError;
1226
+ }
975
1227
  return { lastText: lastStepText, accumText };
976
1228
  }
1229
+ catch (error) {
1230
+ await dispatchRuntimeHook("turn:error", {
1231
+ loop: loopName,
1232
+ error: error instanceof Error ? error.message : String(error),
1233
+ }, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
1234
+ throw error;
1235
+ }
977
1236
  finally {
1237
+ if (sandboxHookStarted) {
1238
+ await dispatchRuntimeHook("sandbox:end", {
1239
+ cwd,
1240
+ aborted: abortController.signal.aborted,
1241
+ }, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
1242
+ }
1243
+ if (agentHookStarted) {
1244
+ await dispatchRuntimeHook("agent:end", {
1245
+ loop: loopName,
1246
+ source: ctx?.inject ? "chat-run" : "task-run",
1247
+ aborted: abortController.signal.aborted,
1248
+ }, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
1249
+ }
1250
+ await dispatchRuntimeHook("session:end", {
1251
+ loop: loopName,
1252
+ aborted: abortController.signal.aborted,
1253
+ }, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
1254
+ await dispatchRuntimeHook("run:end", {
1255
+ loop: loopName,
1256
+ aborted: abortController.signal.aborted,
1257
+ }, { agent: sessionAgent.name, loop: loopName }).catch(() => { });
978
1258
  if (sessionCleanup) {
979
1259
  await sessionCleanup().catch(() => undefined);
980
1260
  toolCleanups.delete(sessionCleanup);